@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xmoxmo/bncr",
3
- "version": "0.5.7",
3
+ "version": "0.5.8",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -149,11 +149,9 @@ const requiredSourceFiles = [
149
149
  'src/plugin/outbox-drain-post-push.ts',
150
150
  'src/plugin/outbox-drain-runtime.ts',
151
151
  'src/plugin/outbox-drain-schedule.ts',
152
- 'src/plugin/outbox-file-push-flow.ts',
153
152
  'src/plugin/outbox-push-route-runtime-group.ts',
154
153
  'src/plugin/outbox-push.ts',
155
154
  'src/plugin/outbox-route.ts',
156
- 'src/plugin/outbox-text-push-flow.ts',
157
155
  'src/plugin/runtime-diagnostics-snapshot.ts',
158
156
  'src/plugin/setup.ts',
159
157
  'src/plugin/state-transient-runtime-group.ts',
@@ -44,7 +44,6 @@ const requiredSourceFiles = [
44
44
  'src/core/outbox-summary.ts',
45
45
  'src/core/outbox-text-push-failure.ts',
46
46
  'src/core/outbox-text-push-guards.ts',
47
- 'src/core/outbox-text-push-prep.ts',
48
47
  'src/core/outbox-text-push-success.ts',
49
48
  'src/core/permissions.ts',
50
49
  'src/core/persisted-outbox-entry.ts',
@@ -145,11 +144,9 @@ const requiredSourceFiles = [
145
144
  'src/plugin/outbox-drain-post-push.ts',
146
145
  'src/plugin/outbox-drain-runtime.ts',
147
146
  'src/plugin/outbox-drain-schedule.ts',
148
- 'src/plugin/outbox-file-push-flow.ts',
149
147
  'src/plugin/outbox-push-route-runtime-group.ts',
150
148
  'src/plugin/outbox-push.ts',
151
149
  'src/plugin/outbox-route.ts',
152
- 'src/plugin/outbox-text-push-flow.ts',
153
150
  'src/plugin/runtime-diagnostics-snapshot.ts',
154
151
  'src/plugin/setup.ts',
155
152
  'src/plugin/state-transient-runtime-group.ts',
@@ -44,7 +44,7 @@ export const pluginRoot = resolveBncrPluginRoot(pluginFile);
44
44
  const runtimeSourceDir = resolveBncrRuntimeSourceDir(pluginDir);
45
45
  let runtime: LoadedRuntime | null = null;
46
46
 
47
- export const readPluginVersion = (rootDir = pluginRoot) => {
47
+ const readPluginVersion = (rootDir = pluginRoot) => {
48
48
  try {
49
49
  const raw = fs.readFileSync(path.join(rootDir, 'package.json'), 'utf8');
50
50
  const parsed = JSON.parse(raw);
package/src/channel.ts CHANGED
@@ -19,10 +19,7 @@ import {
19
19
  emitBncrLog,
20
20
  summarizeBncrTextPreview,
21
21
  } from './core/logging.ts';
22
- import {
23
- buildFileTransferOutboxEntry as buildFileTransferOutboxEntryFromRuntime,
24
- buildTextOutboxEntry as buildTextOutboxEntryFromRuntime,
25
- } from './core/outbox-entry-builders.ts';
22
+ import { buildOutboxEntry as buildOutboxEntryFromRuntime } from './core/outbox-entry-builders.ts';
26
23
  import { summarizeOutboxEntry } from './core/outbox-summary.ts';
27
24
  import { normalizePersistedOutboxEntry as normalizePersistedOutboxEntryFromRuntime } from './core/persisted-outbox-entry.ts';
28
25
  import {
@@ -53,7 +50,6 @@ import { OUTBOUND_FLUSH_REASON, OUTBOUND_FLUSH_TRIGGER } from './messaging/outbo
53
50
  import type {
54
51
  NormalizedReplyPayload,
55
52
  OutboundReplyTargetPolicy,
56
- ReplyMediaEntriesParams,
57
53
  ReplyPayloadInput,
58
54
  } from './messaging/outbound/reply-enqueue.ts';
59
55
  import type { OpenClawChannelRuntimeApiHolder } from './openclaw/channel-runtime-contracts.ts';
@@ -67,7 +63,6 @@ import { createBncrBridgeConnectionFacade } from './plugin/bridge-connection-fac
67
63
  import { createBncrBridgeDiagnosticsFacade } from './plugin/bridge-diagnostics-facade.ts';
68
64
  import { createBncrBridgeDrainFacade } from './plugin/bridge-drain-facade.ts';
69
65
  import { createBncrBridgeExtendedDiagnosticsFacade } from './plugin/bridge-extended-diagnostics-facade.ts';
70
- import { createBncrBridgeFileTransferPushFacade } from './plugin/bridge-file-transfer-push-facade.ts';
71
66
  import {
72
67
  cleanupBncrBridgeRuntime,
73
68
  shutdownBncrBridgeService,
@@ -175,9 +170,9 @@ import { createBncrInboundSurfaceHandlersGroup } from './plugin/inbound-surface-
175
170
  import { createBncrMediaOrchestratorsRuntimeGroup } from './plugin/media-orchestrators-runtime-group.ts';
176
171
  import { BNCR_CHANNEL_META } from './plugin/meta.ts';
177
172
  import type { BncrOutboxAckOkTelemetryPatch } from './plugin/outbox-ack-outcome.ts';
178
- import { runBncrFileTransferOutboxPush } from './plugin/outbox-file-push-flow.ts';
173
+
179
174
  import { createBncrOutboxPushRouteRuntimeGroup } from './plugin/outbox-push-route-runtime-group.ts';
180
- import { runBncrTextOutboxPush } from './plugin/outbox-text-push-flow.ts';
175
+ import { runBncrOutboxPush } from './plugin/outbox-unified-push-flow.ts';
181
176
  import {
182
177
  createBncrDeadLetterDiagnosticsHelpers,
183
178
  createBncrDiagnosticsSelectionHelpers,
@@ -1131,83 +1126,53 @@ class BncrBridgeRuntime {
1131
1126
  // This block prepares media, chooses transfer mode, sends chunk/complete
1132
1127
  // frames, and feeds successful media pushes back into the shared outbox path.
1133
1128
 
1134
- private async pushFileTransferSuccessPath(args: {
1135
- entry: OutboxEntry;
1136
- msg: Record<string, unknown>;
1137
- owner: ReturnType<BncrBridgeRuntime['resolveOutboxPushOwner']>;
1138
- connIds: Iterable<string>;
1139
- recentInboundReachable: boolean;
1140
- routeReason: string;
1141
- mediaUrl: string;
1142
- }): Promise<void> {
1143
- await this.fileTransferPushFacade.pushFileTransferSuccessPath(args);
1144
- }
1145
-
1146
1129
  private handleFileTransferPushFailure(args: { entry: OutboxEntry; error: unknown }) {
1147
- this.fileTransferPushFacade.handleFileTransferPushFailure(args);
1130
+ this.outboxPush.handleFileTransferPushFailure(args);
1148
1131
  }
1149
1132
 
1150
1133
  private handleFileTransferPushGuardFailure(
1151
- args: Parameters<typeof this.fileTransferPushFacade.handleFileTransferPushGuardFailure>[0],
1134
+ args: Parameters<typeof this.outboxPush.handleFileTransferPushGuardFailure>[0],
1152
1135
  ) {
1153
- this.fileTransferPushFacade.handleFileTransferPushGuardFailure(args);
1136
+ this.outboxPush.handleFileTransferPushGuardFailure(args);
1154
1137
  }
1155
1138
 
1156
1139
  // File-transfer outbound entry builders / dedupe -------------------------
1157
1140
 
1158
- private async tryPushFileTransferEntry(
1159
- entry: OutboxEntry,
1160
- msg: Record<string, unknown>,
1161
- ): Promise<boolean> {
1162
- return await runBncrFileTransferOutboxPush({
1163
- entry,
1164
- msg,
1165
- gatewayContext: this.gatewayContext,
1166
- owner: this.resolveOutboxPushOwner(entry.accountId),
1167
- resolvePushConnIds: (accountId) => this.resolvePushConnIds(accountId),
1168
- resolveRecentInboundConnIds: (accountId) => this.resolveRecentInboundConnIds(accountId),
1169
- hasRecentInboundReachability: (accountId) => this.hasRecentInboundReachability(accountId),
1170
- isRevalidatedAttemptedConn: (connId) => this.isRevalidatedAttemptedConn(entry, connId),
1171
- handleFileTransferPushGuardFailure: (flowArgs) =>
1172
- this.handleFileTransferPushGuardFailure(flowArgs),
1173
- pushFileTransferSuccessPath: (flowArgs) => this.pushFileTransferSuccessPath(flowArgs),
1174
- handleFileTransferPushFailure: (flowArgs) => this.handleFileTransferPushFailure(flowArgs),
1175
- });
1176
- }
1177
-
1178
- private buildFileTransferOutboxEntry(params: {
1141
+ private buildOutboxEntry(params: {
1179
1142
  accountId: string;
1180
1143
  sessionKey: string;
1181
1144
  route: BncrRoute;
1182
- mediaUrl: string;
1183
- mediaLocalRoots?: readonly string[];
1184
- text?: string;
1185
- asVoice?: boolean;
1186
- audioAsVoice?: boolean;
1187
1145
  type?: string;
1188
- extra?: Record<string, unknown>;
1146
+ msg: string;
1189
1147
  kind?: 'tool' | 'block' | 'final';
1190
1148
  replyToId?: string;
1191
1149
  replyTargetPolicy?: OutboundReplyTargetPolicy;
1150
+ extra?: Record<string, unknown>;
1151
+ transferMode?: 'text' | 'media';
1152
+ mediaUrl?: string;
1153
+ mediaLocalRoots?: readonly string[];
1154
+ asVoice?: boolean;
1155
+ audioAsVoice?: boolean;
1192
1156
  downloadMedia?: boolean;
1193
1157
  }): OutboxEntry {
1194
- return buildFileTransferOutboxEntryFromRuntime({
1158
+ return buildOutboxEntryFromRuntime({
1195
1159
  createMessageId: () => randomUUID(),
1196
1160
  now,
1197
1161
  normalizeAccountId,
1198
1162
  accountId: params.accountId,
1199
1163
  sessionKey: params.sessionKey,
1200
1164
  route: params.route,
1201
- mediaUrl: params.mediaUrl,
1202
- mediaLocalRoots: params.mediaLocalRoots,
1203
- text: asString(params.text || ''),
1204
- asVoice: params.asVoice,
1205
- audioAsVoice: params.audioAsVoice,
1206
1165
  type: params.type,
1207
- extra: params.extra,
1166
+ msg: params.msg,
1208
1167
  kind: params.kind,
1209
1168
  replyToId: asString(params.replyToId || '').trim() || undefined,
1210
1169
  replyTargetPolicy: params.replyTargetPolicy,
1170
+ extra: params.extra,
1171
+ transferMode: params.transferMode,
1172
+ mediaUrl: params.mediaUrl,
1173
+ mediaLocalRoots: params.mediaLocalRoots,
1174
+ asVoice: params.asVoice,
1175
+ audioAsVoice: params.audioAsVoice,
1211
1176
  downloadMedia: params.downloadMedia,
1212
1177
  });
1213
1178
  }
@@ -1321,66 +1286,8 @@ class BncrBridgeRuntime {
1321
1286
  // This is the core outbound state machine: build entries, enqueue them,
1322
1287
  // push them, observe ACK outcomes, and terminate in retry or dead-letter.
1323
1288
 
1324
- private buildTextOutboxEntry(params: {
1325
- accountId: string;
1326
- sessionKey: string;
1327
- route: BncrRoute;
1328
- text: string;
1329
- extra?: Record<string, unknown>;
1330
- kind?: 'tool' | 'block' | 'final';
1331
- replyToId?: string;
1332
- replyTargetPolicy?: OutboundReplyTargetPolicy;
1333
- }): OutboxEntry {
1334
- return buildTextOutboxEntryFromRuntime({
1335
- createMessageId: () => randomUUID(),
1336
- now,
1337
- normalizeAccountId,
1338
- accountId: params.accountId,
1339
- sessionKey: params.sessionKey,
1340
- route: params.route,
1341
- text: params.text,
1342
- extra: params.extra && Object.keys(params.extra).length > 0 ? { ...params.extra } : undefined,
1343
- kind: params.kind,
1344
- replyToId: params.replyToId,
1345
- replyTargetPolicy: params.replyTargetPolicy,
1346
- });
1347
- }
1348
-
1349
1289
  private async tryPushEntry(entry: OutboxEntry): Promise<boolean> {
1350
- const msg =
1351
- isPlainObject(entry.payload) &&
1352
- isPlainObject((entry.payload as Record<string, unknown>).message)
1353
- ? ((entry.payload as Record<string, unknown>).message as Record<string, unknown>)
1354
- : null;
1355
- if (msg?.transferMode === 'media') {
1356
- return this.tryPushFileTransferEntry(entry, msg);
1357
- }
1358
- return this.tryPushTextEntry(entry);
1359
- }
1360
-
1361
- private pushTextSuccessPath(args: {
1362
- entry: OutboxEntry;
1363
- owner: ReturnType<BncrBridgeRuntime['resolveOutboxPushOwner']>;
1364
- connIds: Iterable<string>;
1365
- recentInboundReachable: boolean;
1366
- routeReason: string;
1367
- ownerConnId?: string;
1368
- }) {
1369
- this.outboxPush.pushTextSuccessPath(args);
1370
- }
1371
-
1372
- private handleTextPushFailure(args: { entry: OutboxEntry; error: unknown }) {
1373
- this.outboxPush.handleTextPushFailure(args);
1374
- }
1375
-
1376
- // Text outbound push runtime bridge --------------------------------------
1377
-
1378
- // Outbound diagnostics/logging helpers -----------------------------------
1379
- // Keep the compact logging helpers adjacent to the outbound state machine so
1380
- // queue transitions and emitted diagnostics remain easy to correlate.
1381
-
1382
- private async tryPushTextEntry(entry: OutboxEntry): Promise<boolean> {
1383
- return await runBncrTextOutboxPush({
1290
+ return runBncrOutboxPush({
1384
1291
  entry,
1385
1292
  gatewayContext: this.gatewayContext,
1386
1293
  owner: this.resolveOutboxPushOwner(entry.accountId),
@@ -1390,11 +1297,34 @@ class BncrBridgeRuntime {
1390
1297
  isRevalidatedAttemptedConn: (connId) => this.isRevalidatedAttemptedConn(entry, connId),
1391
1298
  recordOutboxPrePushFailure: (flowArgs) => this.recordOutboxPrePushFailure(flowArgs),
1392
1299
  logOutboxPushSkip: (flowArgs) => this.logOutboxPushSkip(flowArgs),
1393
- pushTextSuccessPath: (flowArgs) => this.pushTextSuccessPath(flowArgs),
1300
+ handleFileTransferPushGuardFailure: (flowArgs) =>
1301
+ this.handleFileTransferPushGuardFailure(flowArgs),
1394
1302
  handleTextPushFailure: (flowArgs) => this.handleTextPushFailure(flowArgs),
1303
+ handleFileTransferPushFailure: (flowArgs) => this.handleFileTransferPushFailure(flowArgs),
1304
+ // Unified push deps
1305
+ pushEvent: BNCR_PUSH_EVENT,
1306
+ gatewayBroadcastToConnIds: (event, payload, connIds) =>
1307
+ this.gatewayContext?.broadcastToConnIds(event, payload, connIds),
1308
+ recordOutboxPushSuccess: (args) => this.recordOutboxPushSuccess(args),
1309
+ logOutboxRouteSelect: (args) => this.logOutboxRouteSelect(args),
1310
+ logOutboxPushOkSummary: (mid) => this.logOutboxPushOkSummary(mid),
1311
+ logOutboundSummary: (entry) => this.logOutboundSummary(entry),
1312
+ logOutboxPushOk: (args) => this.logOutboxPushOk(args),
1313
+ transferMediaToBncrClient: (params) => this.transferMediaToBncrClient(params),
1314
+ buildFileTransferOutboundFrame: (params) => this.buildFileTransferOutboundFrame(params),
1395
1315
  });
1396
1316
  }
1397
1317
 
1318
+ private handleTextPushFailure(args: { entry: OutboxEntry; error: unknown }) {
1319
+ this.outboxPush.handleTextPushFailure(args);
1320
+ }
1321
+
1322
+ // Text outbound push runtime bridge --------------------------------------
1323
+
1324
+ // Outbound diagnostics/logging helpers -----------------------------------
1325
+ // Keep the compact logging helpers adjacent to the outbound state machine so
1326
+ // queue transitions and emitted diagnostics remain easy to correlate.
1327
+
1398
1328
  private logOutboxPushSkip(args: {
1399
1329
  messageId: string;
1400
1330
  accountId: string;
@@ -2734,8 +2664,7 @@ class BncrBridgeRuntime {
2734
2664
  logInfo: (scope, message, options) => this.logInfo(scope, message, options),
2735
2665
  logEnqueueFromReply: (args) => this.logEnqueueFromReply(args),
2736
2666
  enqueueOutbound: (entry) => this.enqueueOutbound(entry),
2737
- buildTextOutboxEntry: (args) => this.buildTextOutboxEntry(args),
2738
- buildFileTransferOutboxEntry: (args) => this.buildFileTransferOutboxEntry(args),
2667
+ buildOutboxEntry: (args) => this.buildOutboxEntry(args),
2739
2668
  rememberRecentMediaSend: (args) => this.rememberRecentMediaSend(args),
2740
2669
  tryBuildMediaDedupeFallback: (args) => this.tryBuildMediaDedupeFallback(args),
2741
2670
  });
@@ -2789,7 +2718,7 @@ class BncrBridgeRuntime {
2789
2718
  mediaLocalRoots?: readonly string[];
2790
2719
  replyTargetPolicy?: OutboundReplyTargetPolicy;
2791
2720
  }) {
2792
- this.bridgeMediaFacade.enqueueFromReply(params);
2721
+ return this.bridgeMediaFacade.enqueueFromReply(params);
2793
2722
  }
2794
2723
 
2795
2724
  private logEnqueueFromReply(args: {
@@ -2829,24 +2758,6 @@ class BncrBridgeRuntime {
2829
2758
  normalizeAccountId,
2830
2759
  });
2831
2760
 
2832
- private readonly fileTransferPushFacade = createBncrBridgeFileTransferPushFacade({
2833
- pushEvent: BNCR_PUSH_EVENT,
2834
- getGatewayContext: () => this.gatewayContext,
2835
- transferMediaToBncrClient: (params) => this.transferMediaToBncrClient(params),
2836
- buildFileTransferOutboundFrame: (params) => this.buildFileTransferOutboundFrame(params),
2837
- logOutboxRouteSelect: (args) => this.logOutboxRouteSelect(args),
2838
- recordOutboxPushSuccess: (args) => this.recordOutboxPushSuccess(args),
2839
- logOutboxPushOkSummary: (messageId) => this.logOutboxPushOkSummary(messageId),
2840
- logOutboxPushOk: (args) => this.logOutboxPushOk(args),
2841
- handleFileTransferPushFailure: (args) => this.outboxPush.handleFileTransferPushFailure(args),
2842
- handleFileTransferPushGuardFailure: (args) =>
2843
- this.outboxPush.handleFileTransferPushGuardFailure(args),
2844
- });
2845
-
2846
- private enqueueReplyMediaEntries(params: ReplyMediaEntriesParams) {
2847
- this.bridgeMediaFacade.enqueueReplyMediaEntries(params);
2848
- }
2849
-
2850
2761
  // Surface runtime assembly -----------------------------------------------
2851
2762
  // The bridge remains the ownership root, but these builders keep the final
2852
2763
  // gateway/inbound/channel send wiring readable by concern instead of as one
@@ -141,7 +141,7 @@ function scoreOutboundPushConnection(args: {
141
141
  };
142
142
  }
143
143
 
144
- export function compareOutboundPushConnections(args: {
144
+ function compareOutboundPushConnections(args: {
145
145
  a: BncrConnection;
146
146
  b: BncrConnection;
147
147
  now: number;
@@ -117,7 +117,7 @@ function sanitizeDebugValue(key: string, value: unknown): unknown {
117
117
  return value;
118
118
  }
119
119
 
120
- export function sanitizeBncrDebugPayload(payload: Record<string, unknown>) {
120
+ function sanitizeBncrDebugPayload(payload: Record<string, unknown>) {
121
121
  return sanitizeDebugPayload(payload);
122
122
  }
123
123
 
@@ -12,27 +12,67 @@ function sanitizeExtraSpread(extra: Record<string, unknown>): Record<string, unk
12
12
  return out;
13
13
  }
14
14
 
15
- export function buildFileTransferOutboxEntry(args: {
15
+ export type BuildOutboxEntryParams = {
16
16
  createMessageId: () => string;
17
17
  now: () => number;
18
18
  normalizeAccountId: (accountId?: string | null) => string;
19
19
  accountId: string;
20
20
  sessionKey: string;
21
21
  route: BncrRoute;
22
- mediaUrl: string;
23
- mediaLocalRoots?: readonly string[];
24
- text: string;
25
- asVoice?: boolean;
26
- audioAsVoice?: boolean;
22
+ /** For text sends this is 'text'; for media it may be undefined (inferred downstream). */
27
23
  type?: string;
28
- extra?: Record<string, unknown>;
24
+ /** Message body (text content for text sends, caption for media sends). */
25
+ msg: string;
29
26
  kind?: 'tool' | 'block' | 'final';
30
27
  replyToId?: string;
31
28
  replyTargetPolicy?: OutboundReplyTargetPolicy;
29
+ extra?: Record<string, unknown>;
30
+ /** Media-only fields. When transferMode === 'media' these shape the frame. */
31
+ transferMode?: 'text' | 'media';
32
+ mediaUrl?: string;
33
+ mediaLocalRoots?: readonly string[];
34
+ asVoice?: boolean;
35
+ audioAsVoice?: boolean;
32
36
  downloadMedia?: boolean;
33
- }): OutboxEntry {
37
+ };
38
+
39
+ /**
40
+ * Unified outbox entry builder.
41
+ *
42
+ * Text and media sends share the same outbox lifecycle (queue, drain, retry).
43
+ * The only structural difference is that media entries carry extra media fields
44
+ * and set `transferMode: 'media'` so the push layer knows to process media.
45
+ *
46
+ * - **Text**: `{ type: 'text', transferMode: undefined, no media fields }`
47
+ * - **Media**: `{ type: args.type || undefined, transferMode: 'media', ...media fields }`
48
+ */
49
+ export function buildOutboxEntry(args: BuildOutboxEntryParams): OutboxEntry {
34
50
  const messageId = args.createMessageId();
35
51
  const createdAt = args.now();
52
+ const isMedia = args.transferMode === 'media';
53
+
54
+ const message: Record<string, unknown> = {
55
+ platform: args.route.platform,
56
+ groupId: args.route.groupId,
57
+ userId: args.route.userId,
58
+ type: isMedia ? args.type : 'text',
59
+ kind: args.kind,
60
+ msg: args.msg,
61
+ path: '',
62
+ base64: '',
63
+ fileName: '',
64
+ ...(args.extra ? sanitizeExtraSpread(args.extra) : {}),
65
+ };
66
+
67
+ if (isMedia) {
68
+ message.mediaUrl = args.mediaUrl || '';
69
+ if (args.mediaLocalRoots) message.mediaLocalRoots = Array.from(args.mediaLocalRoots);
70
+ message.asVoice = args.asVoice === true;
71
+ message.audioAsVoice = args.audioAsVoice === true;
72
+ message.downloadMedia = args.downloadMedia;
73
+ message.transferMode = 'media';
74
+ }
75
+
36
76
  return {
37
77
  messageId,
38
78
  accountId: args.normalizeAccountId(args.accountId),
@@ -49,84 +89,22 @@ export function buildFileTransferOutboxEntry(args: {
49
89
  replyToId: args.replyToId,
50
90
  replyTargetPolicy: args.replyTargetPolicy,
51
91
  }) || undefined,
52
- message: {
53
- platform: args.route.platform,
54
- groupId: args.route.groupId,
55
- userId: args.route.userId,
56
- type: args.type,
57
- // No fallback: when type is not provided the downstream adapter
58
- // (tgBot / GewePlus) infers from mediaUrl extension.
59
- kind: args.kind,
60
- msg: args.text,
61
- mediaUrl: args.mediaUrl,
62
- mediaLocalRoots: args.mediaLocalRoots ? Array.from(args.mediaLocalRoots) : undefined,
63
- asVoice: args.asVoice === true,
64
- audioAsVoice: args.audioAsVoice === true,
65
- downloadMedia: args.downloadMedia,
66
- transferMode: 'media',
67
- path: '',
68
- base64: '',
69
- fileName: '',
70
- ...(args.extra ? sanitizeExtraSpread(args.extra) : {}),
92
+ message: message as {
93
+ platform: string;
94
+ groupId: string;
95
+ userId: string;
96
+ type?: string;
97
+ kind?: 'tool' | 'block' | 'final';
98
+ msg: string;
99
+ path: string;
100
+ base64: string;
101
+ fileName: string;
102
+ [key: string]: unknown;
71
103
  },
104
+ ts: createdAt,
72
105
  },
73
106
  createdAt,
74
107
  retryCount: 0,
75
108
  nextAttemptAt: createdAt,
76
109
  };
77
110
  }
78
-
79
- export function buildTextOutboxEntry(args: {
80
- createMessageId: () => string;
81
- now: () => number;
82
- normalizeAccountId: (accountId?: string | null) => string;
83
- accountId: string;
84
- extra?: Record<string, unknown>;
85
- sessionKey: string;
86
- route: BncrRoute;
87
- text: string;
88
- kind?: 'tool' | 'block' | 'final';
89
- replyToId?: string;
90
- replyTargetPolicy?: OutboundReplyTargetPolicy;
91
- }): OutboxEntry {
92
- const messageId = args.createMessageId();
93
- const createdAt = args.now();
94
- const frame = {
95
- type: 'message.outbound',
96
- messageId,
97
- idempotencyKey: messageId,
98
- sessionKey: args.sessionKey,
99
- replyToId:
100
- normalizeOutboundReplyToId({
101
- kind: args.kind,
102
- replyToId: args.replyToId,
103
- replyTargetPolicy: args.replyTargetPolicy,
104
- }) || undefined,
105
- message: {
106
- platform: args.route.platform,
107
- groupId: args.route.groupId,
108
- userId: args.route.userId,
109
- type: 'text',
110
- kind: args.kind,
111
- msg: args.text,
112
- path: '',
113
- base64: '',
114
- fileName: '',
115
- // Extra fields from marker/pipeline spread for adapter-specific data
116
- // (e.g. type="appmsg", msg="<appmsg>..." from [BncrParam:...])
117
- ...(args.extra ? sanitizeExtraSpread(args.extra) : {}),
118
- },
119
- ts: createdAt,
120
- };
121
-
122
- return {
123
- messageId,
124
- accountId: args.normalizeAccountId(args.accountId),
125
- sessionKey: args.sessionKey,
126
- route: args.route,
127
- payload: frame,
128
- createdAt,
129
- retryCount: 0,
130
- nextAttemptAt: createdAt,
131
- };
132
- }
@@ -1,26 +1,6 @@
1
1
  import type { OutboxEntry } from './types.ts';
2
2
 
3
- type OutboxSummaryMessage = {
4
- type?: unknown;
5
- msg?: unknown;
6
- mediaUrl?: unknown;
7
- audioAsVoice?: boolean;
8
- asVoice?: boolean;
9
- };
10
-
11
- type OutboxSummaryPayload = {
12
- type?: unknown;
13
- message?: OutboxSummaryMessage;
14
- };
15
-
16
- function inferMediaTypeFromUrl(raw: string): 'image' | 'video' | 'audio' | 'file' {
17
- const clean = String(raw || '').split(/[?#]/, 1)[0] || '';
18
- const ext = clean.includes('.') ? clean.slice(clean.lastIndexOf('.') + 1).toLowerCase() : '';
19
- if (['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'heic', 'heif'].includes(ext)) return 'image';
20
- if (['mp4', 'mov', 'mkv', 'webm', 'avi', 'm4v'].includes(ext)) return 'video';
21
- if (['mp3', 'wav', 'ogg', 'oga', 'opus', 'm4a', 'aac', 'flac'].includes(ext)) return 'audio';
22
- return 'file';
23
- }
3
+ type OutboxSummaryMessage = { type?: unknown; msg?: unknown; mediaUrl?: unknown };
24
4
 
25
5
  function filenameFromUrl(raw: string): string {
26
6
  const clean = String(raw || '').split(/[?#]/, 1)[0] || '';
@@ -33,27 +13,13 @@ function filenameFromUrl(raw: string): string {
33
13
  }
34
14
  }
35
15
 
36
- function summarizeOutboxType(
37
- payload: OutboxSummaryPayload,
38
- msg: OutboxSummaryMessage,
39
- asString: (value: unknown) => string,
40
- ) {
41
- const directType = asString(msg.type || '');
42
- if (directType) return directType;
43
- // Infer type from mediaUrl when no direct type is set
44
- const mediaUrl = asString(msg.mediaUrl || '');
45
- if (mediaUrl) {
46
- if (msg.audioAsVoice === true || msg.asVoice === true) return 'voice';
47
- const inferred = inferMediaTypeFromUrl(mediaUrl);
48
- if (inferred) return inferred;
49
- }
50
- return asString(payload?.type || 'unknown');
16
+ function summarizeOutboxType(msg: OutboxSummaryMessage, asString: (value: unknown) => string) {
17
+ return asString(msg.type || '') || 'unknown';
51
18
  }
52
19
 
53
20
  function summarizeOutboxText(msg: OutboxSummaryMessage, asString: (value: unknown) => string) {
54
21
  const text = asString(msg.msg || '').trim();
55
22
  if (text) return text;
56
- // Fall back to filename from mediaUrl when no caption is set
57
23
  const mediaUrl = asString(msg.mediaUrl || '');
58
24
  if (mediaUrl) return filenameFromUrl(mediaUrl);
59
25
  return '';
@@ -65,9 +31,9 @@ export function summarizeOutboxEntry(args: {
65
31
  formatDisplayScope: (route: OutboxEntry['route']) => string;
66
32
  summarizeTextPreview: (raw: string, limit?: number) => string;
67
33
  }) {
68
- const payload = args.entry.payload as OutboxSummaryPayload;
34
+ const payload = args.entry.payload as { message?: OutboxSummaryMessage } | undefined;
69
35
  const msg = payload?.message || {};
70
- const type = summarizeOutboxType(payload, msg, args.asString);
36
+ const type = summarizeOutboxType(msg, args.asString);
71
37
  const text = summarizeOutboxText(msg, args.asString);
72
38
  const preview = args.summarizeTextPreview(text);
73
39
  return [type, args.formatDisplayScope(args.entry.route), preview].join('|');
@@ -10,7 +10,7 @@ type BncrElevatedConfigRoot = {
10
10
  };
11
11
  };
12
12
  };
13
- export function getBncrElevatedConfig(rootCfg: BncrElevatedConfigRoot | null | undefined) {
13
+ function getBncrElevatedConfig(rootCfg: BncrElevatedConfigRoot | null | undefined) {
14
14
  const elevated = rootCfg?.tools?.elevated || {};
15
15
  const allowFrom = elevated?.allowFrom || {};
16
16
  const bncrRules = Array.isArray(allowFrom?.bncr)
@@ -43,7 +43,7 @@ export type RegisterDriftSnapshot = {
43
43
  traceRecent: Array<Record<string, unknown>>;
44
44
  };
45
45
 
46
- export function classifyRegisterTrace(stack: string) {
46
+ function classifyRegisterTrace(stack: string) {
47
47
  if (
48
48
  stack.includes('prepareSecretsRuntimeSnapshot') ||
49
49
  stack.includes('resolveRuntimeWebTools') ||
@@ -66,7 +66,7 @@ export function classifyRegisterTrace(stack: string) {
66
66
  return 'other';
67
67
  }
68
68
 
69
- export function dominantRegisterBucket(sourceBuckets: Record<string, number>) {
69
+ function dominantRegisterBucket(sourceBuckets: Record<string, number>) {
70
70
  let winner: string | null = null;
71
71
  let winnerCount = -1;
72
72
  for (const [bucket, count] of Object.entries(sourceBuckets)) {
@@ -58,7 +58,7 @@ function asInboundMediaItems(params: BncrInboundParamsInput): BncrInboundMediaIt
58
58
  }));
59
59
  }
60
60
 
61
- export function inboundDedupKey(params: {
61
+ function inboundDedupKey(params: {
62
62
  accountId: string;
63
63
  platform: string;
64
64
  groupId: string;
@@ -348,18 +348,3 @@ export function buildPushFailureDebugInfo(args: {
348
348
  (typeof args.lastError === 'string' && args.lastError) || OUTBOUND_TERMINAL_REASON.PUSH_RETRY,
349
349
  };
350
350
  }
351
-
352
- export function buildReplyMediaFallbackDebugInfo(args: {
353
- sessionKey: string;
354
- mediaUrl: string;
355
- replyToId: string;
356
- fallback: { text: string; reason: string };
357
- }) {
358
- return {
359
- sessionKey: args.sessionKey,
360
- mediaUrl: args.mediaUrl,
361
- replyToId: args.replyToId || undefined,
362
- fallbackText: args.fallback.text,
363
- reason: args.fallback.reason,
364
- };
365
- }
@@ -17,7 +17,6 @@ export {
17
17
  buildOutboxRouteSelectDebugInfo,
18
18
  buildOutboxScheduleDebugInfo,
19
19
  buildPushFailureDebugInfo,
20
- buildReplyMediaFallbackDebugInfo,
21
20
  buildRetryRerouteDebugInfo,
22
21
  } from './diagnostics-debug-builders.ts';
23
22