@xmoxmo/bncr 0.5.7 → 0.5.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/package.json +1 -1
  2. package/scripts/check-pack.mjs +0 -2
  3. package/scripts/selfcheck.mjs +0 -3
  4. package/src/bootstrap/runtime-loader.ts +1 -1
  5. package/src/channel.ts +49 -138
  6. package/src/core/connection-reachability.ts +1 -1
  7. package/src/core/logging.ts +1 -1
  8. package/src/core/outbox-entry-builders.ts +60 -82
  9. package/src/core/outbox-summary.ts +5 -39
  10. package/src/core/permissions.ts +1 -1
  11. package/src/core/register-trace.ts +2 -2
  12. package/src/messaging/inbound/parse.ts +1 -1
  13. package/src/messaging/outbound/diagnostics-debug-builders.ts +0 -15
  14. package/src/messaging/outbound/diagnostics.ts +0 -1
  15. package/src/messaging/outbound/marker-parser.ts +215 -0
  16. package/src/messaging/outbound/media-dedupe.ts +1 -4
  17. package/src/messaging/outbound/normalize-outbound-send.ts +9 -7
  18. package/src/messaging/outbound/reply-enqueue-media.ts +1 -184
  19. package/src/messaging/outbound/reply-enqueue.ts +249 -162
  20. package/src/openclaw/media-runtime.ts +1 -1
  21. package/src/plugin/bridge-media-facade.ts +0 -3
  22. package/src/plugin/bridge-outbox-facade.ts +0 -1
  23. package/src/plugin/channel-runtime-builders-delivery.ts +2 -362
  24. package/src/plugin/channel-runtime-builders-status.ts +0 -160
  25. package/src/plugin/channel-runtime-builders.ts +3 -17
  26. package/src/plugin/channel-runtime-constants.ts +1 -1
  27. package/src/plugin/channel-send.ts +5 -4
  28. package/src/plugin/file-transfer-orchestrator.ts +39 -2
  29. package/src/plugin/media-orchestrators-runtime-group.ts +20 -149
  30. package/src/plugin/messaging.ts +3 -3
  31. package/src/plugin/outbox-push.ts +1 -52
  32. package/src/plugin/outbox-unified-push-flow.ts +307 -0
  33. package/src/plugin/scene-registry.ts +1 -1
  34. package/src/runtime/log-dedupe.ts +3 -3
  35. package/src/runtime/outbound-ack-timeout.ts +2 -4
  36. package/src/runtime/status-snapshots.ts +2 -2
  37. package/src/runtime/status-worker.ts +2 -6
  38. package/src/core/outbox-text-push-prep.ts +0 -1
  39. package/src/plugin/bridge-file-transfer-push-facade.ts +0 -142
  40. package/src/plugin/outbox-file-push-flow.ts +0 -69
  41. package/src/plugin/outbox-text-push-flow.ts +0 -90
@@ -193,7 +193,38 @@ export function createBncrFileTransferOrchestrator(runtime: BncrFileTransferOrch
193
193
  };
194
194
  const HTTP_URL_RE = /^https?:\/\//i;
195
195
 
196
- /** Try HEAD for Content-Type; fall back to Range+sniff for magic bytes when unclear. */
196
+ const EXT_TO_MIME: Record<string, string> = {
197
+ jpg: 'image/jpeg',
198
+ jpeg: 'image/jpeg',
199
+ png: 'image/png',
200
+ gif: 'image/gif',
201
+ webp: 'image/webp',
202
+ bmp: 'image/bmp',
203
+ heic: 'image/heic',
204
+ heif: 'image/heif',
205
+ mp4: 'video/mp4',
206
+ mov: 'video/quicktime',
207
+ mkv: 'video/x-matroska',
208
+ webm: 'video/webm',
209
+ avi: 'video/x-msvideo',
210
+ m4v: 'video/x-m4v',
211
+ mp3: 'audio/mpeg',
212
+ wav: 'audio/wav',
213
+ ogg: 'audio/ogg',
214
+ oga: 'audio/ogg',
215
+ opus: 'audio/opus',
216
+ m4a: 'audio/mp4',
217
+ aac: 'audio/aac',
218
+ flac: 'audio/flac',
219
+ };
220
+
221
+ function mimeFromUrlExt(url: string): string | undefined {
222
+ const clean = url.split(/[?#]/, 1)[0] || '';
223
+ const ext = clean.includes('.') ? clean.slice(clean.lastIndexOf('.') + 1).toLowerCase() : '';
224
+ return EXT_TO_MIME[ext];
225
+ }
226
+
227
+ /** Try HEAD for Content-Type; fall back to Range+sniff for magic bytes; then URL extension. */
197
228
  async function resolveRemoteMediaType(url: string): Promise<{ mimeType?: string }> {
198
229
  // 1) HEAD -> Content-Type
199
230
  let headerType = '';
@@ -229,8 +260,14 @@ export function createBncrFileTransferOrchestrator(runtime: BncrFileTransferOrch
229
260
  if (!sniffed) return {};
230
261
  return { mimeType: sniffed };
231
262
  } catch {
232
- return {};
263
+ /* Range sniff failed, fall through to extension inference */
233
264
  }
265
+
266
+ // 3) URL extension -> mime type (fallback when HEAD and Range both fail)
267
+ const extMime = mimeFromUrlExt(url);
268
+ if (extMime) return { mimeType: extMime };
269
+
270
+ return {};
234
271
  }
235
272
 
236
273
  /** Minimal magic-byte MIME sniffer for the first ~512 bytes. */
@@ -1,21 +1,13 @@
1
1
  import type { BncrRoute, FileSendTransferState, OutboxEntry } from '../core/types.ts';
2
- import { normalizeMessageText } from '../messaging/outbound/media-dedupe.ts';
3
2
  import type {
4
3
  NormalizedReplyPayload,
5
4
  OutboundReplyTargetPolicy,
6
- ReplyMediaEntriesParams,
7
5
  ReplyPayloadInput,
8
6
  } from '../messaging/outbound/reply-enqueue.ts';
9
7
  import {
10
8
  enqueueNormalizedReplyPayload,
11
- enqueueReplyTextEntry,
12
9
  normalizeReplyPayload,
13
10
  } from '../messaging/outbound/reply-enqueue.ts';
14
- import {
15
- enqueueReplyMediaFallbackTextEntry,
16
- enqueueReplyMediaFileTransferEntry,
17
- enqueueSingleReplyMediaEntry,
18
- } from '../messaging/outbound/reply-enqueue-media.ts';
19
11
  import type { createBncrFileAckRuntime } from './file-ack-runtime.ts';
20
12
  import { createBncrFileTransferOrchestrator } from './file-transfer-orchestrator.ts';
21
13
  import type { createBncrFileTransferSetup } from './file-transfer-setup.ts';
@@ -27,92 +19,12 @@ type FileAckPayload = Awaited<
27
19
  ReturnType<ReturnType<typeof createBncrFileAckRuntime>['waitForFileAck']>
28
20
  >;
29
21
 
30
- function buildReplyMediaEntryHelpers(runtime: {
31
- logInfo: (scope: string | undefined, message: string, options?: { debugOnly?: boolean }) => void;
32
- enqueueOutbound: (entry: OutboxEntry) => void;
33
- buildTextOutboxEntry: (args: {
34
- accountId: string;
35
- sessionKey: string;
36
- route: BncrRoute;
37
- text: string;
38
- kind?: 'tool' | 'block' | 'final';
39
- replyToId?: string;
40
- replyTargetPolicy?: OutboundReplyTargetPolicy;
41
- }) => OutboxEntry;
42
- buildFileTransferOutboxEntry: (args: {
43
- accountId: string;
44
- sessionKey: string;
45
- route: BncrRoute;
46
- mediaUrl: string;
47
- mediaLocalRoots?: readonly string[];
48
- text: string;
49
- asVoice: boolean;
50
- audioAsVoice: boolean;
51
- type?: string;
52
- extra?: Record<string, unknown>;
53
- kind?: 'tool' | 'block' | 'final';
54
- replyToId?: string;
55
- replyTargetPolicy?: OutboundReplyTargetPolicy;
56
- downloadMedia?: boolean;
57
- }) => OutboxEntry;
58
- rememberRecentMediaSend: (args: {
59
- sessionKey: string;
60
- mediaUrl: string;
61
- text: string;
62
- replyToId: string;
63
- createdAt: number;
64
- }) => void;
65
- }) {
66
- return {
67
- enqueueReplyMediaFallbackTextEntry: (
68
- params: Parameters<typeof enqueueReplyMediaFallbackTextEntry>[0],
69
- ) =>
70
- enqueueReplyMediaFallbackTextEntry(params, {
71
- logInfo: runtime.logInfo,
72
- enqueueOutbound: runtime.enqueueOutbound,
73
- buildTextOutboxEntry: runtime.buildTextOutboxEntry,
74
- }),
75
- enqueueReplyMediaFileTransferEntry: (
76
- params: Parameters<typeof enqueueReplyMediaFileTransferEntry>[0],
77
- ) =>
78
- enqueueReplyMediaFileTransferEntry(params, {
79
- enqueueOutbound: runtime.enqueueOutbound,
80
- buildFileTransferOutboxEntry: runtime.buildFileTransferOutboxEntry,
81
- rememberRecentMediaSend: runtime.rememberRecentMediaSend,
82
- }),
83
- };
84
- }
85
-
86
- function buildReplyTextEntryHelper(runtime: {
87
- enqueueOutbound: (entry: OutboxEntry) => void;
88
- buildTextOutboxEntry: (args: {
89
- accountId: string;
90
- sessionKey: string;
91
- route: BncrRoute;
92
- text: string;
93
- kind?: 'tool' | 'block' | 'final';
94
- replyToId?: string;
95
- replyTargetPolicy?: OutboundReplyTargetPolicy;
96
- }) => OutboxEntry;
97
- }) {
98
- return (params: {
99
- accountId: string;
100
- sessionKey: string;
101
- route: BncrRoute;
102
- payload: NormalizedReplyPayload;
103
- }) =>
104
- enqueueReplyTextEntry(params, {
105
- enqueueOutbound: runtime.enqueueOutbound,
106
- buildTextOutboxEntry: runtime.buildTextOutboxEntry,
107
- });
108
- }
109
-
110
- function normalizeReplyOrchestratorPayload(args: {
22
+ async function normalizeReplyOrchestratorPayload(args: {
111
23
  payload: ReplyPayloadInput;
112
24
  asString: (value: unknown, fallback?: string) => string;
113
25
  replyTargetPolicy?: OutboundReplyTargetPolicy;
114
26
  }) {
115
- return normalizeReplyPayload(
27
+ return await normalizeReplyPayload(
116
28
  args.payload,
117
29
  { asString: args.asString },
118
30
  {
@@ -186,29 +98,21 @@ export function createBncrMediaOrchestratorsRuntimeGroup(runtime: {
186
98
  payload: NormalizedReplyPayload;
187
99
  }) => void;
188
100
  enqueueOutbound: (entry: OutboxEntry) => void;
189
- buildTextOutboxEntry: (args: {
101
+ buildOutboxEntry: (args: {
190
102
  accountId: string;
191
103
  sessionKey: string;
192
104
  route: BncrRoute;
193
- text: string;
194
- kind?: 'tool' | 'block' | 'final';
195
- replyToId?: string;
196
- replyTargetPolicy?: OutboundReplyTargetPolicy;
197
- }) => OutboxEntry;
198
- buildFileTransferOutboxEntry: (args: {
199
- accountId: string;
200
- sessionKey: string;
201
- route: BncrRoute;
202
- mediaUrl: string;
203
- mediaLocalRoots?: readonly string[];
204
- text: string;
205
- asVoice: boolean;
206
- audioAsVoice: boolean;
207
105
  type?: string;
208
- extra?: Record<string, unknown>;
106
+ msg: string;
209
107
  kind?: 'tool' | 'block' | 'final';
210
108
  replyToId?: string;
211
109
  replyTargetPolicy?: OutboundReplyTargetPolicy;
110
+ extra?: Record<string, unknown>;
111
+ transferMode?: 'text' | 'media';
112
+ mediaUrl?: string;
113
+ mediaLocalRoots?: readonly string[];
114
+ asVoice?: boolean;
115
+ audioAsVoice?: boolean;
212
116
  downloadMedia?: boolean;
213
117
  }) => OutboxEntry;
214
118
  rememberRecentMediaSend: (args: {
@@ -244,40 +148,7 @@ export function createBncrMediaOrchestratorsRuntimeGroup(runtime: {
244
148
  logFileTransferCompleteAck: runtime.logFileTransferCompleteAck,
245
149
  });
246
150
 
247
- const replyMediaEntryHelpers = buildReplyMediaEntryHelpers(runtime);
248
- const replyTextEntryHelper = buildReplyTextEntryHelper(runtime);
249
-
250
- const enqueueReplyMediaEntries = (params: ReplyMediaEntriesParams) => {
251
- let first = true;
252
- const currentTime = runtime.now();
253
-
254
- for (const mediaUrl of params.payload.mediaList) {
255
- const normalizedText = normalizeMessageText(first ? params.payload.text : '');
256
- const fallback = runtime.tryBuildMediaDedupeFallback({
257
- sessionKey: params.sessionKey,
258
- mediaUrl,
259
- text: normalizedText,
260
- replyToId: params.payload.replyToId,
261
- currentTime,
262
- });
263
-
264
- enqueueSingleReplyMediaEntry(
265
- {
266
- params,
267
- mediaUrl,
268
- normalizedText,
269
- text: first ? params.payload.text : '',
270
- fallback,
271
- currentTime,
272
- },
273
- replyMediaEntryHelpers,
274
- );
275
-
276
- first = false;
277
- }
278
- };
279
-
280
- const enqueueFromReply = (params: {
151
+ const enqueueFromReply = async (params: {
281
152
  accountId: string;
282
153
  sessionKey: string;
283
154
  route: BncrRoute;
@@ -286,7 +157,7 @@ export function createBncrMediaOrchestratorsRuntimeGroup(runtime: {
286
157
  replyTargetPolicy?: OutboundReplyTargetPolicy;
287
158
  }) => {
288
159
  const { accountId, sessionKey, route, payload, mediaLocalRoots, replyTargetPolicy } = params;
289
- const normalized = normalizeReplyOrchestratorPayload({
160
+ const normalized = await normalizeReplyOrchestratorPayload({
290
161
  payload,
291
162
  asString: runtime.asString,
292
163
  replyTargetPolicy,
@@ -302,19 +173,19 @@ export function createBncrMediaOrchestratorsRuntimeGroup(runtime: {
302
173
  },
303
174
  {
304
175
  logEnqueueFromReply: runtime.logEnqueueFromReply,
305
- enqueueReplyMediaEntries,
306
- enqueueReplyTextEntry: replyTextEntryHelper,
176
+ enqueueOutbound: runtime.enqueueOutbound,
177
+ buildOutboxEntry: runtime.buildOutboxEntry,
178
+ tryBuildMediaDedupeFallback: runtime.tryBuildMediaDedupeFallback,
179
+ rememberRecentMediaSend: runtime.rememberRecentMediaSend,
180
+ logInfo: runtime.logInfo,
307
181
  },
308
182
  );
309
183
  };
310
184
 
311
- const replyMediaOrchestrator = {
312
- enqueueFromReply,
313
- enqueueReplyMediaEntries,
314
- };
315
-
316
185
  return {
317
186
  fileTransferOrchestrator,
318
- replyMediaOrchestrator,
187
+ replyMediaOrchestrator: {
188
+ enqueueFromReply,
189
+ },
319
190
  };
320
191
  }
@@ -67,12 +67,12 @@ function isDisplayTarget(
67
67
  return Boolean(value && typeof value === 'object');
68
68
  }
69
69
 
70
- export function normalizeBncrMessagingTarget(raw: string) {
70
+ function normalizeBncrMessagingTarget(raw: string) {
71
71
  const input = asSanitizedString(raw).trim();
72
72
  return input || undefined;
73
73
  }
74
74
 
75
- export function formatBncrMessagingTargetDisplay({ target }: BncrMessagingTargetDisplayInput) {
75
+ function formatBncrMessagingTargetDisplay({ target }: BncrMessagingTargetDisplayInput) {
76
76
  if (typeof target === 'string') {
77
77
  const parsed = parseExplicitTarget(asSanitizedString(target).trim());
78
78
  return parsed?.route ? formatBncrHumanDisplay(parsed.route) : asSanitizedString(target).trim();
@@ -183,7 +183,7 @@ export function createBncrMessagingSurface(getBridge: () => BncrMessagingRuntime
183
183
  };
184
184
  }
185
185
 
186
- export function createBncrMessagingTargetResolver(getBridge: () => BncrMessagingRuntimeBridge) {
186
+ function createBncrMessagingTargetResolver(getBridge: () => BncrMessagingRuntimeBridge) {
187
187
  return {
188
188
  looksLikeId: (raw: string, normalized?: string) => {
189
189
  return looksLikeBncrExplicitTarget(asSanitizedString(normalized || raw).trim());
@@ -3,12 +3,7 @@ import {
3
3
  resolveFileTransferFailureState,
4
4
  } from '../core/outbox-file-transfer-failure.ts';
5
5
  import { buildTextPushFailureArgs } from '../core/outbox-text-push-failure.ts';
6
- import {
7
- buildTextPushBroadcastPayload,
8
- buildTextPushOkArgs,
9
- buildTextPushRouteSelectArgs,
10
- buildTextPushSuccessArgs,
11
- } from '../core/outbox-text-push-success.ts';
6
+
12
7
  import type { BncrConnection, OutboxEntry } from '../core/types.ts';
13
8
  import {
14
9
  buildOutboxPushOkDebugInfo,
@@ -137,51 +132,6 @@ export function createBncrOutboxPush(runtime: BncrOutboxPushRuntime) {
137
132
  );
138
133
  };
139
134
 
140
- const pushTextSuccessPath = (args: {
141
- entry: OutboxEntry;
142
- owner: BncrConnection | null;
143
- connIds: Iterable<string>;
144
- recentInboundReachable: boolean;
145
- routeReason: string;
146
- ownerConnId?: string;
147
- }) => {
148
- runtime.gatewayBroadcastToConnIds(
149
- runtime.pushEvent,
150
- buildTextPushBroadcastPayload({
151
- payload: args.entry.payload,
152
- messageId: args.entry.messageId,
153
- }),
154
- new Set(args.connIds),
155
- );
156
- logOutboxRouteSelect(
157
- buildTextPushRouteSelectArgs({
158
- entry: args.entry,
159
- connIds: args.connIds,
160
- routeReason: args.routeReason,
161
- recentInboundReachable: args.recentInboundReachable,
162
- owner: args.owner,
163
- event: runtime.pushEvent,
164
- }),
165
- );
166
- runtime.recordOutboxPushSuccess(
167
- buildTextPushSuccessArgs({
168
- entry: args.entry,
169
- connIds: args.connIds,
170
- ownerConnId: args.ownerConnId,
171
- ownerClientId: args.ownerConnId ? args.owner?.clientId : undefined,
172
- }),
173
- );
174
- logOutboxPushOkSummary(args.entry.messageId);
175
- logOutboxPushOk(
176
- buildTextPushOkArgs({
177
- entry: args.entry,
178
- connIds: args.connIds,
179
- recentInboundReachable: args.recentInboundReachable,
180
- event: runtime.pushEvent,
181
- }),
182
- );
183
- };
184
-
185
135
  const handleTextPushFailure = (args: { entry: OutboxEntry; error: unknown }) => {
186
136
  runtime.recordOutboxPushFailure({
187
137
  entry: args.entry,
@@ -253,7 +203,6 @@ export function createBncrOutboxPush(runtime: BncrOutboxPushRuntime) {
253
203
  };
254
204
 
255
205
  return {
256
- pushTextSuccessPath,
257
206
  handleTextPushFailure,
258
207
  handleFileTransferPushFailure,
259
208
  handleFileTransferPushGuardFailure,
@@ -0,0 +1,307 @@
1
+ import {
2
+ buildFileTransferPushOkArgs,
3
+ buildFileTransferPushSuccessArgs,
4
+ } from '../core/outbox-file-transfer-bookkeeping.ts';
5
+ import { resolveFileTransferGuard } from '../core/outbox-file-transfer-guards.ts';
6
+ import {
7
+ prepareRouteSelection,
8
+ prepareRouteSelection as prepareTextPushRouteSelection,
9
+ } from '../core/outbox-file-transfer-prep.ts';
10
+ import {
11
+ buildFileTransferBroadcastPayload,
12
+ buildFileTransferRouteSelectArgs,
13
+ } from '../core/outbox-file-transfer-success.ts';
14
+ import { resolveTextPushGuard } from '../core/outbox-text-push-guards.ts';
15
+ import {
16
+ buildTextPushBroadcastPayload,
17
+ buildTextPushOkArgs,
18
+ buildTextPushRouteSelectArgs,
19
+ buildTextPushSuccessArgs,
20
+ } from '../core/outbox-text-push-success.ts';
21
+ import type { BncrConnection, BncrRoute, OutboxEntry } from '../core/types.ts';
22
+ import { isPlainObject } from '../core/value-sanitize.ts';
23
+ import {
24
+ selectOutboxFileTransferRouteCandidates,
25
+ selectOutboxRouteCandidates,
26
+ } from '../messaging/outbound/queue-selectors.ts';
27
+
28
+ type FileTransferGuardFailure = {
29
+ reason: 'media-url-missing' | 'no-gateway-context' | 'no-active-connection';
30
+ lastError: string;
31
+ recentInboundReachable?: boolean;
32
+ };
33
+
34
+ export type RunBncrOutboxPushArgs = {
35
+ entry: OutboxEntry;
36
+ gatewayContext: unknown;
37
+ owner: BncrConnection | null;
38
+ resolvePushConnIds: (accountId: string) => Set<string>;
39
+ resolveRecentInboundConnIds: (accountId: string) => Set<string>;
40
+ hasRecentInboundReachability: (accountId: string) => boolean;
41
+ isRevalidatedAttemptedConn: (connId: string) => boolean;
42
+ // Route + guard failure
43
+ recordOutboxPrePushFailure: (args: {
44
+ entry: OutboxEntry;
45
+ lastError: string;
46
+ persist?: boolean;
47
+ }) => void;
48
+ logOutboxPushSkip: (args: {
49
+ messageId: string;
50
+ accountId: string;
51
+ reason: string;
52
+ recentInboundReachable?: boolean;
53
+ routeReason?: string;
54
+ connIds?: Iterable<string>;
55
+ ownerConnId?: string;
56
+ ownerClientId?: string;
57
+ }) => void;
58
+ handleFileTransferPushGuardFailure: (args: {
59
+ entry: OutboxEntry;
60
+ guard: FileTransferGuardFailure & { ok: false };
61
+ }) => void;
62
+ // Broadcast + bookkeeping (unified)
63
+ pushEvent: string;
64
+ gatewayBroadcastToConnIds: (
65
+ event: string,
66
+ payload: unknown,
67
+ connIds: ReadonlySet<string>,
68
+ ) => void;
69
+ recordOutboxPushSuccess: (args: {
70
+ entry: OutboxEntry;
71
+ connIds: Iterable<string>;
72
+ ownerConnId?: string;
73
+ ownerClientId?: string;
74
+ }) => void;
75
+ logOutboxRouteSelect: (args: {
76
+ messageId: string;
77
+ accountId: string;
78
+ kind?: 'file-transfer';
79
+ routeReason: string;
80
+ connIds: Iterable<string>;
81
+ ownerConnId: string;
82
+ ownerClientId: string;
83
+ recentInboundReachable: boolean;
84
+ event: string;
85
+ }) => void;
86
+ logOutboxPushOkSummary: (messageId: string) => void;
87
+ logOutboundSummary: (entry: OutboxEntry) => void;
88
+ logOutboxPushOk: (args: {
89
+ messageId: string;
90
+ accountId: string;
91
+ kind?: 'file-transfer';
92
+ connIds: Iterable<string>;
93
+ ownerConnId: string;
94
+ ownerClientId: string;
95
+ recentInboundReachable: boolean;
96
+ event: string;
97
+ }) => void;
98
+ // Failure handlers
99
+ handleTextPushFailure: (args: { entry: OutboxEntry; error: unknown }) => void;
100
+ handleFileTransferPushFailure: (args: { entry: OutboxEntry; error: unknown }) => void;
101
+ // Media-specific (optional)
102
+ transferMediaToBncrClient?: (params: {
103
+ accountId: string;
104
+ sessionKey: string;
105
+ route: BncrRoute;
106
+ mediaUrl: string;
107
+ mediaLocalRoots?: readonly string[];
108
+ downloadMedia?: boolean;
109
+ }) => Promise<{
110
+ mode: 'base64' | 'chunk';
111
+ mimeType?: string;
112
+ fileName?: string;
113
+ base64?: string;
114
+ path?: string;
115
+ }>;
116
+ buildFileTransferOutboundFrame?: (params: {
117
+ entry: OutboxEntry;
118
+ msg: Record<string, unknown>;
119
+ media: { fileName?: string; mimeType?: string; path?: string; base64?: string; type?: string };
120
+ mediaUrl: string;
121
+ }) => Record<string, unknown>;
122
+ };
123
+
124
+ export async function runBncrOutboxPush(args: RunBncrOutboxPushArgs): Promise<boolean> {
125
+ const entry = args.entry;
126
+ const msg =
127
+ isPlainObject(entry.payload) &&
128
+ isPlainObject((entry.payload as Record<string, unknown>).message)
129
+ ? ((entry.payload as Record<string, unknown>).message as Record<string, unknown>)
130
+ : null;
131
+ const transferMode = msg?.transferMode === 'media' ? 'media' : 'text';
132
+
133
+ // Route selection
134
+ const selection =
135
+ transferMode === 'media'
136
+ ? prepareRouteSelection({
137
+ entry,
138
+ owner: args.owner,
139
+ resolvePushConnIds: args.resolvePushConnIds,
140
+ resolveRecentInboundConnIds: args.resolveRecentInboundConnIds,
141
+ hasRecentInboundReachability: args.hasRecentInboundReachability,
142
+ isRevalidatedAttemptedConn: args.isRevalidatedAttemptedConn,
143
+ selectRouteCandidates: selectOutboxFileTransferRouteCandidates,
144
+ })
145
+ : prepareTextPushRouteSelection({
146
+ entry,
147
+ owner: args.owner,
148
+ resolvePushConnIds: args.resolvePushConnIds,
149
+ resolveRecentInboundConnIds: args.resolveRecentInboundConnIds,
150
+ hasRecentInboundReachability: args.hasRecentInboundReachability,
151
+ isRevalidatedAttemptedConn: args.isRevalidatedAttemptedConn,
152
+ selectRouteCandidates: selectOutboxRouteCandidates,
153
+ });
154
+
155
+ // Guard + dispatch
156
+ if (transferMode === 'media') {
157
+ const guard = resolveFileTransferGuard({
158
+ gatewayContext: args.gatewayContext,
159
+ entry,
160
+ owner: args.owner,
161
+ routeSelection: selection,
162
+ mediaUrl: String(msg!.mediaUrl || '').trim(),
163
+ });
164
+ if (!guard.ok) {
165
+ args.handleFileTransferPushGuardFailure({
166
+ entry,
167
+ guard: guard as FileTransferGuardFailure & { ok: false },
168
+ });
169
+ return false;
170
+ }
171
+
172
+ // ── Media success path (inline) ──
173
+ try {
174
+ const media = await args.transferMediaToBncrClient!({
175
+ accountId: entry.accountId,
176
+ sessionKey: entry.sessionKey,
177
+ route: entry.route,
178
+ mediaUrl: guard.mediaUrl,
179
+ mediaLocalRoots: Array.isArray(msg!.mediaLocalRoots)
180
+ ? (msg!.mediaLocalRoots as readonly string[])
181
+ : undefined,
182
+ downloadMedia: msg!.downloadMedia as boolean | undefined,
183
+ });
184
+ const frame = args.buildFileTransferOutboundFrame!({
185
+ entry,
186
+ msg: msg!,
187
+ media,
188
+ mediaUrl: guard.mediaUrl,
189
+ });
190
+
191
+ args.gatewayBroadcastToConnIds(
192
+ args.pushEvent,
193
+ buildFileTransferBroadcastPayload({ frame, messageId: entry.messageId }),
194
+ new Set(guard.connIds),
195
+ );
196
+ args.logOutboxRouteSelect(
197
+ buildFileTransferRouteSelectArgs({
198
+ entry,
199
+ connIds: guard.connIds,
200
+ routeReason: guard.routeReason,
201
+ recentInboundReachable: guard.recentInboundReachable,
202
+ owner: args.owner,
203
+ event: args.pushEvent,
204
+ }),
205
+ );
206
+ args.recordOutboxPushSuccess(
207
+ buildFileTransferPushSuccessArgs({
208
+ entry,
209
+ connIds: guard.connIds,
210
+ owner: args.owner,
211
+ }),
212
+ );
213
+ args.logOutboxPushOkSummary(entry.messageId);
214
+ // Sync entry type from the resolved frame
215
+ const frameMsg = frame?.message as Record<string, unknown> | undefined;
216
+ if (frameMsg?.type !== undefined) {
217
+ (msg! as Record<string, unknown>).type = frameMsg.type;
218
+ }
219
+ args.logOutboundSummary(entry);
220
+ args.logOutboxPushOk(
221
+ buildFileTransferPushOkArgs({
222
+ entry,
223
+ connIds: guard.connIds,
224
+ recentInboundReachable: guard.recentInboundReachable,
225
+ event: args.pushEvent,
226
+ }),
227
+ );
228
+ return true;
229
+ } catch (error) {
230
+ args.handleFileTransferPushFailure({ entry, error });
231
+ return false;
232
+ }
233
+ }
234
+
235
+ // ── Text ──
236
+ const textGuard = resolveTextPushGuard({
237
+ gatewayContext: args.gatewayContext,
238
+ entry,
239
+ routeSelection: selection,
240
+ });
241
+ if (!textGuard.ok) {
242
+ args.recordOutboxPrePushFailure({
243
+ entry,
244
+ lastError:
245
+ textGuard.reason === 'no-gateway-context'
246
+ ? 'gateway context unavailable'
247
+ : 'no active bncr client',
248
+ persist: true,
249
+ });
250
+ args.logOutboxPushSkip({
251
+ messageId: entry.messageId,
252
+ accountId: entry.accountId,
253
+ reason: textGuard.reason,
254
+ recentInboundReachable:
255
+ textGuard.reason === 'no-active-connection' ? textGuard.recentInboundReachable : undefined,
256
+ routeReason: selection.routeReason,
257
+ connIds: selection.connIds,
258
+ ownerConnId: selection.ownerConnId,
259
+ ownerClientId: args.owner?.clientId,
260
+ });
261
+ return false;
262
+ }
263
+
264
+ // ── Text success path (inline) ──
265
+ try {
266
+ args.gatewayBroadcastToConnIds(
267
+ args.pushEvent,
268
+ buildTextPushBroadcastPayload({
269
+ payload: entry.payload,
270
+ messageId: entry.messageId,
271
+ }),
272
+ new Set(textGuard.connIds),
273
+ );
274
+ args.logOutboxRouteSelect(
275
+ buildTextPushRouteSelectArgs({
276
+ entry,
277
+ connIds: textGuard.connIds,
278
+ routeReason: textGuard.routeReason,
279
+ recentInboundReachable: textGuard.recentInboundReachable,
280
+ owner: args.owner,
281
+ event: args.pushEvent,
282
+ }),
283
+ );
284
+ args.recordOutboxPushSuccess(
285
+ buildTextPushSuccessArgs({
286
+ entry,
287
+ connIds: textGuard.connIds,
288
+ ownerConnId: textGuard.ownerConnId,
289
+ ownerClientId: textGuard.ownerConnId ? args.owner?.clientId : undefined,
290
+ }),
291
+ );
292
+ args.logOutboxPushOkSummary(entry.messageId);
293
+ args.logOutboundSummary(entry);
294
+ args.logOutboxPushOk(
295
+ buildTextPushOkArgs({
296
+ entry,
297
+ connIds: textGuard.connIds,
298
+ recentInboundReachable: textGuard.recentInboundReachable,
299
+ event: args.pushEvent,
300
+ }),
301
+ );
302
+ return true;
303
+ } catch (error) {
304
+ args.handleTextPushFailure({ entry, error });
305
+ return false;
306
+ }
307
+ }
@@ -36,7 +36,7 @@ export function buildSceneKey(parsed: ParsedInbound): string {
36
36
  : `${parsed.platform}:${parsed.userId}`;
37
37
  }
38
38
 
39
- export function buildSceneRecord(args: {
39
+ function buildSceneRecord(args: {
40
40
  parsed: ParsedInbound;
41
41
  sceneKey: string;
42
42
  kind: BncrSceneKind;