@xmoxmo/bncr 0.3.5 → 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 +57 -134
  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,440 @@
1
+ import type { GatewayRequestHandlerOptions } from 'openclaw/plugin-sdk/core';
2
+ import { CHANNEL_ID } from '../core/accounts.ts';
3
+ import type { FileSendTransferState } from '../core/types.ts';
4
+ import {
5
+ applyFileAckState,
6
+ buildConnectionHandlerActivityResponse,
7
+ buildConnectionHandlerConnectResponse,
8
+ buildFileAckPayload,
9
+ buildGatewayDebugFields,
10
+ buildHandledFileAckResponse,
11
+ buildTerminalFileAckResponse,
12
+ type ConnectionDiagnostics,
13
+ type ConnectionQueueCounters,
14
+ type ConnectionRuntimeFlags,
15
+ type FileAckPayload,
16
+ hasTerminalFileAckState,
17
+ isBncrFileAckStage,
18
+ type LeaseEventPayload,
19
+ matchesTransferOwner,
20
+ type PreparedAckHandling,
21
+ resolveFileAckLeaseEventKind,
22
+ } from './connection-handlers-helpers.ts';
23
+ import { buildBncrGatewayEventContext } from './gateway-event-context.ts';
24
+
25
+ export type {
26
+ ConnectionDiagnostics,
27
+ ConnectionQueueCounters,
28
+ ConnectionRuntimeFlags,
29
+ FileAckPayload,
30
+ LeaseEventPayload,
31
+ PreparedAckHandling,
32
+ } from './connection-handlers-helpers.ts';
33
+
34
+ type LeaseEventKind =
35
+ | 'connect'
36
+ | 'inbound'
37
+ | 'activity'
38
+ | 'ack'
39
+ | 'file.init'
40
+ | 'file.chunk'
41
+ | 'file.complete'
42
+ | 'file.abort';
43
+
44
+ // Runtime contract ----------------------------------------------------------
45
+
46
+ export type BncrConnectionHandlersRuntime = {
47
+ bridgeId: string;
48
+ gatewayPid: number;
49
+ pushEvent: string;
50
+ bridgeVersion: number;
51
+ asString: (value: unknown, fallback?: string) => string;
52
+ now: () => number;
53
+ finiteNonNegativeNumberOrNull: (value: unknown) => number | null;
54
+ syncDebugFlag: () => Promise<void>;
55
+ logInfo: (scope: string, message: string, options?: { debugOnly?: boolean }) => void;
56
+ logWarn: (scope: string, message: string, options?: { debugOnly?: boolean }) => void;
57
+ normalizeAccountId: (value: string) => string;
58
+ buildAccountQueueCounters: (accountId: string) => ConnectionQueueCounters;
59
+ buildExtendedDiagnostics: (accountId: string) => ConnectionDiagnostics;
60
+ buildRuntimeFlags: (accountId: string) => ConnectionRuntimeFlags;
61
+ isPrimaryConnection: (accountId: string, clientId?: string) => boolean;
62
+ activeConnectionCount?: (accountId: string) => number;
63
+ acceptConnection: () => { leaseId: string; connectionEpoch: number; acceptedAt: number };
64
+ refreshLiveConnectionState: (args: {
65
+ accountId: string;
66
+ connId: string;
67
+ clientId?: string;
68
+ outboundReady: boolean;
69
+ preferredForOutbound: boolean;
70
+ inboundOnly: boolean;
71
+ context: GatewayRequestHandlerOptions['context'];
72
+ }) => void;
73
+ flushOnConnect: (accountId: string) => void;
74
+ flushOnActivity: (accountId: string) => void;
75
+ shouldIgnoreStaleEvent: (args: {
76
+ kind: Exclude<LeaseEventKind, 'connect'>;
77
+ payload: LeaseEventPayload;
78
+ accountId: string;
79
+ connId: string;
80
+ clientId?: string;
81
+ }) => boolean;
82
+ incrementConnectEvents: (accountId: string) => void;
83
+ incrementActivityEvents: (accountId: string) => void;
84
+ incrementAckEvents: (accountId: string) => void;
85
+ markLastActivityAt: () => void;
86
+ markLastAckAt: () => void;
87
+ messageAckWaiterCount: () => number;
88
+ fileAckWaiterCount: () => number;
89
+ prepareAckHandling: (args: {
90
+ params: GatewayRequestHandlerOptions['params'];
91
+ respond: GatewayRequestHandlerOptions['respond'];
92
+ client: GatewayRequestHandlerOptions['client'];
93
+ context: GatewayRequestHandlerOptions['context'];
94
+ }) => PreparedAckHandling | null;
95
+ handleAckOutcome: (
96
+ args: {
97
+ params: GatewayRequestHandlerOptions['params'];
98
+ respond: GatewayRequestHandlerOptions['respond'];
99
+ } & PreparedAckHandling,
100
+ ) => void;
101
+ fileSendTransfers: Map<string, FileSendTransferState>;
102
+ hasFileAckWaiter: (key: string) => boolean;
103
+ fileAckKey: (transferId: string, stage: string, chunkIndex?: number) => string;
104
+ observeLease: (kind: LeaseEventKind, payload: LeaseEventPayload) => { stale: boolean };
105
+ tryAdoptTransferOwner: (args: {
106
+ accountId: string;
107
+ transfer: FileSendTransferState | undefined;
108
+ connId: string;
109
+ clientId?: string;
110
+ }) => boolean;
111
+ refreshAcceptedFileTransferLiveState: (args: {
112
+ accountId: string;
113
+ connId: string;
114
+ clientId?: string;
115
+ context: GatewayRequestHandlerOptions['context'];
116
+ }) => void;
117
+ resolveFileAck: (args: {
118
+ transferId: string;
119
+ stage: string;
120
+ chunkIndex?: number;
121
+ payload: FileAckPayload;
122
+ ok: boolean;
123
+ }) => void;
124
+ };
125
+
126
+ export function createBncrConnectionHandlers(runtime: BncrConnectionHandlersRuntime) {
127
+ // Handler order mirrors the host lifecycle:
128
+ // connect bootstrap -> steady-state activity -> message ack -> file ack.
129
+ // Keep that order stable so gateway event review follows the same path the
130
+ // host takes at runtime.
131
+
132
+ // Shared gateway event context -------------------------------------------
133
+
134
+ const buildGatewayContext = (args: {
135
+ params: GatewayRequestHandlerOptions['params'];
136
+ client: GatewayRequestHandlerOptions['client'];
137
+ context: GatewayRequestHandlerOptions['context'];
138
+ }) => {
139
+ return buildBncrGatewayEventContext({
140
+ params: args.params,
141
+ client: args.client,
142
+ context: args.context,
143
+ asString: runtime.asString,
144
+ normalizeAccountId: runtime.normalizeAccountId,
145
+ now: runtime.now,
146
+ });
147
+ };
148
+
149
+ return {
150
+ // Connect / activity handler surface -----------------------------------
151
+
152
+ // Connection bootstrap establishes live routing state and returns the
153
+ // current bridge/runtime snapshot that the client needs immediately.
154
+ handleConnect: async ({ params, respond, client, context }: GatewayRequestHandlerOptions) => {
155
+ await runtime.syncDebugFlag();
156
+ const gatewayContext = buildGatewayContext({ params, client, context });
157
+ const { accountId, connId, clientId, outboundReady, preferredForOutbound, inboundOnly } =
158
+ gatewayContext;
159
+
160
+ runtime.logInfo(
161
+ 'connection',
162
+ `connect ${JSON.stringify(
163
+ buildGatewayDebugFields({
164
+ bridgeId: runtime.bridgeId,
165
+ accountId,
166
+ connId,
167
+ clientId,
168
+ outboundReady,
169
+ preferredForOutbound,
170
+ inboundOnly,
171
+ hasContext: Boolean(context),
172
+ }),
173
+ )}`,
174
+ { debugOnly: true },
175
+ );
176
+
177
+ runtime.refreshLiveConnectionState({
178
+ accountId,
179
+ connId,
180
+ clientId,
181
+ outboundReady,
182
+ preferredForOutbound,
183
+ inboundOnly,
184
+ context: gatewayContext.context,
185
+ });
186
+ runtime.incrementConnectEvents(accountId);
187
+ const lease = runtime.acceptConnection();
188
+
189
+ respond(
190
+ true,
191
+ buildConnectionHandlerConnectResponse({
192
+ channelId: CHANNEL_ID,
193
+ accountId,
194
+ bridgeVersion: runtime.bridgeVersion,
195
+ pushEvent: runtime.pushEvent,
196
+ online: true,
197
+ isPrimary: runtime.isPrimaryConnection(accountId, clientId),
198
+ queueCounters: runtime.buildAccountQueueCounters(accountId),
199
+ diagnostics: runtime.buildExtendedDiagnostics(accountId),
200
+ runtimeFlags: runtime.buildRuntimeFlags(accountId),
201
+ messageAckWaiters: runtime.messageAckWaiterCount(),
202
+ fileAckWaiters: runtime.fileAckWaiterCount(),
203
+ leaseId: lease.leaseId,
204
+ connectionEpoch: lease.connectionEpoch,
205
+ acceptedAt: lease.acceptedAt,
206
+ serverPid: runtime.gatewayPid,
207
+ bridgeId: runtime.bridgeId,
208
+ now: runtime.now(),
209
+ }),
210
+ );
211
+
212
+ runtime.flushOnConnect(accountId);
213
+ },
214
+
215
+ // Activity is the steady-state heartbeat. It refreshes routing/capability
216
+ // state and nudges queued outbound work for the same account.
217
+ handleActivity: async ({ params, respond, client, context }: GatewayRequestHandlerOptions) => {
218
+ await runtime.syncDebugFlag();
219
+ const gatewayContext = buildGatewayContext({ params, client, context });
220
+ const { accountId, connId, clientId, outboundReady, preferredForOutbound, inboundOnly } =
221
+ gatewayContext;
222
+ if (
223
+ runtime.shouldIgnoreStaleEvent({
224
+ kind: 'activity',
225
+ payload: params ?? {},
226
+ accountId,
227
+ connId,
228
+ clientId,
229
+ })
230
+ ) {
231
+ respond(true, { accountId, ok: true, event: 'activity', stale: true, ignored: true });
232
+ return;
233
+ }
234
+ runtime.markLastActivityAt();
235
+ runtime.logInfo(
236
+ 'activity',
237
+ `event ${JSON.stringify(
238
+ buildGatewayDebugFields({
239
+ bridgeId: runtime.bridgeId,
240
+ accountId,
241
+ connId,
242
+ clientId,
243
+ outboundReady,
244
+ preferredForOutbound,
245
+ inboundOnly,
246
+ hasContext: Boolean(context),
247
+ }),
248
+ )}`,
249
+ { debugOnly: true },
250
+ );
251
+ runtime.refreshLiveConnectionState({
252
+ accountId,
253
+ connId,
254
+ clientId,
255
+ outboundReady,
256
+ preferredForOutbound,
257
+ inboundOnly,
258
+ context: gatewayContext.context,
259
+ });
260
+ runtime.incrementActivityEvents(accountId);
261
+
262
+ respond(
263
+ true,
264
+ buildConnectionHandlerActivityResponse({
265
+ accountId,
266
+ queueCounters: runtime.buildAccountQueueCounters(accountId),
267
+ now: runtime.now(),
268
+ }),
269
+ );
270
+ runtime.flushOnActivity(accountId);
271
+ },
272
+
273
+ // Message ACK handler surface -------------------------------------------
274
+
275
+ // Message ACK is intentionally thin here: parsing and outcome transitions
276
+ // stay in the bridge-owned ACK runtime so all queue semantics share one path.
277
+ handleAck: async ({ params, respond, client, context }: GatewayRequestHandlerOptions) => {
278
+ await runtime.syncDebugFlag();
279
+ const prepared = runtime.prepareAckHandling({ params, respond, client, context });
280
+ if (!prepared) return;
281
+
282
+ runtime.markLastAckAt();
283
+ runtime.incrementAckEvents(prepared.accountId);
284
+ runtime.handleAckOutcome({ params, respond, ...prepared });
285
+ },
286
+
287
+ // File-transfer ACK handler surface ------------------------------------
288
+
289
+ // File ACK handling is slightly different from message ACK handling: the
290
+ // handler must validate stage ownership and mutate transfer state before it
291
+ // wakes any waiter bound to the file-transfer lifecycle.
292
+ handleFileAck: async ({ params, respond, client, context }: GatewayRequestHandlerOptions) => {
293
+ const gatewayContext = buildGatewayContext({ params, client, context });
294
+ const { accountId, connId, clientId } = gatewayContext;
295
+
296
+ const transferId = runtime.asString(params?.transferId || '').trim();
297
+ const stage = runtime.asString(params?.stage || '').trim();
298
+ const ok = params?.ok !== false;
299
+ const chunkIndex = runtime.finiteNonNegativeNumberOrNull(params?.chunkIndex);
300
+
301
+ runtime.logInfo(
302
+ 'file-ack-inbound',
303
+ JSON.stringify({
304
+ ...buildGatewayDebugFields({
305
+ bridgeId: runtime.bridgeId,
306
+ accountId,
307
+ connId,
308
+ clientId,
309
+ }),
310
+ transferId,
311
+ stage,
312
+ ackStage: stage,
313
+ ackOutcome: ok ? 'acked' : 'failed',
314
+ ok,
315
+ chunkIndex: chunkIndex != null ? chunkIndex : undefined,
316
+ errorCode: runtime.asString(params?.errorCode || ''),
317
+ errorMessage: runtime.asString(params?.errorMessage || ''),
318
+ path: runtime.asString(params?.path || '').trim(),
319
+ }),
320
+ { debugOnly: true },
321
+ );
322
+
323
+ if (!transferId || !stage) {
324
+ respond(false, { error: 'transferId/stage required' });
325
+ return;
326
+ }
327
+
328
+ if (!isBncrFileAckStage(stage)) {
329
+ respond(false, { error: 'invalid file ack stage' });
330
+ return;
331
+ }
332
+
333
+ const transferState = runtime.fileSendTransfers.get(transferId);
334
+ const fileAckWaiterKey = runtime.fileAckKey(
335
+ transferId,
336
+ stage,
337
+ chunkIndex != null ? chunkIndex : undefined,
338
+ );
339
+ if (!transferState && !runtime.hasFileAckWaiter(fileAckWaiterKey)) {
340
+ respond(false, { error: 'unknown transferId' });
341
+ return;
342
+ }
343
+
344
+ const staleKind = resolveFileAckLeaseEventKind(stage);
345
+ const staleObserved = runtime.observeLease(staleKind, params ?? {});
346
+ const terminalTransfer = hasTerminalFileAckState(transferState) ? transferState : null;
347
+ if (terminalTransfer) {
348
+ respond(
349
+ true,
350
+ buildTerminalFileAckResponse({
351
+ transferId,
352
+ stage,
353
+ state: terminalTransfer.status,
354
+ stale: staleObserved.stale,
355
+ }),
356
+ );
357
+ return;
358
+ }
359
+
360
+ const activeTransfer = transferState;
361
+ const transferOwnerConnId = activeTransfer?.ownerConnId;
362
+ const transferOwnerClientId = activeTransfer?.ownerClientId;
363
+ let resolvedState = activeTransfer?.status ?? 'late';
364
+
365
+ if (staleObserved.stale) {
366
+ const { sameConn, sameClient } = matchesTransferOwner({
367
+ transfer: activeTransfer,
368
+ connId,
369
+ clientId,
370
+ });
371
+ const adopted =
372
+ !(sameConn || sameClient) &&
373
+ runtime.tryAdoptTransferOwner({
374
+ accountId,
375
+ transfer: activeTransfer,
376
+ connId,
377
+ clientId,
378
+ });
379
+ if (!(sameConn || sameClient || adopted)) {
380
+ runtime.logWarn(
381
+ 'stale',
382
+ `ignore kind=file.ack accountId=${accountId} connId=${connId} clientId=${clientId || '-'} transferId=${transferId} stage=${stage} reason=owner-mismatch ownerConnId=${transferOwnerConnId || '-'} ownerClientId=${transferOwnerClientId || '-'}`,
383
+ { debugOnly: true },
384
+ );
385
+ respond(true, { ok: true, stale: true, ignored: true });
386
+ return;
387
+ }
388
+ } else {
389
+ runtime.refreshAcceptedFileTransferLiveState({
390
+ accountId,
391
+ connId,
392
+ clientId,
393
+ context: gatewayContext.context,
394
+ });
395
+ }
396
+
397
+ const fileAckPayload = buildFileAckPayload({
398
+ ok,
399
+ transferId,
400
+ stage,
401
+ path: runtime.asString(params?.path || '').trim(),
402
+ errorCode: runtime.asString(params?.errorCode || ''),
403
+ errorMessage: runtime.asString(params?.errorMessage || ''),
404
+ });
405
+
406
+ if (activeTransfer) {
407
+ applyFileAckState({
408
+ transfer: activeTransfer,
409
+ stage,
410
+ ok,
411
+ chunkIndex,
412
+ now: runtime.now(),
413
+ path: fileAckPayload.path,
414
+ errorCode: fileAckPayload.errorCode,
415
+ errorMessage: fileAckPayload.errorMessage,
416
+ });
417
+ runtime.fileSendTransfers.set(transferId, activeTransfer);
418
+ resolvedState = activeTransfer.status;
419
+ }
420
+
421
+ runtime.resolveFileAck({
422
+ transferId,
423
+ stage,
424
+ chunkIndex: chunkIndex != null ? chunkIndex : undefined,
425
+ payload: fileAckPayload,
426
+ ok,
427
+ });
428
+
429
+ respond(
430
+ true,
431
+ buildHandledFileAckResponse({
432
+ transferId,
433
+ stage,
434
+ state: resolvedState,
435
+ stale: staleObserved.stale,
436
+ }),
437
+ );
438
+ },
439
+ };
440
+ }
@@ -0,0 +1,159 @@
1
+ import { buildCapabilitySnapshot } from '../core/connection-capability.ts';
2
+ import type { BncrConnection } from '../core/types.ts';
3
+ import type { BncrActiveConnectionDebugEntry } from './connection-state.ts';
4
+
5
+ type BncrCapabilityConnection = BncrConnection & {
6
+ outboundReadyUntil?: number;
7
+ preferredForOutboundUntil?: number;
8
+ inboundOnly?: boolean;
9
+ };
10
+
11
+ export function buildConnectionPromotePayload(args: {
12
+ bridgeId: string;
13
+ accountId: string;
14
+ reason: string;
15
+ previousActiveKey: string | null;
16
+ previousActiveConn: BncrConnection | null;
17
+ nextActiveKey: string;
18
+ nextActiveConn: BncrConnection;
19
+ activeConnections: BncrActiveConnectionDebugEntry[];
20
+ }) {
21
+ return {
22
+ bridge: args.bridgeId,
23
+ accountId: args.accountId,
24
+ reason: args.reason,
25
+ previousActiveKey: args.previousActiveKey,
26
+ previousActiveConn: args.previousActiveConn,
27
+ nextActiveKey: args.nextActiveKey,
28
+ nextActiveConn: args.nextActiveConn,
29
+ activeConnections: args.activeConnections,
30
+ };
31
+ }
32
+
33
+ export function buildConnectionCapabilityDebugPayload(args: {
34
+ bridgeId: string;
35
+ accountId: string;
36
+ connection: BncrConnection;
37
+ outboundReady: boolean;
38
+ preferredForOutbound: boolean;
39
+ }) {
40
+ const snapshot = buildCapabilitySnapshot(args.connection);
41
+ return {
42
+ payload: {
43
+ bridge: args.bridgeId,
44
+ accountId: args.accountId,
45
+ connId: args.connection.connId,
46
+ clientId: args.connection.clientId,
47
+ outboundReady: args.outboundReady,
48
+ preferredForOutbound: args.preferredForOutbound,
49
+ inboundOnly: snapshot.inboundOnly,
50
+ outboundReadyUntil: snapshot.outboundReadyUntil,
51
+ preferredForOutboundUntil: snapshot.preferredForOutboundUntil,
52
+ },
53
+ snapshot,
54
+ };
55
+ }
56
+
57
+ export function buildConnectionCapabilityDebugSig(args: {
58
+ bridgeId: string;
59
+ accountId: string;
60
+ connection: BncrConnection;
61
+ outboundReady: boolean;
62
+ preferredForOutbound: boolean;
63
+ snapshot: ReturnType<typeof buildCapabilitySnapshot>;
64
+ nowMs: number;
65
+ }) {
66
+ return JSON.stringify({
67
+ bridge: args.bridgeId,
68
+ accountId: args.accountId,
69
+ connId: args.connection.connId,
70
+ clientId: args.connection.clientId || null,
71
+ outboundReady: args.outboundReady,
72
+ preferredForOutbound: args.preferredForOutbound,
73
+ inboundOnly: args.snapshot.inboundOnly,
74
+ outboundReadyActive: Number(args.snapshot.outboundReadyUntil || 0) > args.nowMs,
75
+ preferredForOutboundActive: Number(args.snapshot.preferredForOutboundUntil || 0) > args.nowMs,
76
+ });
77
+ }
78
+
79
+ export function buildConnectionDegradeSkipPayload(args: {
80
+ bridgeId: string;
81
+ accountId: string;
82
+ connection: BncrConnection;
83
+ reason: string;
84
+ at: number;
85
+ currentActiveKey: string | null;
86
+ degradedKey: string;
87
+ before: ReturnType<typeof buildCapabilitySnapshot>;
88
+ }) {
89
+ return {
90
+ bridge: args.bridgeId,
91
+ accountId: args.accountId,
92
+ connId: args.connection.connId,
93
+ clientId: args.connection.clientId,
94
+ reason: args.reason,
95
+ at: args.at,
96
+ currentActiveKey: args.currentActiveKey,
97
+ degradedKey: args.degradedKey,
98
+ skipReason: 'no-alternative-live-connection',
99
+ before: args.before,
100
+ };
101
+ }
102
+
103
+ export function buildConnectionDegradePayload(args: {
104
+ bridgeId: string;
105
+ accountId: string;
106
+ connection: BncrConnection;
107
+ reason: string;
108
+ at: number;
109
+ currentActiveKey: string | null;
110
+ degradedKey: string;
111
+ before: ReturnType<typeof buildCapabilitySnapshot>;
112
+ after: ReturnType<typeof buildCapabilitySnapshot>;
113
+ }) {
114
+ return {
115
+ bridge: args.bridgeId,
116
+ accountId: args.accountId,
117
+ connId: args.connection.connId,
118
+ clientId: args.connection.clientId,
119
+ reason: args.reason,
120
+ at: args.at,
121
+ currentActiveKey: args.currentActiveKey,
122
+ degradedKey: args.degradedKey,
123
+ before: args.before,
124
+ after: args.after,
125
+ };
126
+ }
127
+
128
+ export function buildSeenConnection(args: {
129
+ accountId: string;
130
+ connId: string;
131
+ clientId?: string;
132
+ nowMs: number;
133
+ previous?: BncrCapabilityConnection | null;
134
+ }): BncrCapabilityConnection {
135
+ return {
136
+ accountId: args.accountId,
137
+ connId: args.connId,
138
+ clientId: String(args.clientId || '').trim() || undefined,
139
+ connectedAt: args.previous?.connectedAt || args.nowMs,
140
+ lastSeenAt: args.nowMs,
141
+ outboundReadyUntil: args.previous?.outboundReadyUntil,
142
+ preferredForOutboundUntil: args.previous?.preferredForOutboundUntil,
143
+ inboundOnly: args.previous?.inboundOnly,
144
+ };
145
+ }
146
+
147
+ export function resolveSeenConnectionPromoteReason(args: {
148
+ currentActiveKey: string | null;
149
+ currentConnection: BncrConnection | null;
150
+ nowMs: number;
151
+ connectTtlMs: number;
152
+ }) {
153
+ if (!args.currentActiveKey) return 'no-current-active' as const;
154
+ if (!args.currentConnection) return 'current-missing' as const;
155
+ if (args.nowMs - args.currentConnection.lastSeenAt > args.connectTtlMs) {
156
+ return 'current-stale' as const;
157
+ }
158
+ return null;
159
+ }
@@ -0,0 +1,51 @@
1
+ import { createBncrConnectionState } from './connection-state.ts';
2
+
3
+ export function createBncrConnectionStateRuntimeGroup(runtime: {
4
+ bridgeId: string;
5
+ now: () => number;
6
+ asString: (value: unknown, fallback?: string) => string;
7
+ connectTtlMs: number;
8
+ recentInboundSendWindowMs: number;
9
+ outboundReadyTtlMs: number;
10
+ preferredOutboundTtlMs: number;
11
+ connections: Parameters<typeof createBncrConnectionState>[0]['connections'];
12
+ activeConnectionByAccount: Parameters<
13
+ typeof createBncrConnectionState
14
+ >[0]['activeConnectionByAccount'];
15
+ lastInboundByAccount: Parameters<typeof createBncrConnectionState>[0]['lastInboundByAccount'];
16
+ lastActivityByAccount: Parameters<typeof createBncrConnectionState>[0]['lastActivityByAccount'];
17
+ gcTransientState: Parameters<typeof createBncrConnectionState>[0]['gcTransientState'];
18
+ connectionKey: Parameters<typeof createBncrConnectionState>[0]['connectionKey'];
19
+ buildActiveConnectionDebugList: Parameters<
20
+ typeof createBncrConnectionState
21
+ >[0]['buildActiveConnectionDebugList'];
22
+ rememberGatewayContext: Parameters<typeof createBncrConnectionState>[0]['rememberGatewayContext'];
23
+ markActivity: Parameters<typeof createBncrConnectionState>[0]['markActivity'];
24
+ logInfo: Parameters<typeof createBncrConnectionState>[0]['logInfo'];
25
+ logInfoDedupJson: Parameters<typeof createBncrConnectionState>[0]['logInfoDedupJson'];
26
+ }) {
27
+ const connectionState = createBncrConnectionState({
28
+ bridgeId: runtime.bridgeId,
29
+ now: runtime.now,
30
+ asString: runtime.asString,
31
+ connectTtlMs: runtime.connectTtlMs,
32
+ recentInboundSendWindowMs: runtime.recentInboundSendWindowMs,
33
+ outboundReadyTtlMs: runtime.outboundReadyTtlMs,
34
+ preferredOutboundTtlMs: runtime.preferredOutboundTtlMs,
35
+ connections: runtime.connections,
36
+ activeConnectionByAccount: runtime.activeConnectionByAccount,
37
+ lastInboundByAccount: runtime.lastInboundByAccount,
38
+ lastActivityByAccount: runtime.lastActivityByAccount,
39
+ gcTransientState: runtime.gcTransientState,
40
+ connectionKey: runtime.connectionKey,
41
+ buildActiveConnectionDebugList: runtime.buildActiveConnectionDebugList,
42
+ rememberGatewayContext: runtime.rememberGatewayContext,
43
+ markActivity: runtime.markActivity,
44
+ logInfo: runtime.logInfo,
45
+ logInfoDedupJson: runtime.logInfoDedupJson,
46
+ });
47
+
48
+ return {
49
+ connectionState,
50
+ };
51
+ }