@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
@@ -0,0 +1,527 @@
1
+ import type { GatewayRequestHandlerOptions } from 'openclaw/plugin-sdk/core';
2
+ import { normalizeAccountId } from '../core/accounts.ts';
3
+ import {
4
+ applyOutboundCapability,
5
+ buildCapabilitySnapshot,
6
+ clearOutboundCapability,
7
+ findCapabilityConnection,
8
+ } from '../core/connection-capability.ts';
9
+ import {
10
+ getRevalidatedAttemptReason,
11
+ hasAlternativeLiveConnection as hasAlternativeLiveConnectionFromRuntime,
12
+ hasRecentInboundReachability as hasRecentInboundReachabilityFromRuntime,
13
+ isRecentlyReachableConn as isRecentlyReachableConnFromRuntime,
14
+ resolveRecentInboundConnIds as resolveRecentInboundConnIdsFromRuntime,
15
+ } from '../core/connection-reachability.ts';
16
+ import type { BncrConnection, OutboxEntry } from '../core/types.ts';
17
+ import {
18
+ buildConnectionCapabilityDebugPayload,
19
+ buildConnectionCapabilityDebugSig,
20
+ buildConnectionDegradePayload,
21
+ buildConnectionDegradeSkipPayload,
22
+ buildConnectionPromotePayload,
23
+ buildSeenConnection,
24
+ resolveSeenConnectionPromoteReason,
25
+ } from './connection-state-helpers.ts';
26
+
27
+ export type BncrActiveConnectionDebugEntry = {
28
+ accountId: string;
29
+ connId: string;
30
+ clientId?: string;
31
+ connectedAt: number;
32
+ lastSeenAt: number;
33
+ outboundReadyUntil?: number | null;
34
+ preferredForOutboundUntil?: number | null;
35
+ inboundOnly?: boolean;
36
+ };
37
+
38
+ type BncrCapabilityConnection = BncrConnection & {
39
+ outboundReadyUntil?: number;
40
+ preferredForOutboundUntil?: number;
41
+ inboundOnly?: boolean;
42
+ };
43
+
44
+ // This module owns the live connection model used by outbound routing:
45
+ // seen/active state, outbound capability, recent inbound reachability, and
46
+ // degradation when ACK / push signals prove a connection is no longer usable.
47
+
48
+ function logSeenConnectionPromotion(args: {
49
+ runtime: Pick<
50
+ BncrConnectionStateRuntime,
51
+ 'bridgeId' | 'buildActiveConnectionDebugList' | 'logInfo'
52
+ >;
53
+ accountId: string;
54
+ reason: string;
55
+ previousActiveKey: string | null;
56
+ previousActiveConn: BncrConnection | null;
57
+ nextActiveKey: string;
58
+ nextActiveConn: BncrConnection;
59
+ }) {
60
+ args.runtime.logInfo(
61
+ 'connection',
62
+ `seen:promote ${JSON.stringify(
63
+ buildConnectionPromotePayload({
64
+ bridgeId: args.runtime.bridgeId,
65
+ accountId: args.accountId,
66
+ reason: args.reason,
67
+ previousActiveKey: args.previousActiveKey,
68
+ previousActiveConn: args.previousActiveConn,
69
+ nextActiveKey: args.nextActiveKey,
70
+ nextActiveConn: args.nextActiveConn,
71
+ activeConnections: args.runtime.buildActiveConnectionDebugList(args.accountId, {
72
+ includeOutboundState: true,
73
+ }),
74
+ }),
75
+ )}`,
76
+ { debugOnly: true },
77
+ );
78
+ }
79
+
80
+ export type TransferOwnerState = {
81
+ ownerConnId?: string;
82
+ ownerClientId?: string;
83
+ };
84
+
85
+ export type BncrConnectionStateRuntime = {
86
+ bridgeId: string;
87
+ now: () => number;
88
+ asString: (value: unknown, fallback?: string) => string;
89
+ connectTtlMs: number;
90
+ recentInboundSendWindowMs: number;
91
+ outboundReadyTtlMs: number;
92
+ preferredOutboundTtlMs: number;
93
+ connections: Map<string, BncrConnection>;
94
+ activeConnectionByAccount: Map<string, string>;
95
+ lastInboundByAccount: Map<string, number>;
96
+ lastActivityByAccount: Map<string, number>;
97
+ gcTransientState: () => void;
98
+ connectionKey: (accountId: string, clientId?: string) => string;
99
+ buildActiveConnectionDebugList: (
100
+ accountId: string,
101
+ options?: { includeOutboundState?: boolean },
102
+ ) => BncrActiveConnectionDebugEntry[];
103
+ rememberGatewayContext: (context: GatewayRequestHandlerOptions['context']) => void;
104
+ markActivity: (accountId: string, at?: number) => void;
105
+ logInfo: (scope: string, message: string, options?: { debugOnly?: boolean }) => void;
106
+ logInfoDedupJson: (
107
+ scope: string,
108
+ label: string,
109
+ payload: unknown,
110
+ options?: { key?: string; sig?: string; debugOnly?: boolean },
111
+ ) => void;
112
+ };
113
+
114
+ export function createBncrConnectionState(runtime: BncrConnectionStateRuntime) {
115
+ // 1) Reachability/read helpers
116
+ // 2) Live-state refresh entrypoints
117
+ // 3) Canonical live-connection mutations
118
+ // 4) Outbound capability degradation / online probes
119
+ //
120
+ // Keep this order stable. The later mutation paths depend on the earlier
121
+ // read-model helpers, and that dependency direction makes the file easier to
122
+ // scan than grouping everything by event type.
123
+
124
+ // Reachability helpers are grouped first because they are consumed by both
125
+ // ownership adoption and outbound route-selection decisions later in the file.
126
+
127
+ const hasRecentInboundReachability = (accountId: string): boolean => {
128
+ const acc = normalizeAccountId(accountId);
129
+ return hasRecentInboundReachabilityFromRuntime({
130
+ now: runtime.now(),
131
+ windowMs: runtime.recentInboundSendWindowMs,
132
+ lastInboundAt: runtime.lastInboundByAccount.get(acc) || 0,
133
+ lastActivityAt: runtime.lastActivityByAccount.get(acc) || 0,
134
+ });
135
+ };
136
+
137
+ const resolveRecentInboundConnIds = (accountId: string): Set<string> => {
138
+ const acc = normalizeAccountId(accountId);
139
+ return resolveRecentInboundConnIdsFromRuntime({
140
+ accountId: acc,
141
+ now: runtime.now(),
142
+ connectTtlMs: runtime.connectTtlMs,
143
+ recentInboundReachable: hasRecentInboundReachability(acc),
144
+ connections: runtime.connections.values(),
145
+ });
146
+ };
147
+
148
+ const isRecentlyReachableConn = (
149
+ accountId: string,
150
+ connId?: string,
151
+ clientId?: string,
152
+ ): boolean => {
153
+ const acc = normalizeAccountId(accountId);
154
+ const activeKey = runtime.activeConnectionByAccount.get(acc);
155
+ const active = activeKey ? runtime.connections.get(activeKey) || null : null;
156
+ return isRecentlyReachableConnFromRuntime({
157
+ accountId: acc,
158
+ connId,
159
+ clientId,
160
+ recentConnIds: resolveRecentInboundConnIds(acc),
161
+ activeConnection: active,
162
+ });
163
+ };
164
+
165
+ const isRevalidatedAttemptedConn = (entry: OutboxEntry, connId: string): boolean => {
166
+ const acc = normalizeAccountId(entry.accountId);
167
+ const revalidated = getRevalidatedAttemptReason({
168
+ entry,
169
+ connId,
170
+ accountId: acc,
171
+ now: runtime.now(),
172
+ connectTtlMs: runtime.connectTtlMs,
173
+ recentInboundReachable: hasRecentInboundReachability(acc),
174
+ connections: runtime.connections.values(),
175
+ });
176
+ if (!revalidated) return false;
177
+
178
+ runtime.logInfo(
179
+ 'outbox',
180
+ `revalidated-retry ${JSON.stringify({
181
+ messageId: entry.messageId,
182
+ accountId: acc,
183
+ connId: String(connId || '').trim(),
184
+ ...revalidated,
185
+ })}`,
186
+ { debugOnly: true },
187
+ );
188
+ return true;
189
+ };
190
+
191
+ const tryAdoptTransferOwner = (args: {
192
+ accountId: string;
193
+ transfer: TransferOwnerState | undefined;
194
+ connId: string;
195
+ clientId?: string;
196
+ }): boolean => {
197
+ const { accountId, transfer, connId, clientId } = args;
198
+ if (!transfer) return false;
199
+ if (!hasRecentInboundReachability(accountId)) return false;
200
+ if (!isRecentlyReachableConn(accountId, connId, clientId)) return false;
201
+
202
+ transfer.ownerConnId = connId;
203
+ transfer.ownerClientId = runtime.asString(clientId || '').trim() || undefined;
204
+ return true;
205
+ };
206
+
207
+ // Live-state refresh entrypoints -----------------------------------------
208
+ // These are the external-facing refresh paths used by connect/activity/file
209
+ // transfer acceptance before deeper capability or routing decisions.
210
+
211
+ // Live-state refresh is the steady-state entrypoint used by connection and
212
+ // activity events before any explicit routing/capability mutations happen.
213
+
214
+ const refreshAcceptedFileTransferLiveState = (args: {
215
+ accountId: string;
216
+ connId: string;
217
+ clientId?: string;
218
+ context: GatewayRequestHandlerOptions['context'];
219
+ }) => {
220
+ const { accountId, connId, clientId, context } = args;
221
+ runtime.rememberGatewayContext(context);
222
+ markSeen(accountId, connId, clientId);
223
+ runtime.markActivity(accountId);
224
+ };
225
+
226
+ const refreshLiveConnectionState = (args: {
227
+ accountId: string;
228
+ connId: string;
229
+ clientId?: string;
230
+ outboundReady: boolean;
231
+ preferredForOutbound: boolean;
232
+ inboundOnly: boolean;
233
+ context: GatewayRequestHandlerOptions['context'];
234
+ }) => {
235
+ const {
236
+ accountId,
237
+ connId,
238
+ clientId,
239
+ outboundReady,
240
+ preferredForOutbound,
241
+ inboundOnly,
242
+ context,
243
+ } = args;
244
+ refreshAcceptedFileTransferLiveState({
245
+ accountId,
246
+ connId,
247
+ clientId,
248
+ context,
249
+ });
250
+ markOutboundCapability({
251
+ accountId,
252
+ connId,
253
+ clientId,
254
+ outboundReady,
255
+ preferredForOutbound,
256
+ inboundOnly,
257
+ });
258
+ };
259
+
260
+ // The remaining helpers mutate the canonical live-connection model used by
261
+ // route selection, ownership recovery, and capability degradation.
262
+
263
+ // Canonical live-connection mutations ------------------------------------
264
+
265
+ const markSeen = (accountId: string, connId: string, clientId?: string) => {
266
+ runtime.gcTransientState();
267
+
268
+ const acc = normalizeAccountId(accountId);
269
+ const key = runtime.connectionKey(acc, clientId);
270
+ const t = runtime.now();
271
+ const prev = runtime.connections.get(key) as BncrCapabilityConnection | undefined;
272
+ const previousActiveKey = runtime.activeConnectionByAccount.get(acc) || null;
273
+ const previousActiveConn = previousActiveKey
274
+ ? runtime.connections.get(previousActiveKey) || null
275
+ : null;
276
+
277
+ const nextConn = buildSeenConnection({
278
+ accountId: acc,
279
+ connId,
280
+ clientId: runtime.asString(clientId || '').trim() || undefined,
281
+ nowMs: t,
282
+ previous: prev || null,
283
+ });
284
+
285
+ runtime.connections.set(key, nextConn);
286
+ const connectionSeenPayload = {
287
+ bridge: runtime.bridgeId,
288
+ accountId: acc,
289
+ connId,
290
+ clientId: nextConn.clientId,
291
+ connectedAt: nextConn.connectedAt,
292
+ lastSeenAt: nextConn.lastSeenAt,
293
+ outboundReadyUntil: nextConn.outboundReadyUntil || null,
294
+ preferredForOutboundUntil: nextConn.preferredForOutboundUntil || null,
295
+ inboundOnly: nextConn.inboundOnly === true,
296
+ };
297
+ const connectionSeenSig = JSON.stringify({
298
+ bridge: runtime.bridgeId,
299
+ accountId: acc,
300
+ connId,
301
+ clientId: nextConn.clientId || null,
302
+ inboundOnly: nextConn.inboundOnly === true,
303
+ outboundReadyActive: Number(nextConn.outboundReadyUntil || 0) > t,
304
+ preferredForOutboundActive: Number(nextConn.preferredForOutboundUntil || 0) > t,
305
+ });
306
+ runtime.logInfoDedupJson('connection', 'seen', connectionSeenPayload, {
307
+ key: `connection-seen:${acc}:${nextConn.clientId || connId}`,
308
+ sig: connectionSeenSig,
309
+ debugOnly: true,
310
+ });
311
+
312
+ const current = runtime.activeConnectionByAccount.get(acc) || null;
313
+ const curConn = current ? runtime.connections.get(current) || null : null;
314
+ const promoteReason = resolveSeenConnectionPromoteReason({
315
+ currentActiveKey: current,
316
+ currentConnection: curConn,
317
+ nowMs: t,
318
+ connectTtlMs: runtime.connectTtlMs,
319
+ });
320
+
321
+ if (promoteReason === 'no-current-active') {
322
+ runtime.activeConnectionByAccount.set(acc, key);
323
+ logSeenConnectionPromotion({
324
+ runtime,
325
+ accountId: acc,
326
+ reason: promoteReason,
327
+ previousActiveKey,
328
+ previousActiveConn,
329
+ nextActiveKey: key,
330
+ nextActiveConn: nextConn,
331
+ });
332
+ return;
333
+ }
334
+
335
+ if (promoteReason) {
336
+ runtime.activeConnectionByAccount.set(acc, key);
337
+ logSeenConnectionPromotion({
338
+ runtime,
339
+ accountId: acc,
340
+ reason: promoteReason,
341
+ previousActiveKey,
342
+ previousActiveConn,
343
+ nextActiveKey: key,
344
+ nextActiveConn: nextConn,
345
+ });
346
+ }
347
+ };
348
+
349
+ // Capability updates keep the live routing model in sync with what the
350
+ // client claims it can currently do for outbound delivery.
351
+
352
+ // Outbound capability / degradation --------------------------------------
353
+
354
+ const markOutboundCapability = (args: {
355
+ accountId: string;
356
+ connId: string;
357
+ clientId?: string;
358
+ outboundReady?: boolean;
359
+ preferredForOutbound?: boolean;
360
+ inboundOnly?: boolean;
361
+ at?: number;
362
+ }) => {
363
+ const acc = normalizeAccountId(args.accountId);
364
+ const key = runtime.connectionKey(acc, args.clientId);
365
+ const t = Number(args.at || runtime.now());
366
+ const current = runtime.connections.get(key) as BncrCapabilityConnection | undefined;
367
+ if (!current || current.connId !== args.connId) return;
368
+
369
+ const next = applyOutboundCapability({
370
+ connection: current,
371
+ at: t,
372
+ outboundReadyTtlMs: runtime.outboundReadyTtlMs,
373
+ preferredOutboundTtlMs: runtime.preferredOutboundTtlMs,
374
+ outboundReady: args.outboundReady,
375
+ preferredForOutbound: args.preferredForOutbound,
376
+ inboundOnly: args.inboundOnly,
377
+ });
378
+
379
+ runtime.connections.set(key, next);
380
+ const { payload: connectionCapabilityPayload, snapshot } =
381
+ buildConnectionCapabilityDebugPayload({
382
+ bridgeId: runtime.bridgeId,
383
+ accountId: acc,
384
+ connection: next,
385
+ outboundReady: args.outboundReady === true,
386
+ preferredForOutbound: args.preferredForOutbound === true,
387
+ });
388
+ const connectionCapabilitySig = buildConnectionCapabilityDebugSig({
389
+ bridgeId: runtime.bridgeId,
390
+ accountId: acc,
391
+ connection: next,
392
+ outboundReady: args.outboundReady === true,
393
+ preferredForOutbound: args.preferredForOutbound === true,
394
+ snapshot,
395
+ nowMs: t,
396
+ });
397
+ runtime.logInfoDedupJson('connection', 'capability', connectionCapabilityPayload, {
398
+ key: `connection-capability:${acc}:${next.clientId || next.connId}`,
399
+ sig: connectionCapabilitySig,
400
+ debugOnly: true,
401
+ });
402
+ };
403
+
404
+ const hasAlternativeLiveConnection = (
405
+ accountId: string,
406
+ currentConnId?: string,
407
+ currentClientId?: string,
408
+ ): boolean => {
409
+ const acc = normalizeAccountId(accountId);
410
+ return hasAlternativeLiveConnectionFromRuntime({
411
+ accountId: acc,
412
+ now: runtime.now(),
413
+ connectTtlMs: runtime.connectTtlMs,
414
+ currentConnId,
415
+ currentClientId,
416
+ connections: runtime.connections.values(),
417
+ });
418
+ };
419
+
420
+ // Degradation is the corrective path after push/ACK evidence says the current
421
+ // outbound capability should no longer be trusted for routing decisions.
422
+
423
+ const degradeOutboundCapability = (args: {
424
+ accountId: string;
425
+ connId?: string;
426
+ clientId?: string;
427
+ reason: string;
428
+ at?: number;
429
+ }) => {
430
+ const acc = normalizeAccountId(args.accountId);
431
+ const t = Number(args.at || runtime.now());
432
+ const hasAlternative = hasAlternativeLiveConnection(acc, args.connId, args.clientId);
433
+ const currentKey = runtime.activeConnectionByAccount.get(acc) || null;
434
+ const matched = findCapabilityConnection({
435
+ accountId: acc,
436
+ connId: args.connId,
437
+ clientId: args.clientId,
438
+ connections: runtime.connections.entries(),
439
+ });
440
+
441
+ if (!matched) return;
442
+
443
+ const before = buildCapabilitySnapshot(matched.connection);
444
+
445
+ if (!hasAlternative) {
446
+ runtime.logInfo(
447
+ 'connection',
448
+ `outbound-degrade skip ${JSON.stringify(
449
+ buildConnectionDegradeSkipPayload({
450
+ bridgeId: runtime.bridgeId,
451
+ accountId: acc,
452
+ connection: matched.connection,
453
+ reason: args.reason,
454
+ at: t,
455
+ currentActiveKey: currentKey,
456
+ degradedKey: matched.key,
457
+ before,
458
+ }),
459
+ )}`,
460
+ { debugOnly: true },
461
+ );
462
+ return;
463
+ }
464
+
465
+ const next = clearOutboundCapability(matched.connection);
466
+ runtime.connections.set(matched.key, next);
467
+
468
+ runtime.logInfo(
469
+ 'connection',
470
+ `outbound-degrade ${JSON.stringify(
471
+ buildConnectionDegradePayload({
472
+ bridgeId: runtime.bridgeId,
473
+ accountId: acc,
474
+ connection: next,
475
+ reason: args.reason,
476
+ at: t,
477
+ currentActiveKey: currentKey,
478
+ degradedKey: matched.key,
479
+ before,
480
+ after: buildCapabilitySnapshot(next),
481
+ }),
482
+ )}`,
483
+ { debugOnly: true },
484
+ );
485
+ };
486
+
487
+ // Online/read-model helpers ----------------------------------------------
488
+ // These stay at the end because they are pure projections over the runtime
489
+ // maps after all mutation logic above has established the current state.
490
+
491
+ const isOnline = (accountId: string): boolean => {
492
+ const acc = normalizeAccountId(accountId);
493
+ const t = runtime.now();
494
+ for (const c of runtime.connections.values()) {
495
+ if (c.accountId !== acc) continue;
496
+ if (t - c.lastSeenAt <= runtime.connectTtlMs) return true;
497
+ }
498
+ return false;
499
+ };
500
+
501
+ const activeConnectionCount = (accountId: string): number => {
502
+ const acc = normalizeAccountId(accountId);
503
+ const t = runtime.now();
504
+ let n = 0;
505
+ for (const c of runtime.connections.values()) {
506
+ if (c.accountId !== acc) continue;
507
+ if (t - c.lastSeenAt <= runtime.connectTtlMs) n += 1;
508
+ }
509
+ return n;
510
+ };
511
+
512
+ return {
513
+ hasRecentInboundReachability,
514
+ resolveRecentInboundConnIds,
515
+ isRecentlyReachableConn,
516
+ isRevalidatedAttemptedConn,
517
+ tryAdoptTransferOwner,
518
+ refreshAcceptedFileTransferLiveState,
519
+ refreshLiveConnectionState,
520
+ markSeen,
521
+ markOutboundCapability,
522
+ hasAlternativeLiveConnection,
523
+ degradeOutboundCapability,
524
+ isOnline,
525
+ activeConnectionCount,
526
+ };
527
+ }