@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,248 @@
1
+ import type { GatewayRequestHandlerOptions } from 'openclaw/plugin-sdk/core';
2
+ import type { BncrConnection, BncrRoute } from '../core/types.ts';
3
+ import type { parseBncrInboundParams } from '../messaging/inbound/parse.ts';
4
+ import type {
5
+ buildInboundResponsePayload,
6
+ resolveInboundSessionContext,
7
+ } from './channel-inbound-helpers.ts';
8
+ import type { BncrChannelConfigRoot } from './channel-runtime-types.ts';
9
+ import { buildBncrGatewayEventContext } from './gateway-event-context.ts';
10
+
11
+ type InboundLifecycleStage = 'accepted';
12
+ type LeaseEventKind =
13
+ | 'inbound'
14
+ | 'activity'
15
+ | 'ack'
16
+ | 'file.init'
17
+ | 'file.chunk'
18
+ | 'file.complete'
19
+ | 'file.abort';
20
+ type InboundResponseArgs =
21
+ | { kind: 'stale-ignored'; accountId: string; msgId?: string | null }
22
+ | {
23
+ kind: 'accepted';
24
+ accountId: string;
25
+ sessionKey: string;
26
+ msgId?: string | null;
27
+ taskKey?: string | null;
28
+ }
29
+ | { kind: 'invalid-peer' }
30
+ | { kind: 'invalid-session'; accountId: string; msgId?: string | null };
31
+
32
+ type ParsedInboundParams = ReturnType<typeof parseBncrInboundParams>;
33
+ type InboundResponsePayload = ReturnType<typeof buildInboundResponsePayload>;
34
+ type EnsureCanonicalAgentIdPeer = Parameters<typeof resolveInboundSessionContext>[0]['peer'];
35
+ type InboundAcceptanceResult =
36
+ | {
37
+ ok: true;
38
+ accountId?: string;
39
+ sessionKey: string;
40
+ inboundText: string;
41
+ hasMedia: boolean;
42
+ }
43
+ | {
44
+ ok: false;
45
+ status: boolean;
46
+ payload: Record<string, unknown>;
47
+ };
48
+
49
+ export type BncrInboundHandlersRuntime = {
50
+ channelId: string;
51
+ bridgeId: string;
52
+ asString: (value: unknown, fallback?: string) => string;
53
+ now: () => number;
54
+ syncDebugFlag: () => Promise<void>;
55
+ parseInboundParams: (params: unknown) => ParsedInboundParams;
56
+ shouldIgnoreStaleEvent: (args: {
57
+ kind: LeaseEventKind;
58
+ payload: { leaseId?: string; connectionEpoch?: number };
59
+ accountId: string;
60
+ connId: string;
61
+ clientId?: string;
62
+ }) => boolean;
63
+ buildInboundResponsePayload: (args: InboundResponseArgs) => InboundResponsePayload;
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
+ logInfo: (scope: string, message: string, options?: { debugOnly?: boolean }) => void;
74
+ logError: (scope: string, message: string, options?: { debugOnly?: boolean }) => void;
75
+ buildInboundAcceptedLifecycleDebugInfo: (args: {
76
+ stage: InboundLifecycleStage;
77
+ bridge: string;
78
+ accountId: string;
79
+ connId: string;
80
+ clientId?: string;
81
+ outboundReady: boolean;
82
+ preferredForOutbound: boolean;
83
+ inboundOnly: boolean;
84
+ onlineAfterSeen: boolean;
85
+ recentInboundReachable: boolean;
86
+ activeConnectionKey: string | null;
87
+ activeConnections: BncrConnection[];
88
+ }) => Record<string, unknown>;
89
+ isOnline: (accountId: string) => boolean;
90
+ hasRecentInboundReachability: (accountId: string) => boolean;
91
+ getActiveConnectionKey: (accountId: string) => string | null;
92
+ buildActiveConnectionDebugList: (accountId: string) => BncrConnection[];
93
+ markLastInboundAt: (accountId: string) => void;
94
+ getConfig: () => BncrChannelConfigRoot;
95
+ ensureCanonicalAgentId: (args: {
96
+ cfg: BncrChannelConfigRoot;
97
+ accountId: string;
98
+ peer: EnsureCanonicalAgentIdPeer;
99
+ channelId: string;
100
+ }) => string;
101
+ prepareInboundAcceptance: (args: {
102
+ parsed: ParsedInboundParams;
103
+ canonicalAgentId: string;
104
+ }) => Promise<InboundAcceptanceResult>;
105
+ formatDisplayScope: (route: BncrRoute) => string;
106
+ logInboundSummary: (args: {
107
+ accountId: string;
108
+ route: BncrRoute;
109
+ msgType: string;
110
+ text: string;
111
+ hasMedia: boolean;
112
+ }) => void;
113
+ respond: GatewayRequestHandlerOptions['respond'];
114
+ flushOnInboundAccepted: (accountId: string) => void;
115
+ dispatchInbound: (args: {
116
+ cfg: BncrChannelConfigRoot;
117
+ parsed: ParsedInboundParams;
118
+ canonicalAgentId: string;
119
+ }) => Promise<unknown>;
120
+ };
121
+
122
+ export function createBncrInboundHandlers(runtime: Omit<BncrInboundHandlersRuntime, 'respond'>) {
123
+ return {
124
+ handleInbound: async ({ params, respond, client, context }: GatewayRequestHandlerOptions) => {
125
+ await runtime.syncDebugFlag();
126
+ const parsed = runtime.parseInboundParams(params);
127
+ const { accountId, platform, route, msgType, msgId, peer, extracted } = parsed;
128
+ const gatewayContext = buildBncrGatewayEventContext({
129
+ params,
130
+ client,
131
+ context,
132
+ asString: runtime.asString,
133
+ normalizeAccountId: (value) => value,
134
+ now: runtime.now,
135
+ });
136
+ const { connId, clientId, outboundReady, preferredForOutbound, inboundOnly } = gatewayContext;
137
+ if (
138
+ runtime.shouldIgnoreStaleEvent({
139
+ kind: 'inbound',
140
+ payload: params ?? {},
141
+ accountId,
142
+ connId,
143
+ clientId,
144
+ })
145
+ ) {
146
+ respond(
147
+ true,
148
+ runtime.buildInboundResponsePayload({
149
+ kind: 'stale-ignored',
150
+ accountId,
151
+ msgId: msgId ?? null,
152
+ }),
153
+ );
154
+ return;
155
+ }
156
+ runtime.refreshLiveConnectionState({
157
+ accountId,
158
+ connId,
159
+ clientId,
160
+ outboundReady,
161
+ preferredForOutbound,
162
+ inboundOnly,
163
+ context: gatewayContext.context,
164
+ });
165
+ runtime.logInfo(
166
+ 'inbound',
167
+ `lifecycle ${JSON.stringify(
168
+ runtime.buildInboundAcceptedLifecycleDebugInfo({
169
+ stage: 'accepted',
170
+ bridge: runtime.bridgeId,
171
+ accountId,
172
+ connId,
173
+ clientId,
174
+ outboundReady,
175
+ preferredForOutbound,
176
+ inboundOnly,
177
+ onlineAfterSeen: runtime.isOnline(accountId),
178
+ recentInboundReachable: runtime.hasRecentInboundReachability(accountId),
179
+ activeConnectionKey: runtime.getActiveConnectionKey(accountId),
180
+ activeConnections: runtime.buildActiveConnectionDebugList(accountId),
181
+ }),
182
+ )}`,
183
+ { debugOnly: true },
184
+ );
185
+ runtime.markLastInboundAt(accountId);
186
+
187
+ const cfg = runtime.getConfig();
188
+ const canonicalAgentId = runtime.ensureCanonicalAgentId({
189
+ cfg,
190
+ accountId,
191
+ peer,
192
+ channelId: runtime.channelId,
193
+ });
194
+ const acceptance = await runtime.prepareInboundAcceptance({ parsed, canonicalAgentId });
195
+ if (!acceptance.ok) {
196
+ respond(acceptance.status, acceptance.payload);
197
+ return;
198
+ }
199
+
200
+ const { sessionKey, inboundText, hasMedia } = acceptance;
201
+ runtime.logInfo(
202
+ 'inbound',
203
+ JSON.stringify({
204
+ accountId,
205
+ msgId: msgId ?? null,
206
+ platform,
207
+ chatType: peer.kind,
208
+ scope: runtime.formatDisplayScope(route),
209
+ sessionKey,
210
+ msgType,
211
+ textLen: inboundText.length,
212
+ textPreview: inboundText.slice(0, 120),
213
+ hasMedia,
214
+ }),
215
+ { debugOnly: true },
216
+ );
217
+ runtime.logInboundSummary({
218
+ accountId,
219
+ route,
220
+ msgType,
221
+ text: inboundText,
222
+ hasMedia,
223
+ });
224
+
225
+ respond(
226
+ true,
227
+ runtime.buildInboundResponsePayload({
228
+ kind: 'accepted',
229
+ accountId,
230
+ sessionKey,
231
+ msgId: msgId ?? null,
232
+ taskKey: extracted.taskKey ?? null,
233
+ }),
234
+ );
235
+ runtime.flushOnInboundAccepted(accountId);
236
+
237
+ void runtime
238
+ .dispatchInbound({
239
+ cfg,
240
+ parsed,
241
+ canonicalAgentId,
242
+ })
243
+ .catch((err) => {
244
+ runtime.logError('inbound', `process failed: ${String(err)}`, { debugOnly: true });
245
+ });
246
+ },
247
+ };
248
+ }
@@ -0,0 +1,152 @@
1
+ import type { GatewayRequestHandlerOptions } from 'openclaw/plugin-sdk/core';
2
+ import type { BncrConnection, BncrRoute } from '../core/types.ts';
3
+ import {
4
+ createBncrFileInboundHandlersComponent,
5
+ createBncrInboundHandlersComponent,
6
+ } from './channel-components.ts';
7
+ import type {
8
+ buildInboundAcceptedLifecycleDebugInfo,
9
+ buildInboundResponsePayload,
10
+ } from './channel-inbound-helpers.ts';
11
+ import type { createBncrFileInboundHandlers } from './file-inbound-handlers.ts';
12
+
13
+ export function createBncrInboundSurfaceHandlersGroup(runtime: {
14
+ getApi: Parameters<typeof createBncrFileInboundHandlersComponent>[0]['getApi'];
15
+ channelId: string;
16
+ bridgeId: string;
17
+ pluginRoot: string;
18
+ asString: (value: unknown, fallback?: string) => string;
19
+ now: () => number;
20
+ normalizeAccountId: (value: string) => string;
21
+ finiteNonNegativeNumberOrNull: (value: unknown) => number | null;
22
+ syncDebugFlag: () => Promise<void>;
23
+ shouldIgnoreStaleEvent: (args: {
24
+ kind:
25
+ | 'inbound'
26
+ | 'activity'
27
+ | 'ack'
28
+ | 'file.init'
29
+ | 'file.chunk'
30
+ | 'file.complete'
31
+ | 'file.abort';
32
+ payload: { leaseId?: string; connectionEpoch?: number };
33
+ accountId: string;
34
+ connId: string;
35
+ clientId?: string;
36
+ }) => boolean;
37
+ observeLease: (
38
+ kind:
39
+ | 'connect'
40
+ | 'inbound'
41
+ | 'activity'
42
+ | 'ack'
43
+ | 'file.init'
44
+ | 'file.chunk'
45
+ | 'file.complete'
46
+ | 'file.abort',
47
+ payload: { leaseId?: string; connectionEpoch?: number },
48
+ ) => { stale: boolean };
49
+ matchesTransferOwner: Parameters<typeof createBncrFileInboundHandlers>[0]['matchesTransferOwner'];
50
+ refreshAcceptedFileTransferLiveState: (args: {
51
+ accountId: string;
52
+ connId: string;
53
+ clientId?: string;
54
+ context: GatewayRequestHandlerOptions['context'];
55
+ }) => void;
56
+ refreshLiveConnectionState: (args: {
57
+ accountId: string;
58
+ connId: string;
59
+ clientId?: string;
60
+ outboundReady: boolean;
61
+ preferredForOutbound: boolean;
62
+ inboundOnly: boolean;
63
+ context: GatewayRequestHandlerOptions['context'];
64
+ }) => void;
65
+ logInfo: (scope: string, message: string, options?: { debugOnly?: boolean }) => void;
66
+ logWarn: Parameters<typeof createBncrFileInboundHandlers>[0]['logWarn'];
67
+ logError: (scope: string, message: string, options?: { debugOnly?: boolean }) => void;
68
+ buildInboundResponsePayload: typeof buildInboundResponsePayload;
69
+ buildInboundAcceptedLifecycleDebugInfo: typeof buildInboundAcceptedLifecycleDebugInfo;
70
+ isOnline: (accountId: string) => boolean;
71
+ hasRecentInboundReachability: (accountId: string) => boolean;
72
+ getActiveConnectionKey: (accountId: string) => string | null;
73
+ buildActiveConnectionDebugList: (accountId: string) => BncrConnection[];
74
+ markLastInboundAt: (accountId: string) => void;
75
+ ensureCanonicalAgentId: Parameters<
76
+ typeof createBncrInboundHandlersComponent
77
+ >[0]['ensureCanonicalAgentId'];
78
+ prepareInboundAcceptance: Parameters<
79
+ typeof createBncrInboundHandlersComponent
80
+ >[0]['prepareInboundAcceptance'];
81
+ logInboundSummary: (args: {
82
+ accountId: string;
83
+ route: BncrRoute;
84
+ msgType: string;
85
+ text: string;
86
+ hasMedia: boolean;
87
+ }) => void;
88
+ flushPushQueueBestEffort: (args?: {
89
+ accountId?: string;
90
+ trigger?: string;
91
+ reason?: string;
92
+ }) => void;
93
+ rememberSessionRoute: (sessionKey: string, accountId: string, route: BncrRoute) => void;
94
+ enqueueFromReply: Parameters<typeof createBncrInboundHandlersComponent>[0]['enqueueFromReply'];
95
+ setInboundActivity: (accountId: string, at: number) => void;
96
+ scheduleSave: () => void;
97
+ fileRecvTransfers: Parameters<
98
+ typeof createBncrFileInboundHandlersComponent
99
+ >[0]['fileRecvTransfers'];
100
+ inboundFileTransferMaxBytes: number;
101
+ inboundFileTransferMaxChunks: number;
102
+ }) {
103
+ const fileInboundHandlers = createBncrFileInboundHandlersComponent({
104
+ getApi: runtime.getApi,
105
+ asString: runtime.asString,
106
+ now: runtime.now,
107
+ normalizeAccountId: runtime.normalizeAccountId,
108
+ finiteNonNegativeNumberOrNull: runtime.finiteNonNegativeNumberOrNull,
109
+ shouldIgnoreStaleEvent: runtime.shouldIgnoreStaleEvent,
110
+ observeLease: runtime.observeLease,
111
+ matchesTransferOwner: runtime.matchesTransferOwner,
112
+ refreshAcceptedFileTransferLiveState: runtime.refreshAcceptedFileTransferLiveState,
113
+ logWarn: runtime.logWarn,
114
+ fileRecvTransfers: runtime.fileRecvTransfers,
115
+ inboundFileTransferMaxBytes: runtime.inboundFileTransferMaxBytes,
116
+ inboundFileTransferMaxChunks: runtime.inboundFileTransferMaxChunks,
117
+ });
118
+
119
+ const inboundHandlers = createBncrInboundHandlersComponent({
120
+ getApi: runtime.getApi,
121
+ channelId: runtime.channelId,
122
+ bridgeId: runtime.bridgeId,
123
+ pluginRoot: runtime.pluginRoot,
124
+ asString: runtime.asString,
125
+ now: runtime.now,
126
+ syncDebugFlag: runtime.syncDebugFlag,
127
+ shouldIgnoreStaleEvent: runtime.shouldIgnoreStaleEvent,
128
+ buildInboundResponsePayload: runtime.buildInboundResponsePayload,
129
+ refreshLiveConnectionState: runtime.refreshLiveConnectionState,
130
+ logInfo: runtime.logInfo,
131
+ logError: runtime.logError,
132
+ buildInboundAcceptedLifecycleDebugInfo: runtime.buildInboundAcceptedLifecycleDebugInfo,
133
+ isOnline: runtime.isOnline,
134
+ hasRecentInboundReachability: runtime.hasRecentInboundReachability,
135
+ getActiveConnectionKey: runtime.getActiveConnectionKey,
136
+ buildActiveConnectionDebugList: runtime.buildActiveConnectionDebugList,
137
+ markLastInboundAt: runtime.markLastInboundAt,
138
+ ensureCanonicalAgentId: runtime.ensureCanonicalAgentId,
139
+ prepareInboundAcceptance: runtime.prepareInboundAcceptance,
140
+ logInboundSummary: runtime.logInboundSummary,
141
+ flushPushQueueBestEffort: runtime.flushPushQueueBestEffort,
142
+ rememberSessionRoute: runtime.rememberSessionRoute,
143
+ enqueueFromReply: runtime.enqueueFromReply,
144
+ setInboundActivity: runtime.setInboundActivity,
145
+ scheduleSave: runtime.scheduleSave,
146
+ });
147
+
148
+ return {
149
+ fileInboundHandlers,
150
+ inboundHandlers,
151
+ };
152
+ }
@@ -0,0 +1,90 @@
1
+ import {
2
+ buildMediaTextFallback,
3
+ type MediaDedupeCacheEntry,
4
+ normalizeMessageText,
5
+ normalizeReplyToId,
6
+ } from '../messaging/outbound/media-dedupe.ts';
7
+
8
+ function asString(v: unknown, fallback = ''): string {
9
+ if (typeof v === 'string') return v;
10
+ if (v == null) return fallback;
11
+ return String(v);
12
+ }
13
+
14
+ export function createBncrMediaDedupeRuntime(runtime: {
15
+ now: () => number;
16
+ recentMediaDedupeBySession: Map<string, Map<string, MediaDedupeCacheEntry>>;
17
+ }) {
18
+ function pruneMediaDedupeCache(sessionKey: string, currentTime = runtime.now()) {
19
+ const sessionCache = runtime.recentMediaDedupeBySession.get(sessionKey);
20
+ if (!sessionCache) return;
21
+
22
+ for (const [mediaUrl, entry] of sessionCache.entries()) {
23
+ if (currentTime - entry.createdAt > 10_000) {
24
+ sessionCache.delete(mediaUrl);
25
+ }
26
+ }
27
+
28
+ if (sessionCache.size === 0) {
29
+ runtime.recentMediaDedupeBySession.delete(sessionKey);
30
+ }
31
+ }
32
+
33
+ function rememberRecentMediaSend(params: {
34
+ sessionKey: string;
35
+ mediaUrl: string;
36
+ text: string;
37
+ replyToId: string;
38
+ createdAt?: number;
39
+ }) {
40
+ const sessionKey = asString(params.sessionKey || '').trim();
41
+ const mediaUrl = asString(params.mediaUrl || '').trim();
42
+ if (!sessionKey || !mediaUrl) return;
43
+
44
+ const createdAt = typeof params.createdAt === 'number' ? params.createdAt : runtime.now();
45
+ pruneMediaDedupeCache(sessionKey, createdAt);
46
+ let sessionCache = runtime.recentMediaDedupeBySession.get(sessionKey);
47
+ if (!sessionCache) {
48
+ sessionCache = new Map<string, MediaDedupeCacheEntry>();
49
+ runtime.recentMediaDedupeBySession.set(sessionKey, sessionCache);
50
+ }
51
+ sessionCache.set(mediaUrl, {
52
+ mediaUrl,
53
+ text: normalizeMessageText(params.text),
54
+ replyToId: normalizeReplyToId(params.replyToId),
55
+ createdAt,
56
+ });
57
+ }
58
+
59
+ function tryBuildMediaDedupeFallback(params: {
60
+ sessionKey: string;
61
+ mediaUrl: string;
62
+ text: string;
63
+ replyToId: string;
64
+ currentTime?: number;
65
+ }): { text: string; reason: 'same-text-sent-checkmark' | 'text-changed-downgrade' } | null {
66
+ const sessionKey = asString(params.sessionKey || '').trim();
67
+ const mediaUrl = asString(params.mediaUrl || '').trim();
68
+ if (!sessionKey || !mediaUrl) return null;
69
+
70
+ const currentTime = typeof params.currentTime === 'number' ? params.currentTime : runtime.now();
71
+ pruneMediaDedupeCache(sessionKey, currentTime);
72
+ const sessionCache = runtime.recentMediaDedupeBySession.get(sessionKey);
73
+ const previous = sessionCache?.get(mediaUrl);
74
+ if (!previous) return null;
75
+ if (currentTime - previous.createdAt > 10_000) return null;
76
+
77
+ return buildMediaTextFallback({
78
+ currentText: normalizeMessageText(params.text),
79
+ previousText: previous.text,
80
+ currentReplyToId: normalizeReplyToId(params.replyToId),
81
+ previousReplyToId: previous.replyToId,
82
+ });
83
+ }
84
+
85
+ return {
86
+ pruneMediaDedupeCache,
87
+ rememberRecentMediaSend,
88
+ tryBuildMediaDedupeFallback,
89
+ };
90
+ }