@xmoxmo/bncr 0.3.6 → 0.3.7

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 (164) hide show
  1. package/README.md +5 -0
  2. package/dist/index.js +28 -5
  3. package/index.ts +55 -721
  4. package/package.json +8 -4
  5. package/scripts/check-pack.mjs +93 -18
  6. package/scripts/check-register-drift.mjs +35 -13
  7. package/scripts/selfcheck.mjs +80 -11
  8. package/src/bootstrap/channel-plugin-runtime.ts +81 -0
  9. package/src/bootstrap/cli.ts +97 -0
  10. package/src/bootstrap/register-runtime-gateway.ts +129 -0
  11. package/src/bootstrap/register-runtime-helpers.ts +140 -0
  12. package/src/bootstrap/register-runtime-singleton.ts +137 -0
  13. package/src/bootstrap/register-runtime.ts +201 -0
  14. package/src/bootstrap/runtime-discovery.ts +187 -0
  15. package/src/bootstrap/runtime-loader.ts +54 -0
  16. package/src/channel.ts +1590 -4967
  17. package/src/core/accounts.ts +23 -4
  18. package/src/core/dead-letter-diagnostics.ts +37 -5
  19. package/src/core/diagnostics.ts +31 -15
  20. package/src/core/downlink-health.ts +3 -11
  21. package/src/core/extended-diagnostics.ts +78 -36
  22. package/src/core/file-transfer-payloads.ts +1 -1
  23. package/src/core/logging.ts +1 -0
  24. package/src/core/outbox-enqueue.ts +13 -2
  25. package/src/core/outbox-entry-builders.ts +2 -0
  26. package/src/core/outbox-summary.ts +75 -3
  27. package/src/core/permissions.ts +15 -2
  28. package/src/core/persisted-outbox-entry.ts +21 -6
  29. package/src/core/policy.ts +45 -4
  30. package/src/core/probe.ts +3 -15
  31. package/src/core/register-trace.ts +3 -3
  32. package/src/core/status.ts +43 -4
  33. package/src/core/targets.ts +216 -205
  34. package/src/core/types.ts +221 -0
  35. package/src/core/value-sanitize.ts +29 -0
  36. package/src/messaging/inbound/commands.ts +147 -172
  37. package/src/messaging/inbound/context-facts.ts +4 -2
  38. package/src/messaging/inbound/contracts.ts +70 -0
  39. package/src/messaging/inbound/dispatch-prep.ts +303 -0
  40. package/src/messaging/inbound/dispatch.ts +49 -462
  41. package/src/messaging/inbound/gate.ts +18 -5
  42. package/src/messaging/inbound/last-route.ts +10 -4
  43. package/src/messaging/inbound/media-url-download.ts +109 -0
  44. package/src/messaging/inbound/native-command-runtime.ts +225 -0
  45. package/src/messaging/inbound/parse.ts +2 -1
  46. package/src/messaging/inbound/remote-media.ts +49 -0
  47. package/src/messaging/inbound/reply-config.ts +16 -4
  48. package/src/messaging/inbound/reply-dispatch.ts +162 -0
  49. package/src/messaging/inbound/runtime-compat.ts +31 -10
  50. package/src/messaging/inbound/session-label.ts +15 -7
  51. package/src/messaging/inbound/turn-context.ts +131 -0
  52. package/src/messaging/outbound/actions.ts +24 -10
  53. package/src/messaging/outbound/diagnostics-debug-builders.ts +365 -0
  54. package/src/messaging/outbound/diagnostics.ts +31 -355
  55. package/src/messaging/outbound/durable-message-adapter.ts +20 -16
  56. package/src/messaging/outbound/durable-queue-adapter.ts +20 -7
  57. package/src/messaging/outbound/media.ts +24 -13
  58. package/src/messaging/outbound/reply-enqueue-media.ts +181 -0
  59. package/src/messaging/outbound/reply-enqueue.ts +46 -155
  60. package/src/messaging/outbound/send-params.ts +3 -0
  61. package/src/messaging/outbound/send.ts +19 -10
  62. package/src/messaging/outbound/session-route.ts +18 -3
  63. package/src/openclaw/channel-runtime-contracts.ts +76 -0
  64. package/src/openclaw/config-runtime.ts +13 -7
  65. package/src/openclaw/inbound-session-runtime.ts +7 -3
  66. package/src/openclaw/ingress-runtime.ts +17 -27
  67. package/src/openclaw/reply-runtime.ts +54 -59
  68. package/src/openclaw/routing-runtime.ts +35 -18
  69. package/src/openclaw/runtime-surface.ts +156 -12
  70. package/src/openclaw/sdk-helpers.ts +8 -1
  71. package/src/openclaw/session-route-runtime.ts +12 -12
  72. package/src/plugin/ack-outbox-runtime-group.ts +264 -0
  73. package/src/plugin/bridge-ack-facade.ts +137 -0
  74. package/src/plugin/bridge-connection-facade.ts +111 -0
  75. package/src/plugin/bridge-diagnostics-facade.ts +23 -0
  76. package/src/plugin/bridge-drain-facade.ts +98 -0
  77. package/src/plugin/bridge-extended-diagnostics-facade.ts +149 -0
  78. package/src/plugin/bridge-file-transfer-push-facade.ts +140 -0
  79. package/src/plugin/bridge-lifecycle.ts +156 -0
  80. package/src/plugin/bridge-media-facade.ts +241 -0
  81. package/src/plugin/bridge-outbox-facade.ts +182 -0
  82. package/src/plugin/bridge-runtime-helpers.ts +266 -0
  83. package/src/plugin/bridge-runtime-snapshots.ts +104 -0
  84. package/src/plugin/bridge-runtime-surface-facade.ts +8 -0
  85. package/src/plugin/bridge-status-facade.ts +76 -0
  86. package/src/plugin/bridge-status-worker-facade.ts +72 -0
  87. package/src/plugin/bridge-support-runtime.ts +137 -0
  88. package/src/plugin/bridge-surface-handlers-group.ts +242 -0
  89. package/src/plugin/bridge-surface-helpers.ts +28 -0
  90. package/src/plugin/capabilities.ts +1 -3
  91. package/src/plugin/channel-components.ts +289 -0
  92. package/src/plugin/channel-inbound-helpers.ts +149 -0
  93. package/src/plugin/channel-plugin-bridge-group.ts +129 -0
  94. package/src/plugin/channel-plugin-surface-group.ts +202 -0
  95. package/src/plugin/channel-runtime-builders-delivery.ts +513 -0
  96. package/src/plugin/channel-runtime-builders-status.ts +331 -0
  97. package/src/plugin/channel-runtime-builders.ts +25 -0
  98. package/src/plugin/channel-runtime-constants.ts +40 -0
  99. package/src/plugin/channel-runtime-types.ts +146 -0
  100. package/src/plugin/channel-send-runtime-group.ts +37 -0
  101. package/src/plugin/channel-send.ts +226 -0
  102. package/src/plugin/channel-utils.ts +102 -0
  103. package/src/plugin/config.ts +24 -3
  104. package/src/plugin/connection-handlers-helpers.ts +254 -0
  105. package/src/plugin/connection-handlers.ts +440 -0
  106. package/src/plugin/connection-state-helpers.ts +159 -0
  107. package/src/plugin/connection-state-runtime-group.ts +51 -0
  108. package/src/plugin/connection-state.ts +527 -0
  109. package/src/plugin/diagnostics-handlers.ts +211 -0
  110. package/src/plugin/error-message.ts +15 -0
  111. package/src/plugin/file-ack-runtime.ts +284 -0
  112. package/src/plugin/file-inbound-abort.ts +112 -0
  113. package/src/plugin/file-inbound-chunk.ts +146 -0
  114. package/src/plugin/file-inbound-complete.ts +153 -0
  115. package/src/plugin/file-inbound-handlers.ts +19 -0
  116. package/src/plugin/file-inbound-init.ts +122 -0
  117. package/src/plugin/file-inbound-runtime.ts +51 -0
  118. package/src/plugin/file-inbound-state.ts +62 -0
  119. package/src/plugin/file-transfer-logs.ts +227 -0
  120. package/src/plugin/file-transfer-orchestrator-chunk.ts +135 -0
  121. package/src/plugin/file-transfer-orchestrator.ts +304 -0
  122. package/src/plugin/file-transfer-runtime-group.ts +102 -0
  123. package/src/plugin/file-transfer-send.ts +89 -0
  124. package/src/plugin/file-transfer-setup.ts +206 -0
  125. package/src/plugin/gateway-event-context.ts +41 -0
  126. package/src/plugin/gateway-runtime.ts +14 -4
  127. package/src/plugin/inbound-acceptance.ts +107 -0
  128. package/src/plugin/inbound-handlers.ts +248 -0
  129. package/src/plugin/inbound-surface-handlers-group.ts +152 -0
  130. package/src/plugin/media-dedupe-runtime.ts +90 -0
  131. package/src/plugin/media-orchestrators-runtime-group.ts +316 -0
  132. package/src/plugin/message-ack-runtime.ts +284 -0
  133. package/src/plugin/message-send.ts +16 -6
  134. package/src/plugin/messaging.ts +98 -36
  135. package/src/plugin/outbound.ts +50 -8
  136. package/src/plugin/outbox-ack-logs.ts +136 -0
  137. package/src/plugin/outbox-ack-outcome.ts +128 -0
  138. package/src/plugin/outbox-drain-ack.ts +145 -0
  139. package/src/plugin/outbox-drain-failure.ts +84 -0
  140. package/src/plugin/outbox-drain-loop.ts +554 -0
  141. package/src/plugin/outbox-drain-post-push.ts +159 -0
  142. package/src/plugin/outbox-drain-runtime.ts +141 -0
  143. package/src/plugin/outbox-drain-schedule.ts +116 -0
  144. package/src/plugin/outbox-file-push-flow.ts +69 -0
  145. package/src/plugin/outbox-push-route-runtime-group.ts +81 -0
  146. package/src/plugin/outbox-push.ts +267 -0
  147. package/src/plugin/outbox-route.ts +181 -0
  148. package/src/plugin/outbox-text-push-flow.ts +90 -0
  149. package/src/plugin/runtime-diagnostics-assembler.ts +183 -0
  150. package/src/plugin/runtime-diagnostics-helpers.ts +302 -0
  151. package/src/plugin/runtime-diagnostics-payload-builders.ts +171 -0
  152. package/src/plugin/runtime-diagnostics-snapshot.ts +31 -0
  153. package/src/plugin/setup.ts +33 -6
  154. package/src/plugin/state-store.ts +249 -0
  155. package/src/plugin/state-transient-runtime-group.ts +105 -0
  156. package/src/plugin/status-runtime.ts +251 -0
  157. package/src/plugin/status.ts +33 -7
  158. package/src/plugin/target-runtime.ts +141 -0
  159. package/src/plugin/target-status-runtime-group.ts +130 -0
  160. package/src/plugin/transient-state-runtime.ts +82 -0
  161. package/src/runtime/outbound-ack-timeout.ts +5 -3
  162. package/src/runtime/outbound-flags.ts +24 -8
  163. package/src/runtime/status-snapshots.ts +36 -7
  164. package/src/runtime/status-worker.ts +34 -4
package/index.ts CHANGED
@@ -1,736 +1,68 @@
1
- import { execFileSync } from 'node:child_process';
2
- import fs from 'node:fs';
3
- import { createRequire } from 'node:module';
4
1
  import path from 'node:path';
5
- import { fileURLToPath } from 'node:url';
2
+ import { createDynamicChannelPlugin } from './src/bootstrap/channel-plugin-runtime.ts';
3
+ import {
4
+ type BncrRegistrationApi,
5
+ registerBncrCli,
6
+ shouldSkipNonRuntimeRegister,
7
+ } from './src/bootstrap/cli.ts';
8
+ import { createBncrRegisterRuntime } from './src/bootstrap/register-runtime.ts';
9
+ import {
10
+ type ChannelModule,
11
+ type LoadedRuntime,
12
+ loadBncrRuntimeSync,
13
+ pluginRoot,
14
+ pluginVersion as runtimePluginVersion,
15
+ } from './src/bootstrap/runtime-loader.ts';
6
16
  import { BncrConfigSchema } from './src/core/config-schema.ts';
7
17
  import { emitBncrLogLine } from './src/core/logging.ts';
8
- import {
9
- getOpenClawRuntimeConfig,
10
- mutateOpenClawRuntimeConfigFile,
11
- } from './src/openclaw/config-runtime.ts';
18
+ import { getOpenClawRuntimeConfig } from './src/openclaw/config-runtime.ts';
12
19
 
13
- const pluginFile = fileURLToPath(import.meta.url);
14
- const pluginDir = path.dirname(pluginFile);
15
- const pluginRequire = createRequire(import.meta.url);
16
- const sdkCoreSpecifier = 'openclaw/plugin-sdk/core';
17
- const linkType = process.platform === 'win32' ? 'junction' : 'dir';
18
-
19
- type ChannelModule = typeof import('./src/channel.ts');
20
- type OpenClawPluginApi = Parameters<ChannelModule['createBncrBridge']>[0];
21
- type BridgeSingleton = ReturnType<ChannelModule['createBncrBridge']>;
22
20
  type ChannelPlugin = ReturnType<ChannelModule['createBncrChannelPlugin']>;
23
21
 
24
- type LoadedRuntime = {
25
- createBncrBridge: ChannelModule['createBncrBridge'];
26
- createBncrChannelPlugin: ChannelModule['createBncrChannelPlugin'];
27
- };
28
-
29
- const BNCR_REGISTER_META = Symbol.for('bncr.register.meta');
30
- const BNCR_GLOBAL_REGISTER_TRACE = Symbol.for('bncr.global.register.trace');
31
- const BNCR_BRIDGE_OWNER = Symbol.for('bncr.bridge.owner');
32
- const BNCR_GATEWAY_RUNTIME = Symbol.for('bncr.gateway.runtime');
33
- const MODULE_EPOCH = `${process.pid}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
34
-
35
- type RegisterMeta = {
36
- service?: boolean;
37
- channel?: boolean;
38
- methods?: Set<string>;
39
- apiInstanceId?: string;
40
- registryFingerprint?: string;
41
- registrationMode?: string;
42
- };
43
-
44
- type GlobalRegisterTrace = {
45
- lastApiInstanceId?: string;
46
- lastRegistryFingerprint?: string;
47
- seenRegistryFingerprints: Set<string>;
48
- seenApiInstanceIds: Set<string>;
49
- };
50
-
51
- type BridgeOwner = {
52
- moduleEpoch: string;
53
- bridgeFactoryId: string;
54
- apiInstanceId: string;
55
- registryFingerprint: string;
56
- registrationMode?: string;
57
- };
58
-
59
- type BridgeRegisterStateSnapshot = {
60
- registerCount: number;
61
- apiGeneration: number;
62
- firstRegisterAt: number | null;
63
- lastRegisterAt: number | null;
64
- lastApiRebindAt: number | null;
65
- pluginSource: string | null;
66
- pluginVersion: string | null;
67
- lastApiInstanceId: string | null;
68
- lastRegistryFingerprint: string | null;
69
- lastDriftSnapshot: unknown;
70
- registerTraceRecent: Array<Record<string, unknown>>;
71
- };
72
-
73
- type GatewayMethodName =
74
- | 'bncr.connect'
75
- | 'bncr.inbound'
76
- | 'bncr.activity'
77
- | 'bncr.ack'
78
- | 'bncr.diagnostics'
79
- | 'bncr.deadLetter.inspect'
80
- | 'bncr.deadLetter.prune'
81
- | 'bncr.file.init'
82
- | 'bncr.file.chunk'
83
- | 'bncr.file.complete'
84
- | 'bncr.file.abort'
85
- | 'bncr.file.ack';
86
-
87
- type BridgeSingletonWithOwner = BridgeSingleton & {
88
- [BNCR_BRIDGE_OWNER]?: BridgeOwner;
89
- registerCount?: number;
90
- apiGeneration?: number;
91
- firstRegisterAt?: number | null;
92
- lastRegisterAt?: number | null;
93
- lastApiRebindAt?: number | null;
94
- pluginSource?: string | null;
95
- pluginVersion?: string | null;
96
- lastApiInstanceId?: string | null;
97
- lastRegistryFingerprint?: string | null;
98
- lastDriftSnapshot?: unknown;
99
- registerTraceRecent?: Array<Record<string, unknown>>;
100
- };
101
-
102
- type OpenClawPluginApiWithMeta = OpenClawPluginApi & {
103
- [BNCR_REGISTER_META]?: RegisterMeta;
104
- };
105
-
106
- type BncrGatewayRuntime = {
107
- currentBridge?: BridgeSingletonWithOwner;
108
- registeredMethodsByRegistry: Map<string, Set<GatewayMethodName>>;
109
- serviceRegistered?: boolean;
110
- channelRegistered?: boolean;
111
- serviceOwnerApiInstanceId?: string;
112
- channelOwnerApiInstanceId?: string;
113
- };
114
-
115
- let runtime: LoadedRuntime | null = null;
116
- const identityIds = new WeakMap<object, string>();
117
- let identitySeq = 0;
118
-
119
- const tryExec = (command: string, args: string[]) => {
120
- try {
121
- return execFileSync(command, args, {
122
- encoding: 'utf8',
123
- stdio: ['ignore', 'pipe', 'ignore'],
124
- }).trim();
125
- } catch {
126
- return '';
127
- }
128
- };
129
-
130
- const readOpenClawPackageName = (pkgPath: string) => {
131
- try {
132
- const raw = fs.readFileSync(pkgPath, 'utf8');
133
- const parsed = JSON.parse(raw);
134
- return typeof parsed?.name === 'string' ? parsed.name : '';
135
- } catch {
136
- return '';
137
- }
138
- };
139
-
140
- const readPluginVersion = () => {
141
- try {
142
- const raw = fs.readFileSync(path.join(pluginDir, 'package.json'), 'utf8');
143
- const parsed = JSON.parse(raw);
144
- return typeof parsed?.version === 'string' ? parsed.version : 'unknown';
145
- } catch {
146
- return 'unknown';
147
- }
22
+ const readPluginVersion = (rootDir = pluginRoot) => {
23
+ const packageJsonPath = path.join(rootDir, 'package.json');
24
+ void packageJsonPath;
25
+ return runtimePluginVersion;
148
26
  };
149
27
 
150
28
  const pluginVersion = readPluginVersion();
151
29
 
152
- const findOpenClawPackageRoot = (startPath: string) => {
153
- let current = startPath;
154
- try {
155
- current = fs.realpathSync(startPath);
156
- } catch {
157
- // keep original path when realpath fails
158
- }
159
-
160
- let cursor = current;
161
- while (true) {
162
- const statPath = fs.existsSync(cursor) ? cursor : path.dirname(cursor);
163
- const pkgPath = path.join(statPath, 'package.json');
164
- if (fs.existsSync(pkgPath) && readOpenClawPackageName(pkgPath) === 'openclaw') {
165
- return statPath;
166
- }
167
- const parent = path.dirname(statPath);
168
- if (parent === statPath) break;
169
- cursor = parent;
170
- }
171
- return '';
172
- };
173
-
174
- const unique = (items: string[]) => {
175
- const seen = new Set<string>();
176
- const out: string[] = [];
177
- for (const item of items) {
178
- if (!item) continue;
179
- const normalized = path.normalize(item);
180
- if (seen.has(normalized)) continue;
181
- seen.add(normalized);
182
- out.push(normalized);
183
- }
184
- return out;
185
- };
186
-
187
- const collectOpenClawCandidates = () => {
188
- const directCandidates = [
189
- path.join(pluginDir, 'node_modules', 'openclaw'),
190
- path.join('/usr/lib/node_modules', 'openclaw'),
191
- path.join('/usr/local/lib/node_modules', 'openclaw'),
192
- path.join('/opt/homebrew/lib/node_modules', 'openclaw'),
193
- path.join(process.env.HOME || '', '.npm-global/lib/node_modules', 'openclaw'),
194
- ];
195
-
196
- const npmRoot = tryExec('npm', ['root', '-g']);
197
- if (npmRoot) directCandidates.push(path.join(npmRoot, 'openclaw'));
198
-
199
- const nodePathEntries = (process.env.NODE_PATH || '')
200
- .split(path.delimiter)
201
- .map((entry) => entry.trim())
202
- .filter(Boolean);
203
- for (const entry of nodePathEntries) {
204
- directCandidates.push(path.join(entry, 'openclaw'));
205
- }
206
-
207
- const openclawBin = tryExec('which', ['openclaw']);
208
- if (openclawBin) {
209
- directCandidates.push(openclawBin);
210
- directCandidates.push(path.dirname(openclawBin));
211
- }
212
-
213
- const packageRoots = unique(
214
- directCandidates.map((candidate) => findOpenClawPackageRoot(candidate)).filter(Boolean),
215
- );
216
-
217
- return packageRoots.filter((candidate) => {
218
- const pkgJson = path.join(candidate, 'package.json');
219
- return fs.existsSync(pkgJson) && readOpenClawPackageName(pkgJson) === 'openclaw';
220
- });
221
- };
222
-
223
- const canResolveSdkCore = () => {
224
- try {
225
- pluginRequire.resolve(sdkCoreSpecifier);
226
- return true;
227
- } catch {
228
- return false;
229
- }
230
- };
231
-
232
- const ensurePluginNodeModulesLink = (targetRoot: string) => {
233
- const nodeModulesDir = path.join(pluginDir, 'node_modules');
234
- const linkPath = path.join(nodeModulesDir, 'openclaw');
235
- fs.mkdirSync(nodeModulesDir, { recursive: true });
236
-
237
- try {
238
- const stat = fs.lstatSync(linkPath);
239
- if (stat.isSymbolicLink()) {
240
- const existingTarget = fs.realpathSync(linkPath);
241
- const normalizedExisting = path.normalize(existingTarget);
242
- const normalizedTarget = path.normalize(fs.realpathSync(targetRoot));
243
- if (normalizedExisting === normalizedTarget) return;
244
- fs.unlinkSync(linkPath);
245
- } else {
246
- return;
247
- }
248
- } catch {
249
- // missing link is fine
250
- }
251
-
252
- fs.symlinkSync(targetRoot, linkPath, linkType as fs.symlink.Type);
253
- };
254
-
255
- const runtimeSourceDir = (() => {
256
- const direct = path.join(pluginDir, 'src');
257
- if (fs.existsSync(path.join(direct, 'channel.ts'))) return direct;
258
-
259
- const parent = path.join(pluginDir, '..', 'src');
260
- if (fs.existsSync(path.join(parent, 'channel.ts'))) return parent;
261
-
262
- return direct;
263
- })();
264
-
265
- const ensureOpenClawSdkResolution = () => {
266
- if (canResolveSdkCore()) return;
267
-
268
- let lastError = '';
269
- const candidates = collectOpenClawCandidates();
270
- for (const candidate of candidates) {
271
- try {
272
- ensurePluginNodeModulesLink(candidate);
273
- if (canResolveSdkCore()) return;
274
- } catch (error) {
275
- lastError = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
276
- }
277
- }
278
-
279
- const suffix = candidates.length
280
- ? ` Tried candidates: ${candidates.join(', ')}.`
281
- : ' No openclaw package root candidates were found from npm root, NODE_PATH, common global paths, or the openclaw binary path.';
282
- const extra = lastError ? ` Last repair error: ${lastError}.` : '';
283
- throw new Error(
284
- `bncr failed to resolve ${sdkCoreSpecifier} from ${pluginDir}.${suffix}${extra} ` +
285
- `You can repair manually with: mkdir -p ${path.join(pluginDir, 'node_modules')} && ln -s "$(npm root -g)/openclaw" ${path.join(pluginDir, 'node_modules', 'openclaw')}`,
286
- );
287
- };
288
-
289
- const loadRuntimeSync = (): LoadedRuntime => {
290
- if (runtime) return runtime;
291
- ensureOpenClawSdkResolution();
292
- try {
293
- const mod = pluginRequire(path.join(runtimeSourceDir, 'channel.ts')) as ChannelModule;
294
- runtime = {
295
- createBncrBridge: mod.createBncrBridge,
296
- createBncrChannelPlugin: mod.createBncrChannelPlugin,
297
- };
298
- return runtime;
299
- } catch (error) {
300
- const detail = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
301
- throw new Error(
302
- `bncr failed to load channel runtime after dependency bootstrap from ${runtimeSourceDir}: ${detail}`,
303
- );
304
- }
305
- };
306
-
307
- const getIdentityId = (obj: object, prefix: string) => {
308
- const existing = identityIds.get(obj);
309
- if (existing) return existing;
310
- const next = `${prefix}_${MODULE_EPOCH}_${++identitySeq}`;
311
- identityIds.set(obj, next);
312
- return next;
313
- };
314
-
315
- const getRegistryFingerprint = (api: OpenClawPluginApi) => {
316
- const serviceId = getIdentityId(api.registerService as object, 'svc');
317
- const channelId = getIdentityId(api.registerChannel as object, 'chn');
318
- const methodId = getIdentityId(api.registerGatewayMethod as object, 'mth');
319
- return `${serviceId}:${channelId}:${methodId}`;
320
- };
321
-
322
- const getRegisterMeta = (api: OpenClawPluginApi): RegisterMeta => {
323
- const host = api as OpenClawPluginApiWithMeta;
324
- if (!host[BNCR_REGISTER_META]) {
325
- host[BNCR_REGISTER_META] = { methods: new Set<string>() };
326
- }
327
- if (!host[BNCR_REGISTER_META]!.methods) {
328
- host[BNCR_REGISTER_META]!.methods = new Set<string>();
329
- }
330
- if (!host[BNCR_REGISTER_META]!.apiInstanceId) {
331
- host[BNCR_REGISTER_META]!.apiInstanceId = getIdentityId(api as object, 'api');
332
- }
333
- if (!host[BNCR_REGISTER_META]!.registryFingerprint) {
334
- host[BNCR_REGISTER_META]!.registryFingerprint = getRegistryFingerprint(api);
335
- }
336
- return host[BNCR_REGISTER_META]!;
337
- };
338
-
339
- const getProcessStore = () => {
340
- const p = process as NodeJS.Process & {
341
- [BNCR_GLOBAL_REGISTER_TRACE]?: GlobalRegisterTrace;
342
- [BNCR_GATEWAY_RUNTIME]?: BncrGatewayRuntime;
343
- };
344
- return p;
345
- };
346
-
347
- const getGlobalRegisterTrace = () => {
348
- const p = getProcessStore();
349
- if (!p[BNCR_GLOBAL_REGISTER_TRACE]) {
350
- p[BNCR_GLOBAL_REGISTER_TRACE] = {
351
- seenRegistryFingerprints: new Set<string>(),
352
- seenApiInstanceIds: new Set<string>(),
353
- };
354
- }
355
- return p[BNCR_GLOBAL_REGISTER_TRACE]!;
356
- };
357
-
358
- const getGatewayRuntime = (): BncrGatewayRuntime => {
359
- const p = getProcessStore();
360
- if (!p[BNCR_GATEWAY_RUNTIME]) {
361
- p[BNCR_GATEWAY_RUNTIME] = {
362
- registeredMethodsByRegistry: new Map<string, Set<GatewayMethodName>>(),
363
- serviceRegistered: false,
364
- channelRegistered: false,
365
- };
366
- }
367
- return p[BNCR_GATEWAY_RUNTIME]!;
368
- };
369
-
370
- const getProcessOwnerApiInstanceId = (gatewayRuntime: BncrGatewayRuntime) =>
371
- gatewayRuntime.serviceOwnerApiInstanceId || gatewayRuntime.channelOwnerApiInstanceId || undefined;
372
-
373
- const shouldAdoptProcessOwner = (apiInstanceId: string, gatewayRuntime: BncrGatewayRuntime) => {
374
- const existingOwnerApiInstanceId = getProcessOwnerApiInstanceId(gatewayRuntime);
375
- const hasSingletonOwner =
376
- Boolean(gatewayRuntime.serviceRegistered) || Boolean(gatewayRuntime.channelRegistered);
377
-
378
- if (!hasSingletonOwner) {
379
- return {
380
- adoptOwner: true,
381
- existingOwnerApiInstanceId,
382
- reason: 'no-singleton-owner',
383
- };
384
- }
385
-
386
- if (existingOwnerApiInstanceId && existingOwnerApiInstanceId === apiInstanceId) {
387
- return {
388
- adoptOwner: true,
389
- existingOwnerApiInstanceId,
390
- reason: 'same-owner-api',
30
+ const registerRuntime = createBncrRegisterRuntime();
31
+
32
+ type BridgeSingletonWithOwner = NonNullable<
33
+ ReturnType<typeof registerRuntime.getExistingBridgeSingleton>
34
+ >;
35
+ type BridgeOwner = ReturnType<typeof registerRuntime.getBridgeOwnerFromBridge>;
36
+ const {
37
+ ensureGatewayMethodRegistered,
38
+ getBridgeSingleton,
39
+ getBridgeOwnerFromBridge,
40
+ getCurrentBridge,
41
+ getExistingBridgeSingleton,
42
+ getGatewayRuntime,
43
+ getGlobalRegisterTrace,
44
+ getRegisterMeta,
45
+ shouldAdoptProcessOwner,
46
+ } = registerRuntime;
47
+
48
+ type BncrDebugConfigRoot = {
49
+ channels?: {
50
+ bncr?: {
51
+ enabled?: boolean;
52
+ allowTool?: boolean;
53
+ debug?: {
54
+ verbose?: unknown;
55
+ };
391
56
  };
392
- }
393
-
394
- return {
395
- adoptOwner: false,
396
- existingOwnerApiInstanceId,
397
- reason: 'singleton-owned-by-other-api',
398
- };
399
- };
400
-
401
- const gatewayMethodDispatchers: Record<
402
- GatewayMethodName,
403
- (bridge: BridgeSingletonWithOwner, opts: any) => any
404
- > = {
405
- 'bncr.connect': (bridge, opts) => bridge.handleConnect(opts),
406
- 'bncr.inbound': (bridge, opts) => bridge.handleInbound(opts),
407
- 'bncr.activity': (bridge, opts) => bridge.handleActivity(opts),
408
- 'bncr.ack': (bridge, opts) => bridge.handleAck(opts),
409
- 'bncr.diagnostics': (bridge, opts) => bridge.handleDiagnostics(opts),
410
- 'bncr.deadLetter.inspect': (bridge, opts) => bridge.handleDeadLetterInspect(opts),
411
- 'bncr.deadLetter.prune': (bridge, opts) => bridge.handleDeadLetterPrune(opts),
412
- 'bncr.file.init': (bridge, opts) => bridge.handleFileInit(opts),
413
- 'bncr.file.chunk': (bridge, opts) => bridge.handleFileChunk(opts),
414
- 'bncr.file.complete': (bridge, opts) => bridge.handleFileComplete(opts),
415
- 'bncr.file.abort': (bridge, opts) => bridge.handleFileAbort(opts),
416
- 'bncr.file.ack': (bridge, opts) => bridge.handleFileAck(opts),
417
- };
418
-
419
- const dispatchGatewayMethod = (name: GatewayMethodName, opts: any) => {
420
- const gatewayRuntime = getGatewayRuntime();
421
- const bridge = gatewayRuntime.currentBridge;
422
- if (!bridge) {
423
- throw new Error(`bncr gateway runtime unavailable for ${name}`);
424
- }
425
- try {
426
- return gatewayMethodDispatchers[name](bridge, opts);
427
- } catch (error) {
428
- const detail =
429
- error instanceof Error
430
- ? {
431
- name: error.name,
432
- message: error.message,
433
- stack: error.stack || null,
434
- }
435
- : { name: 'NonError', message: String(error), stack: null };
436
- emitBncrLogLine(
437
- 'error',
438
- `[bncr] gateway method error ${JSON.stringify({
439
- method: name,
440
- bridgeId: bridge.getBridgeId?.() || null,
441
- gatewayPid: bridge.gatewayPid || null,
442
- detail,
443
- })}`,
444
- { debugOnly: true },
445
- () => true,
446
- );
447
- throw error;
448
- }
449
- };
450
-
451
- const mirrorGatewayMethodForMockApi = (api: OpenClawPluginApi, name: GatewayMethodName) => {
452
- const host = api as OpenClawPluginApi & {
453
- methods?: Array<{ name: string; handler: (opts: any) => any }>;
454
- };
455
- if (!Array.isArray(host.methods)) return;
456
- if (host.methods.some((item) => item?.name === name)) return;
457
- host.methods.push({ name, handler: (opts) => dispatchGatewayMethod(name, opts) });
458
- };
459
-
460
- const ensureGatewayMethodRegistered = (
461
- api: OpenClawPluginApi,
462
- name: GatewayMethodName,
463
- debugLog: (...args: any[]) => void,
464
- ) => {
465
- const meta = getRegisterMeta(api);
466
- const gatewayRuntime = getGatewayRuntime();
467
- const registryFingerprint = meta.registryFingerprint || getRegistryFingerprint(api);
468
- let registryMethods = gatewayRuntime.registeredMethodsByRegistry.get(registryFingerprint);
469
- if (!registryMethods) {
470
- registryMethods = new Set<GatewayMethodName>();
471
- gatewayRuntime.registeredMethodsByRegistry.set(registryFingerprint, registryMethods);
472
- }
473
- if (meta.methods?.has(name)) {
474
- debugLog(`register method skip ${name} (already registered on this api)`);
475
- return;
476
- }
477
- if (registryMethods.has(name)) {
478
- mirrorGatewayMethodForMockApi(api, name);
479
- meta.methods?.add(name);
480
- debugLog(`register method reuse ${name} (already registered in registry)`);
481
- return;
482
- }
483
- api.registerGatewayMethod(name, (opts) => dispatchGatewayMethod(name, opts));
484
- mirrorGatewayMethodForMockApi(api, name);
485
- registryMethods.add(name);
486
- meta.methods?.add(name);
487
- debugLog(`register method ok ${name}`);
488
- };
489
-
490
- const getBridgeOwner = (api: OpenClawPluginApi, loaded: LoadedRuntime): BridgeOwner => {
491
- const meta = getRegisterMeta(api);
492
- return {
493
- moduleEpoch: MODULE_EPOCH,
494
- bridgeFactoryId: getIdentityId(loaded.createBncrBridge as object, 'bridgeFactory'),
495
- apiInstanceId: meta.apiInstanceId || 'unknown',
496
- registryFingerprint: meta.registryFingerprint || 'unknown',
497
- registrationMode: meta.registrationMode,
498
57
  };
499
58
  };
500
59
 
501
- const sameBridgeOwner = (left?: BridgeOwner, right?: BridgeOwner) => {
502
- if (!left || !right) return false;
503
- return (
504
- left.moduleEpoch === right.moduleEpoch &&
505
- left.bridgeFactoryId === right.bridgeFactoryId &&
506
- left.apiInstanceId === right.apiInstanceId &&
507
- left.registryFingerprint === right.registryFingerprint
508
- );
509
- };
510
-
511
- const snapshotBridgeRegisterState = (
512
- bridge?: BridgeSingletonWithOwner,
513
- ): BridgeRegisterStateSnapshot | null => {
514
- if (!bridge) return null;
515
- return {
516
- registerCount: Number(bridge.registerCount || 0),
517
- apiGeneration: Number(bridge.apiGeneration || 0),
518
- firstRegisterAt:
519
- typeof bridge.firstRegisterAt === 'number'
520
- ? bridge.firstRegisterAt
521
- : (bridge.firstRegisterAt ?? null),
522
- lastRegisterAt:
523
- typeof bridge.lastRegisterAt === 'number'
524
- ? bridge.lastRegisterAt
525
- : (bridge.lastRegisterAt ?? null),
526
- lastApiRebindAt:
527
- typeof bridge.lastApiRebindAt === 'number'
528
- ? bridge.lastApiRebindAt
529
- : (bridge.lastApiRebindAt ?? null),
530
- pluginSource: typeof bridge.pluginSource === 'string' ? bridge.pluginSource : null,
531
- pluginVersion: typeof bridge.pluginVersion === 'string' ? bridge.pluginVersion : null,
532
- lastApiInstanceId:
533
- typeof bridge.lastApiInstanceId === 'string' ? bridge.lastApiInstanceId : null,
534
- lastRegistryFingerprint:
535
- typeof bridge.lastRegistryFingerprint === 'string' ? bridge.lastRegistryFingerprint : null,
536
- lastDriftSnapshot: bridge.lastDriftSnapshot ?? null,
537
- registerTraceRecent: Array.isArray(bridge.registerTraceRecent)
538
- ? bridge.registerTraceRecent.map((trace) => ({ ...trace }))
539
- : [],
540
- };
541
- };
542
-
543
- const hydrateBridgeRegisterState = (
544
- bridge: BridgeSingletonWithOwner,
545
- snapshot: BridgeRegisterStateSnapshot | null,
546
- ) => {
547
- if (!snapshot) return bridge;
548
- bridge.registerCount = snapshot.registerCount;
549
- bridge.apiGeneration = snapshot.apiGeneration;
550
- bridge.firstRegisterAt = snapshot.firstRegisterAt;
551
- bridge.lastRegisterAt = snapshot.lastRegisterAt;
552
- bridge.lastApiRebindAt = snapshot.lastApiRebindAt;
553
- bridge.pluginSource = snapshot.pluginSource;
554
- bridge.pluginVersion = snapshot.pluginVersion;
555
- bridge.lastApiInstanceId = snapshot.lastApiInstanceId;
556
- bridge.lastRegistryFingerprint = snapshot.lastRegistryFingerprint;
557
- bridge.lastDriftSnapshot = snapshot.lastDriftSnapshot;
558
- bridge.registerTraceRecent = snapshot.registerTraceRecent.map((trace) => ({ ...trace }));
559
- return bridge;
560
- };
561
-
562
- const assignBridgeOwner = (bridge: BridgeSingleton, owner: BridgeOwner) => {
563
- (bridge as BridgeSingletonWithOwner)[BNCR_BRIDGE_OWNER] = owner;
564
- return bridge as BridgeSingletonWithOwner;
565
- };
566
-
567
- const getBridgeSingleton = (api: OpenClawPluginApi) => {
568
- const loaded = loadRuntimeSync();
569
- const g = globalThis as typeof globalThis & { __bncrBridge?: BridgeSingletonWithOwner };
570
- const owner = getBridgeOwner(api, loaded);
571
- const previousOwner = g.__bncrBridge?.[BNCR_BRIDGE_OWNER];
572
-
573
- let created = false;
574
- let rebuilt = false;
575
-
576
- if (g.__bncrBridge) {
577
- const mustRebuild =
578
- !sameBridgeOwner(previousOwner, owner) &&
579
- (previousOwner?.moduleEpoch !== owner.moduleEpoch ||
580
- previousOwner?.bridgeFactoryId !== owner.bridgeFactoryId ||
581
- previousOwner?.registrationMode !== owner.registrationMode ||
582
- previousOwner?.apiInstanceId !== owner.apiInstanceId ||
583
- previousOwner?.registryFingerprint !== owner.registryFingerprint);
584
-
585
- if (mustRebuild) {
586
- const registerState = snapshotBridgeRegisterState(g.__bncrBridge);
587
- try {
588
- g.__bncrBridge.stopService?.();
589
- } catch {
590
- // ignore stop errors during hot-restart recovery
591
- }
592
- g.__bncrBridge = hydrateBridgeRegisterState(
593
- assignBridgeOwner(loaded.createBncrBridge(api), owner),
594
- registerState,
595
- );
596
- created = true;
597
- rebuilt = true;
598
- } else {
599
- g.__bncrBridge.bindApi?.(api);
600
- assignBridgeOwner(g.__bncrBridge, owner);
601
- created = false;
602
- rebuilt = false;
603
- }
604
- } else {
605
- g.__bncrBridge = assignBridgeOwner(loaded.createBncrBridge(api), owner);
606
- created = true;
607
- }
608
-
609
- return { bridge: g.__bncrBridge, runtime: loaded, created, rebuilt, owner, previousOwner };
610
- };
611
-
612
- const getExistingBridgeSingleton = (): BridgeSingletonWithOwner | undefined => {
613
- const g = globalThis as typeof globalThis & { __bncrBridge?: BridgeSingletonWithOwner };
614
- return g.__bncrBridge;
615
- };
616
-
617
- const isPlainObject = (value: unknown): value is Record<string, unknown> =>
618
- typeof value === 'object' && value !== null && !Array.isArray(value);
619
-
620
- const getCurrentBridge = (): BridgeSingletonWithOwner => {
621
- const bridge = getGatewayRuntime().currentBridge;
622
- if (!bridge) throw new Error('bncr current bridge unavailable');
623
- return bridge;
624
- };
625
-
626
- const createDynamicChannelPlugin = (loaded: LoadedRuntime): ChannelPlugin => {
627
- const base = loaded.createBncrChannelPlugin(() => getCurrentBridge());
628
-
629
- return {
630
- ...base,
631
- outbound: {
632
- ...base.outbound,
633
- sendText: (ctx: any) => getCurrentBridge().channelSendText(ctx),
634
- sendMedia: (ctx: any) => getCurrentBridge().channelSendMedia(ctx),
635
- },
636
- status: {
637
- ...base.status,
638
- buildChannelSummary: async ({ defaultAccountId }: any) =>
639
- getCurrentBridge().getChannelSummary(defaultAccountId || 'Primary'),
640
- buildAccountSnapshot: async ({ account, runtime }: any) => {
641
- const bridgeNow = getCurrentBridge();
642
- return base.status.buildAccountSnapshot({
643
- account,
644
- runtime: runtime || bridgeNow.getAccountRuntimeSnapshot(account?.accountId),
645
- });
646
- },
647
- resolveAccountState: ({ enabled, configured, account, cfg, runtime }: any) => {
648
- const bridgeNow = getCurrentBridge();
649
- return base.status.resolveAccountState({
650
- enabled,
651
- configured,
652
- account,
653
- cfg,
654
- runtime: runtime || bridgeNow.getAccountRuntimeSnapshot(account?.accountId),
655
- });
656
- },
657
- },
658
- gateway: {
659
- ...base.gateway,
660
- startAccount: (ctx: any) => getCurrentBridge().channelStartAccount(ctx),
661
- stopAccount: (ctx: any) => getCurrentBridge().channelStopAccount(ctx),
662
- },
663
- };
664
- };
665
-
666
- const registerBncrCli = (api: OpenClawPluginApi & { registerCli?: (...args: any[]) => void }) => {
667
- if (typeof api.registerCli !== 'function') return;
668
- api.registerCli(
669
- ({ program }: any) => {
670
- const bncr = program.command('bncr').description('Bncr channel utilities');
671
- bncr
672
- .command('miniconfig')
673
- .description(
674
- 'Seed minimal channels.bncr config (adds enabled=true and allowTool=false only when missing)',
675
- )
676
- .action(async () => {
677
- const cfg = getOpenClawRuntimeConfig(api) as Record<string, unknown>;
678
- const channels = isPlainObject(cfg.channels) ? cfg.channels : {};
679
- const existing = isPlainObject(channels.bncr) ? channels.bncr : {};
680
- const added: string[] = [];
681
-
682
- if (existing.enabled === undefined) {
683
- added.push('enabled=true');
684
- }
685
-
686
- if (existing.allowTool === undefined) {
687
- added.push('allowTool=false');
688
- }
689
-
690
- if (added.length === 0) {
691
- console.log('Minimal bncr config already present. No changes made.');
692
- return;
693
- }
694
-
695
- await mutateOpenClawRuntimeConfigFile(api, {
696
- afterWrite: { mode: 'auto' },
697
- mutate(draft: Record<string, unknown>) {
698
- if (!isPlainObject(draft.channels)) draft.channels = {};
699
- const draftChannels = draft.channels as Record<string, unknown>;
700
- const draftExisting = isPlainObject(draftChannels.bncr) ? draftChannels.bncr : {};
701
- const draftBncrCfg: Record<string, unknown> = { ...draftExisting };
702
-
703
- if (draftBncrCfg.enabled === undefined) {
704
- draftBncrCfg.enabled = true;
705
- }
706
-
707
- if (draftBncrCfg.allowTool === undefined) {
708
- draftBncrCfg.allowTool = false;
709
- }
710
-
711
- draftChannels.bncr = draftBncrCfg;
712
- },
713
- });
714
- console.log('Seeded minimal bncr config at channels.bncr.');
715
- console.log(`Added missing fields: ${added.join(', ')}`);
716
- console.log('Gateway will apply the config using the host afterWrite policy.');
717
- });
718
- },
719
- { commands: ['bncr'] },
720
- );
721
- };
722
-
723
- const shouldSkipNonRuntimeRegister = (mode?: string) =>
724
- mode === 'cli-metadata' || mode === 'discovery';
725
-
726
60
  const plugin = {
727
61
  id: 'bncr',
728
62
  name: 'Bncr',
729
63
  description: 'Bncr channel plugin',
730
64
  configSchema: BncrConfigSchema,
731
- register(
732
- api: OpenClawPluginApi & { registerCli?: (...args: any[]) => void; registrationMode?: string },
733
- ) {
65
+ register(api: BncrRegistrationApi) {
734
66
  registerBncrCli(api);
735
67
  if (shouldSkipNonRuntimeRegister(api.registrationMode)) return;
736
68
 
@@ -770,9 +102,9 @@ const plugin = {
770
102
  previousOwner = adopted.previousOwner;
771
103
  gatewayRuntime.currentBridge = bridge;
772
104
  } else {
773
- runtime = loadRuntimeSync();
105
+ runtime = loadBncrRuntimeSync();
774
106
  bridge = gatewayRuntime.currentBridge || getExistingBridgeSingleton();
775
- previousOwner = getExistingBridgeSingleton()?.[BNCR_BRIDGE_OWNER];
107
+ previousOwner = getBridgeOwnerFromBridge(bridge);
776
108
  owner = previousOwner;
777
109
  if (bridge && !gatewayRuntime.currentBridge) {
778
110
  gatewayRuntime.currentBridge = bridge;
@@ -784,13 +116,13 @@ const plugin = {
784
116
  globalTrace.lastApiInstanceId = apiInstanceId;
785
117
  globalTrace.lastRegistryFingerprint = registryFingerprint;
786
118
  bridge?.noteRegister?.({
787
- source: '~/.openclaw/workspace/plugins/bncr/index.ts',
119
+ source: '@xmoxmo/bncr',
788
120
  pluginVersion,
789
121
  apiRebound: ownerDecision.adoptOwner ? !created && !rebuilt : false,
790
122
  apiInstanceId: meta.apiInstanceId,
791
123
  registryFingerprint: meta.registryFingerprint,
792
124
  });
793
- const debugLog = (...args: any[]) => {
125
+ const debugLog = (...args: unknown[]) => {
794
126
  const rendered = args
795
127
  .map((arg) => (typeof arg === 'string' ? arg : JSON.stringify(arg)))
796
128
  .join(' ')
@@ -826,8 +158,8 @@ const plugin = {
826
158
 
827
159
  const resolveDebug = async () => {
828
160
  try {
829
- const cfg = getOpenClawRuntimeConfig(api);
830
- return Boolean((cfg as any)?.channels?.bncr?.debug?.verbose);
161
+ const cfg = getOpenClawRuntimeConfig(api) as BncrDebugConfigRoot | null | undefined;
162
+ return Boolean(cfg?.channels?.bncr?.debug?.verbose);
831
163
  } catch {
832
164
  return false;
833
165
  }
@@ -857,7 +189,9 @@ const plugin = {
857
189
  }
858
190
 
859
191
  if (!gatewayRuntime.channelRegistered) {
860
- api.registerChannel({ plugin: createDynamicChannelPlugin(runtime) });
192
+ api.registerChannel({
193
+ plugin: createDynamicChannelPlugin({ loaded: runtime, getCurrentBridge }) as ChannelPlugin,
194
+ });
861
195
  gatewayRuntime.channelRegistered = true;
862
196
  gatewayRuntime.channelOwnerApiInstanceId = apiInstanceId;
863
197
  meta.channel = true;