@xmoxmo/bncr 0.3.3 → 0.3.4

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 (37) hide show
  1. package/index.ts +5 -10
  2. package/package.json +4 -4
  3. package/scripts/check-pack.mjs +15 -5
  4. package/src/channel.ts +79 -182
  5. package/src/core/accounts.ts +1 -1
  6. package/src/core/connection-reachability.ts +6 -1
  7. package/src/core/downlink-health.ts +3 -3
  8. package/src/core/extended-diagnostics.ts +2 -0
  9. package/src/core/file-transfer-payloads.ts +1 -4
  10. package/src/core/outbox-entry-builders.ts +4 -2
  11. package/src/core/outbox-file-transfer-bookkeeping.ts +1 -1
  12. package/src/core/outbox-file-transfer-failure.ts +2 -5
  13. package/src/core/outbox-file-transfer-success.ts +1 -4
  14. package/src/core/outbox-text-push-failure.ts +2 -4
  15. package/src/core/outbox-text-push-success.ts +1 -1
  16. package/src/messaging/inbound/commands.ts +34 -25
  17. package/src/messaging/inbound/dispatch.ts +16 -18
  18. package/src/messaging/inbound/parse.ts +3 -3
  19. package/src/messaging/inbound/runtime-compat.ts +8 -2
  20. package/src/messaging/outbound/build-send-action.ts +1 -2
  21. package/src/messaging/outbound/durable-message-adapter.ts +15 -5
  22. package/src/messaging/outbound/durable-queue-adapter.ts +3 -1
  23. package/src/messaging/outbound/media.ts +2 -1
  24. package/src/messaging/outbound/queue-selectors.ts +19 -6
  25. package/src/messaging/outbound/reasons.ts +2 -0
  26. package/src/messaging/outbound/reply-enqueue.ts +5 -1
  27. package/src/messaging/outbound/retry-policy.ts +16 -8
  28. package/src/messaging/outbound/session-route.ts +1 -1
  29. package/src/openclaw/reply-runtime.ts +4 -5
  30. package/src/openclaw/routing-runtime.ts +0 -1
  31. package/src/openclaw/sdk-helpers.ts +4 -1
  32. package/src/plugin/messaging.ts +2 -9
  33. package/src/plugin/status.ts +5 -1
  34. package/src/runtime/outbound-flags.ts +1 -1
  35. package/src/runtime/outbox-transitions.ts +4 -4
  36. package/src/runtime/status-snapshots.ts +3 -1
  37. package/src/runtime/status-worker.ts +8 -2
package/index.ts CHANGED
@@ -111,7 +111,6 @@ type BncrGatewayRuntime = {
111
111
  };
112
112
 
113
113
  let runtime: LoadedRuntime | null = null;
114
- let activeServiceStop: (() => Promise<void>) | null = null;
115
114
  const identityIds = new WeakMap<object, string>();
116
115
  let identitySeq = 0;
117
116
 
@@ -297,7 +296,9 @@ const loadRuntimeSync = (): LoadedRuntime => {
297
296
  return runtime;
298
297
  } catch (error) {
299
298
  const detail = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
300
- throw new Error(`bncr failed to load channel runtime after dependency bootstrap from ${runtimeSourceDir}: ${detail}`);
299
+ throw new Error(
300
+ `bncr failed to load channel runtime after dependency bootstrap from ${runtimeSourceDir}: ${detail}`,
301
+ );
301
302
  }
302
303
  };
303
304
 
@@ -365,14 +366,9 @@ const getGatewayRuntime = (): BncrGatewayRuntime => {
365
366
  };
366
367
 
367
368
  const getProcessOwnerApiInstanceId = (gatewayRuntime: BncrGatewayRuntime) =>
368
- gatewayRuntime.serviceOwnerApiInstanceId ||
369
- gatewayRuntime.channelOwnerApiInstanceId ||
370
- undefined;
369
+ gatewayRuntime.serviceOwnerApiInstanceId || gatewayRuntime.channelOwnerApiInstanceId || undefined;
371
370
 
372
- const shouldAdoptProcessOwner = (
373
- apiInstanceId: string,
374
- gatewayRuntime: BncrGatewayRuntime,
375
- ) => {
371
+ const shouldAdoptProcessOwner = (apiInstanceId: string, gatewayRuntime: BncrGatewayRuntime) => {
376
372
  const existingOwnerApiInstanceId = getProcessOwnerApiInstanceId(gatewayRuntime);
377
373
  const hasSingletonOwner =
378
374
  Boolean(gatewayRuntime.serviceRegistered) || Boolean(gatewayRuntime.channelRegistered);
@@ -845,7 +841,6 @@ const plugin = {
845
841
  },
846
842
  stop: serviceStopHandler,
847
843
  });
848
- activeServiceStop = serviceStopHandler;
849
844
  gatewayRuntime.serviceRegistered = true;
850
845
  gatewayRuntime.serviceOwnerApiInstanceId = apiInstanceId;
851
846
  meta.service = true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xmoxmo/bncr",
3
- "version": "0.3.3",
3
+ "version": "0.3.4",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -39,9 +39,9 @@
39
39
  "openclaw": ">=2026.5.27"
40
40
  },
41
41
  "devDependencies": {
42
- "@biomejs/biome": "^1.9.4",
43
- "openclaw": ">=2026.5.27",
44
- "esbuild": "^0.28.0"
42
+ "@biomejs/biome": "^2.4.16",
43
+ "esbuild": "^0.28.0",
44
+ "openclaw": ">=2026.5.27"
45
45
  },
46
46
  "openclaw": {
47
47
  "extensions": [
@@ -48,19 +48,29 @@ const [pack] = JSON.parse(output);
48
48
  const packedFiles = new Set((pack?.files ?? []).map((file) => file.path));
49
49
  const missing = requiredPackFiles.filter((file) => !packedFiles.has(file));
50
50
  const channelSource = fs.readFileSync(path.join(root, 'src/channel.ts'), 'utf8');
51
- const messagePolicySource = fs.readFileSync(path.join(root, 'src/plugin/message-policy.ts'), 'utf8');
51
+ const messagePolicySource = fs.readFileSync(
52
+ path.join(root, 'src/plugin/message-policy.ts'),
53
+ 'utf8',
54
+ );
52
55
  const messageSendSource = fs.readFileSync(path.join(root, 'src/plugin/message-send.ts'), 'utf8');
53
56
  const channelMessageChecks = {
54
57
  registered: channelSource.includes('message: {'),
55
- text: channelSource.includes('createBncrMessageSend') && messageSendSource.includes('channelMessageSendText'),
56
- media: channelSource.includes('createBncrMessageSend') && messageSendSource.includes('channelMessageSendMedia'),
57
- payload: channelSource.includes('createBncrMessageSend') && messageSendSource.includes('channelMessageSendPayload'),
58
+ text:
59
+ channelSource.includes('createBncrMessageSend') &&
60
+ messageSendSource.includes('channelMessageSendText'),
61
+ media:
62
+ channelSource.includes('createBncrMessageSend') &&
63
+ messageSendSource.includes('channelMessageSendMedia'),
64
+ payload:
65
+ channelSource.includes('createBncrMessageSend') &&
66
+ messageSendSource.includes('channelMessageSendPayload'),
58
67
  manualAck:
59
68
  channelSource.includes('BNCR_MESSAGE_RECEIVE_POLICY') &&
60
69
  messagePolicySource.includes("defaultAckPolicy: 'manual'") &&
61
70
  messagePolicySource.includes("supportedAckPolicies: ['manual']"),
62
71
  genericActionsPreserved: channelSource.includes('actions: messageActions'),
63
- noDurableFinal: !channelSource.includes('durableFinal:') && !messagePolicySource.includes('durableFinal:'),
72
+ noDurableFinal:
73
+ !channelSource.includes('durableFinal:') && !messagePolicySource.includes('durableFinal:'),
64
74
  };
65
75
  const channelMessageOk = Object.values(channelMessageChecks).every(Boolean);
66
76
 
package/src/channel.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  import { createHash, randomUUID } from 'node:crypto';
2
- import fs from 'node:fs';
3
2
  import path from 'node:path';
4
3
  import type {
5
4
  GatewayRequestHandlerOptions,
@@ -92,28 +91,18 @@ import {
92
91
  import {
93
92
  buildCanonicalBncrSessionKey,
94
93
  formatDisplayScope,
95
- isLowerHex,
96
94
  normalizeInboundSessionKey,
97
95
  normalizeStoredSessionKey,
98
96
  parseRouteFromDisplayScope,
99
- parseRouteFromHexScope,
100
- parseRouteFromScope,
101
97
  parseRouteLike,
102
98
  parseStrictBncrSessionKey,
103
99
  routeKey,
104
- routeScopeToHex,
105
100
  withTaskSessionKey,
106
101
  } from './core/targets.ts';
107
102
  import type { BncrConnection, BncrRoute, OutboxEntry } from './core/types.ts';
108
103
  import { dispatchBncrInbound } from './messaging/inbound/dispatch.ts';
109
104
  import { checkBncrMessageGate } from './messaging/inbound/gate.ts';
110
105
  import { parseBncrInboundParams } from './messaging/inbound/parse.ts';
111
- import {
112
- deleteBncrMessageAction,
113
- editBncrMessageAction,
114
- reactBncrMessageAction,
115
- sendBncrReplyAction,
116
- } from './messaging/outbound/actions.ts';
117
106
  import {
118
107
  buildEnqueueFromReplyDebugInfo,
119
108
  buildFlushDebugInfo,
@@ -125,20 +114,16 @@ import {
125
114
  buildOutboxRouteSelectDebugInfo,
126
115
  buildOutboxScheduleDebugInfo,
127
116
  buildPushFailureDebugInfo,
128
- buildReplyMediaFallbackDebugInfo,
129
117
  buildRetryRerouteDebugInfo,
130
118
  } from './messaging/outbound/diagnostics.ts';
131
- import {
132
- buildBncrMediaOutboundFrame,
133
- resolveBncrOutboundMessageType,
134
- } from './messaging/outbound/media.ts';
119
+ import { buildBncrMediaOutboundFrame } from './messaging/outbound/media.ts';
135
120
  import {
136
121
  getOpenClawRuntimeConfig,
137
122
  getOpenClawRuntimeConfigOrDefault,
138
123
  } from './openclaw/config-runtime.ts';
139
124
  import {
140
- type OpenClawLoadedMedia,
141
125
  loadOpenClawWebMedia,
126
+ type OpenClawLoadedMedia,
142
127
  saveOpenClawChannelMediaBuffer,
143
128
  } from './openclaw/media-runtime.ts';
144
129
  import { resolveOpenClawAgentRoute } from './openclaw/routing-runtime.ts';
@@ -283,10 +268,11 @@ function buildInboundResponsePayload(
283
268
  };
284
269
  }
285
270
  }
271
+
286
272
  import { buildBncrDurableQueuedResult } from './messaging/outbound/durable-queue-adapter.ts';
287
273
  import {
288
- type MediaDedupeCacheEntry,
289
274
  buildMediaTextFallback,
275
+ type MediaDedupeCacheEntry,
290
276
  normalizeMessageText,
291
277
  normalizeReplyToId,
292
278
  } from './messaging/outbound/media-dedupe.ts';
@@ -310,18 +296,16 @@ import {
310
296
  OUTBOUND_TERMINAL_REASON,
311
297
  } from './messaging/outbound/reasons.ts';
312
298
  import {
313
- type NormalizedReplyPayload,
314
- type ReplyMediaEntriesParams,
315
- type ReplyMediaFileTransferParams,
316
- type ReplyPayloadInput,
317
- buildReplyTextOutboxEntry,
318
299
  enqueueNormalizedReplyPayload,
319
300
  enqueueReplyMediaFallbackTextEntry,
320
301
  enqueueReplyMediaFileTransferEntry,
321
302
  enqueueReplyTextEntry,
322
303
  enqueueSingleReplyMediaEntry,
323
304
  hasReplyMediaEntries,
305
+ type NormalizedReplyPayload,
324
306
  normalizeReplyPayload,
307
+ type ReplyMediaEntriesParams,
308
+ type ReplyPayloadInput,
325
309
  } from './messaging/outbound/reply-enqueue.ts';
326
310
  import {
327
311
  computePushFailureDecision,
@@ -339,10 +323,7 @@ import { BNCR_CHANNEL_META } from './plugin/meta.ts';
339
323
  import { createBncrOutboundRuntime } from './plugin/outbound.ts';
340
324
  import { BNCR_SETUP_SURFACE } from './plugin/setup.ts';
341
325
  import { createBncrStatusSurface } from './plugin/status.ts';
342
- import {
343
- pruneLogDedupeState as pruneLogDedupeStateFromRuntime,
344
- shouldEmitDedupLog as shouldEmitDedupLogFromRuntime,
345
- } from './runtime/log-dedupe.ts';
326
+ import { shouldEmitDedupLog as shouldEmitDedupLogFromRuntime } from './runtime/log-dedupe.ts';
346
327
  import {
347
328
  buildBncrRuntimeAckStrategy,
348
329
  computeBncrRecommendedAckTimeoutMs,
@@ -365,10 +346,10 @@ import { buildRuntimeStatusSnapshots } from './runtime/status-snapshots.ts';
365
346
  import {
366
347
  type ChannelAccountWorkerHandle,
367
348
  clearAllBncrStatusWorkers,
368
- clearBncrStatusWorker,
369
349
  startBncrStatusWorker,
370
350
  stopBncrStatusWorker,
371
351
  } from './runtime/status-worker.ts';
352
+
372
353
  const BRIDGE_VERSION = 2;
373
354
  const BNCR_PUSH_EVENT = 'plugin.bncr.push';
374
355
  const BNCR_FILE_INIT_EVENT = 'plugin.bncr.file.init';
@@ -387,6 +368,7 @@ const PUSH_DRAIN_ACCOUNT_TIME_BUDGET_MS = 2_000;
387
368
  const PUSH_DRAIN_EXCEPTION_RETRY_LIMIT = 3;
388
369
  const PUSH_DRAIN_EXCEPTION_RETRY_DELAY_MS = 1_000;
389
370
  const PUSH_DRAIN_STUCK_WARN_MS = 30_000;
371
+ const PRE_PUSH_GUARD_RETRY_DELAY_MS = 1_000;
390
372
  const PUSH_ACK_TIMEOUT_MS = 30_000;
391
373
  const ADAPTIVE_ACK_TIMEOUT_DEFAULT_ENABLED = true;
392
374
  const RECOMMENDED_ACK_TIMEOUT_MIN_MS = PUSH_ACK_TIMEOUT_MS;
@@ -402,7 +384,6 @@ const FILE_CHUNK_SIZE = 256 * 1024; // 256KB
402
384
  const INBOUND_FILE_TRANSFER_MAX_BYTES = 50 * 1024 * 1024;
403
385
  const INBOUND_FILE_TRANSFER_MAX_CHUNKS =
404
386
  Math.ceil(INBOUND_FILE_TRANSFER_MAX_BYTES / FILE_CHUNK_SIZE) + 1;
405
- const FILE_CHUNK_RETRY = 3;
406
387
  const FILE_ACK_TIMEOUT_MS = 30_000;
407
388
  const FILE_TRANSFER_ACK_TTL_MS = 30_000;
408
389
  const MAX_EARLY_FILE_ACKS = 1000;
@@ -462,8 +443,6 @@ type FileAckPayloadState = {
462
443
  at: number;
463
444
  };
464
445
 
465
- type ChatType = 'direct' | 'group' | (string & {});
466
-
467
446
  type ChannelMessageActionAdapter = {
468
447
  describeMessageTool: (ctx: { cfg: any }) => { actions: string[]; capabilities: unknown[] } | null;
469
448
  supportsAction: (ctx: { action: string }) => boolean;
@@ -770,8 +749,6 @@ class BncrBridgeRuntime {
770
749
  private channelAccountWorkers = new Map<string, ChannelAccountWorkerHandle>();
771
750
  private logDedupeState = new Map<string, { at: number; sig: string }>();
772
751
  private canonicalAgentId: string | null = null;
773
- private canonicalAgentSource: 'startup' | 'runtime' | 'fallback-main' | null = null;
774
- private canonicalAgentResolvedAt: number | null = null;
775
752
 
776
753
  // 内置健康/回归计数(替代独立脚本)
777
754
  private startedAt = now();
@@ -787,6 +764,12 @@ class BncrBridgeRuntime {
787
764
  private pushDrainRunningSinceByAccount = new Map<string, number>();
788
765
  private pushDrainStuckWarnedAtByAccount = new Map<string, number>();
789
766
  private pushDrainExceptionRetryCount = 0;
767
+ private lastGatewayContextAt: number | null = null;
768
+ private outboundEnqueueCountByAccount = new Map<string, number>();
769
+ private lastOutboundEnqueueAtByAccount = new Map<string, number>();
770
+ private prePushGuardSkipCountByAccount = new Map<string, number>();
771
+ private lastPrePushGuardSkipAtByAccount = new Map<string, number>();
772
+ private lastPrePushGuardSkipReasonByAccount = new Map<string, string>();
790
773
  private messageAckWaiters = new Map<
791
774
  // Refactor boundary note (message ACK runtime):
792
775
  // These waiters are part of the outbound message-ack lifecycle, not just a utility map.
@@ -862,28 +845,6 @@ class BncrBridgeRuntime {
862
845
  this.logInfo(scope, this.buildDebugJsonMessage(event, payload), options);
863
846
  }
864
847
 
865
- private logWarnJson(
866
- scope: string | undefined,
867
- event: string,
868
- payload: Record<string, unknown>,
869
- options?: { debugOnly?: boolean },
870
- ) {
871
- this.logWarn(scope, this.buildDebugJsonMessage(event, payload), options);
872
- }
873
-
874
- private logErrorJson(
875
- scope: string | undefined,
876
- event: string,
877
- payload: Record<string, unknown>,
878
- options?: { debugOnly?: boolean },
879
- ) {
880
- this.logError(scope, this.buildDebugJsonMessage(event, payload), options);
881
- }
882
-
883
- private pruneLogDedupeState(currentTime = now()) {
884
- pruneLogDedupeStateFromRuntime(this.logDedupeState, currentTime);
885
- }
886
-
887
848
  private shouldEmitDedupLog(key: string, sig: string, windowMs = 5 * 60 * 1000) {
888
849
  return shouldEmitDedupLogFromRuntime({
889
850
  state: this.logDedupeState,
@@ -903,24 +864,6 @@ class BncrBridgeRuntime {
903
864
  this.logInfo(scope, message, { debugOnly: options.debugOnly });
904
865
  }
905
866
 
906
- private logWarnDedup(
907
- scope: string | undefined,
908
- message: string,
909
- options: { key: string; sig: string; debugOnly?: boolean; windowMs?: number },
910
- ) {
911
- if (!this.shouldEmitDedupLog(options.key, options.sig, options.windowMs)) return;
912
- this.logWarn(scope, message, { debugOnly: options.debugOnly });
913
- }
914
-
915
- private logErrorDedup(
916
- scope: string | undefined,
917
- message: string,
918
- options: { key: string; sig: string; debugOnly?: boolean; windowMs?: number },
919
- ) {
920
- if (!this.shouldEmitDedupLog(options.key, options.sig, options.windowMs)) return;
921
- this.logError(scope, message, { debugOnly: options.debugOnly });
922
- }
923
-
924
867
  private logInfoDedupJson(
925
868
  scope: string | undefined,
926
869
  event: string,
@@ -931,26 +874,6 @@ class BncrBridgeRuntime {
931
874
  this.logInfoJson(scope, event, payload, { debugOnly: options.debugOnly });
932
875
  }
933
876
 
934
- private logWarnDedupJson(
935
- scope: string | undefined,
936
- event: string,
937
- payload: Record<string, unknown>,
938
- options: { key: string; sig: string; debugOnly?: boolean; windowMs?: number },
939
- ) {
940
- if (!this.shouldEmitDedupLog(options.key, options.sig, options.windowMs)) return;
941
- this.logWarnJson(scope, event, payload, { debugOnly: options.debugOnly });
942
- }
943
-
944
- private logErrorDedupJson(
945
- scope: string | undefined,
946
- event: string,
947
- payload: Record<string, unknown>,
948
- options: { key: string; sig: string; debugOnly?: boolean; windowMs?: number },
949
- ) {
950
- if (!this.shouldEmitDedupLog(options.key, options.sig, options.windowMs)) return;
951
- this.logErrorJson(scope, event, payload, { debugOnly: options.debugOnly });
952
- }
953
-
954
877
  private summarizeTextPreview(raw: string, limit = 8) {
955
878
  const compact = asString(raw || '')
956
879
  .replace(/\s+/g, ' ')
@@ -1026,10 +949,6 @@ class BncrBridgeRuntime {
1026
949
  };
1027
950
  }
1028
951
 
1029
- private clearChannelAccountWorker(accountId: string, reason: string) {
1030
- return clearBncrStatusWorker(this.buildStatusWorkerRuntime(), accountId, reason);
1031
- }
1032
-
1033
952
  private clearAllChannelAccountWorkers(reason: string) {
1034
953
  clearAllBncrStatusWorkers(this.buildStatusWorkerRuntime(), reason);
1035
954
  }
@@ -1250,6 +1169,8 @@ class BncrBridgeRuntime {
1250
1169
  lastActivityAt: this.lastActivityAtGlobal,
1251
1170
  lastInboundAt: this.lastInboundAtGlobal,
1252
1171
  lastAckAt: this.lastAckAtGlobal,
1172
+ hasGatewayContext: Boolean(this.gatewayContext),
1173
+ lastGatewayContextAt: this.lastGatewayContextAt,
1253
1174
  recent: Array.from(this.recentConnections.entries()).map(([leaseId, entry]) => ({
1254
1175
  leaseId,
1255
1176
  epoch: entry.epoch,
@@ -1258,6 +1179,16 @@ class BncrBridgeRuntime {
1258
1179
  isPrimary: entry.isPrimary,
1259
1180
  })),
1260
1181
  },
1182
+ outbound: {
1183
+ pending: Array.from(this.outbox.values()).filter((entry) => entry.accountId === acc).length,
1184
+ enqueueCount: this.getCounter(this.outboundEnqueueCountByAccount, acc),
1185
+ lastEnqueueAt: this.lastOutboundEnqueueAtByAccount.get(acc) || null,
1186
+ prePushGuardSkipCount: this.getCounter(this.prePushGuardSkipCountByAccount, acc),
1187
+ lastPrePushGuardSkipAt: this.lastPrePushGuardSkipAtByAccount.get(acc) || null,
1188
+ lastPrePushGuardSkipReason: this.lastPrePushGuardSkipReasonByAccount.get(acc) || null,
1189
+ hasGatewayContext: Boolean(this.gatewayContext),
1190
+ lastGatewayContextAt: this.lastGatewayContextAt,
1191
+ },
1261
1192
  protocol: {
1262
1193
  bridgeVersion: BRIDGE_VERSION,
1263
1194
  protocolVersion: 2,
@@ -1423,8 +1354,6 @@ class BncrBridgeRuntime {
1423
1354
  });
1424
1355
  if (!agentId) return;
1425
1356
  this.canonicalAgentId = agentId;
1426
- this.canonicalAgentSource = 'startup';
1427
- this.canonicalAgentResolvedAt = now();
1428
1357
  }
1429
1358
 
1430
1359
  private ensureCanonicalAgentId(args: {
@@ -1438,14 +1367,10 @@ class BncrBridgeRuntime {
1438
1367
  const agentId = this.tryResolveBindingAgentId(args);
1439
1368
  if (agentId) {
1440
1369
  this.canonicalAgentId = agentId;
1441
- this.canonicalAgentSource = 'runtime';
1442
- this.canonicalAgentResolvedAt = now();
1443
1370
  return agentId;
1444
1371
  }
1445
1372
 
1446
1373
  this.canonicalAgentId = 'main';
1447
- this.canonicalAgentSource = 'fallback-main';
1448
- this.canonicalAgentResolvedAt = now();
1449
1374
  this.logWarn(
1450
1375
  'target',
1451
1376
  'binding agent unresolved; fallback to main for current process lifetime',
@@ -1808,7 +1733,9 @@ class BncrBridgeRuntime {
1808
1733
  }
1809
1734
 
1810
1735
  private rememberGatewayContext(context: GatewayRequestHandlerOptions['context']) {
1811
- if (context) this.gatewayContext = context;
1736
+ if (!context) return;
1737
+ this.gatewayContext = context;
1738
+ this.lastGatewayContextAt = now();
1812
1739
  }
1813
1740
 
1814
1741
  private resolveOutboxPushOwner(accountId: string): BncrConnection | null {
@@ -2156,10 +2083,7 @@ class BncrBridgeRuntime {
2156
2083
  );
2157
2084
  }
2158
2085
 
2159
- private handleFileTransferPushFailure(args: {
2160
- entry: OutboxEntry;
2161
- error: unknown;
2162
- }) {
2086
+ private handleFileTransferPushFailure(args: { entry: OutboxEntry; error: unknown }) {
2163
2087
  this.recordOutboxPushFailure({
2164
2088
  entry: args.entry,
2165
2089
  error: args.error,
@@ -2473,10 +2397,7 @@ class BncrBridgeRuntime {
2473
2397
  );
2474
2398
  }
2475
2399
 
2476
- private handleTextPushFailure(args: {
2477
- entry: OutboxEntry;
2478
- error: unknown;
2479
- }) {
2400
+ private handleTextPushFailure(args: { entry: OutboxEntry; error: unknown }) {
2480
2401
  this.recordOutboxPushFailure({
2481
2402
  entry: args.entry,
2482
2403
  error: args.error,
@@ -2558,6 +2479,7 @@ class BncrBridgeRuntime {
2558
2479
  ownerConnId?: string;
2559
2480
  ownerClientId?: string;
2560
2481
  }) {
2482
+ this.recordPrePushGuardSkip({ accountId: args.accountId, reason: args.reason });
2561
2483
  this.logInfo(
2562
2484
  'outbox push skip',
2563
2485
  `mid=${args.messageId}|q=${this.outbox.size}|reason=${args.reason}${args.kind ? `|kind=${args.kind}` : ''}`,
@@ -3148,6 +3070,25 @@ class BncrBridgeRuntime {
3148
3070
  if (args.persist) this.scheduleSave();
3149
3071
  }
3150
3072
 
3073
+ private isPrePushGuardReason(reason: string) {
3074
+ return reason === 'no-gateway-context' || reason === 'no-active-connection';
3075
+ }
3076
+
3077
+ private recordPrePushGuardSkip(args: { accountId: string; reason: string }) {
3078
+ if (!this.isPrePushGuardReason(args.reason)) return;
3079
+ const acc = normalizeAccountId(args.accountId);
3080
+ this.incrementCounter(this.prePushGuardSkipCountByAccount, acc);
3081
+ this.lastPrePushGuardSkipAtByAccount.set(acc, now());
3082
+ this.lastPrePushGuardSkipReasonByAccount.set(acc, args.reason);
3083
+ }
3084
+
3085
+ private isPrePushGuardDeferral(entry: OutboxEntry) {
3086
+ return (
3087
+ entry.lastError === 'gateway context unavailable' ||
3088
+ entry.lastError === 'no active bncr client'
3089
+ );
3090
+ }
3091
+
3151
3092
  private recordOutboxPushFailure(args: {
3152
3093
  entry: OutboxEntry;
3153
3094
  error: unknown;
@@ -3263,11 +3204,7 @@ class BncrBridgeRuntime {
3263
3204
  return Array.from(this.outbox.values()).filter((entry) => entry.accountId === acc);
3264
3205
  }
3265
3206
 
3266
- private maybeLogOutboxDrainStuck(args: {
3267
- accountId: string;
3268
- trigger: string;
3269
- reason: string;
3270
- }) {
3207
+ private maybeLogOutboxDrainStuck(args: { accountId: string; trigger: string; reason: string }) {
3271
3208
  const acc = normalizeAccountId(args.accountId);
3272
3209
  const startedAt = this.pushDrainRunningSinceByAccount.get(acc) || 0;
3273
3210
  if (!startedAt) return;
@@ -3637,6 +3574,26 @@ class BncrBridgeRuntime {
3637
3574
  continue;
3638
3575
  }
3639
3576
 
3577
+ if (this.isPrePushGuardDeferral(entry)) {
3578
+ const wait = PRE_PUSH_GUARD_RETRY_DELAY_MS;
3579
+ localNextDelay = updateMinOutboxDelay(localNextDelay, wait);
3580
+ this.logInfo(
3581
+ 'outbox',
3582
+ `schedule ${JSON.stringify(
3583
+ buildOutboxScheduleDebugInfo({
3584
+ bridgeId: this.bridgeId,
3585
+ accountId: acc,
3586
+ messageId: entry.messageId,
3587
+ source: OUTBOUND_SCHEDULE_SOURCE.PRE_PUSH_GUARD_WAIT,
3588
+ wait,
3589
+ localNextDelay,
3590
+ }),
3591
+ )}`,
3592
+ { debugOnly: true },
3593
+ );
3594
+ break;
3595
+ }
3596
+
3640
3597
  const decision = computePushFailureDecision(
3641
3598
  {
3642
3599
  nowMs: t,
@@ -4408,37 +4365,6 @@ class BncrBridgeRuntime {
4408
4365
  return true;
4409
4366
  }
4410
4367
 
4411
- private pushFileEventToAccount(
4412
- accountId: string,
4413
- event: string,
4414
- payload: Record<string, unknown>,
4415
- ) {
4416
- const connIds = this.resolvePushConnIds(accountId);
4417
- if (!connIds.size || !this.gatewayContext) {
4418
- throw new Error(`no active bncr connection for account=${accountId}`);
4419
- }
4420
- const normalizedEvent =
4421
- event === 'bncr.file.init'
4422
- ? BNCR_FILE_INIT_EVENT
4423
- : event === 'bncr.file.chunk'
4424
- ? BNCR_FILE_CHUNK_EVENT
4425
- : event === 'bncr.file.complete'
4426
- ? BNCR_FILE_COMPLETE_EVENT
4427
- : event === 'bncr.file.abort'
4428
- ? BNCR_FILE_ABORT_EVENT
4429
- : event;
4430
- this.gatewayContext.broadcastToConnIds(normalizedEvent, payload, connIds);
4431
- }
4432
-
4433
- private resolveInboundFileType(mimeType: string, fileName: string): string {
4434
- const mt = asString(mimeType).toLowerCase();
4435
- const fn = asString(fileName).toLowerCase();
4436
- if (mt.startsWith('image/') || /\.(png|jpe?g|gif|webp|bmp|svg)$/.test(fn)) return 'image';
4437
- if (mt.startsWith('video/') || /\.(mp4|mov|mkv|avi|webm)$/.test(fn)) return 'video';
4438
- if (mt.startsWith('audio/') || /\.(mp3|wav|m4a|aac|ogg|flac)$/.test(fn)) return 'audio';
4439
- return mt || 'file';
4440
- }
4441
-
4442
4368
  private computeRecommendedAckTimeoutReason(args: {
4443
4369
  lateAckOkCount: number;
4444
4370
  recentAckTimeoutCount: number;
@@ -4731,6 +4657,9 @@ class BncrBridgeRuntime {
4731
4657
  { debugOnly: true },
4732
4658
  );
4733
4659
  this.logOutboundSummary(entry);
4660
+ const accountId = normalizeAccountId(entry.accountId);
4661
+ this.incrementCounter(this.outboundEnqueueCountByAccount, accountId);
4662
+ this.lastOutboundEnqueueAtByAccount.set(accountId, now());
4734
4663
  this.outbox.set(entry.messageId, entry);
4735
4664
  this.scheduleSave();
4736
4665
  this.flushPushQueueBestEffort({ accountId: entry.accountId });
@@ -4757,7 +4686,7 @@ class BncrBridgeRuntime {
4757
4686
  this.scheduleSave();
4758
4687
  }
4759
4688
 
4760
- private collectDue(accountId: string, maxBatch: number): Array<Record<string, unknown>> {
4689
+ collectDue(accountId: string, maxBatch: number): Array<Record<string, unknown>> {
4761
4690
  const key = normalizeAccountId(accountId);
4762
4691
  const result = collectDueOutboxEntries({
4763
4692
  outbox: this.outbox.values(),
@@ -4779,21 +4708,6 @@ class BncrBridgeRuntime {
4779
4708
  return result.duePayloads;
4780
4709
  }
4781
4710
 
4782
- private async payloadMediaToBase64(
4783
- mediaUrl: string,
4784
- mediaLocalRoots?: readonly string[],
4785
- ): Promise<{ mediaBase64: string; mimeType?: string; fileName?: string }> {
4786
- const loaded = await loadOpenClawWebMedia(this.api, mediaUrl, {
4787
- localRoots: mediaLocalRoots,
4788
- maxBytes: 20 * 1024 * 1024,
4789
- });
4790
- return {
4791
- mediaBase64: loaded.buffer.toString('base64'),
4792
- mimeType: loaded.contentType,
4793
- fileName: loaded.fileName,
4794
- };
4795
- }
4796
-
4797
4711
  private async loadOutboundTransferMedia(params: {
4798
4712
  mediaUrl: string;
4799
4713
  mediaLocalRoots?: readonly string[];
@@ -6194,24 +6108,7 @@ class BncrBridgeRuntime {
6194
6108
  // versus "scheduled retry" versus "ACK-driven continuation".
6195
6109
  await this.syncDebugFlag();
6196
6110
  const parsed = parseBncrInboundParams(params);
6197
- const {
6198
- accountId,
6199
- platform,
6200
- groupId,
6201
- userId,
6202
- sessionKeyfromroute,
6203
- route,
6204
- text,
6205
- msgType,
6206
- mediaBase64,
6207
- mediaPathFromTransfer,
6208
- mimeType,
6209
- fileName,
6210
- msgId,
6211
- dedupKey,
6212
- peer,
6213
- extracted,
6214
- } = parsed;
6111
+ const { accountId, platform, route, msgType, msgId, peer, extracted } = parsed;
6215
6112
  const connId = asString(client?.connId || '').trim() || `no-conn-${Date.now()}`;
6216
6113
  const clientId = asString((params as any)?.clientId || '').trim() || undefined;
6217
6114
  const outboundReady = (params as any)?.outboundReady === true;
@@ -53,4 +53,4 @@ export function listAccountIds(cfg: any): string[] {
53
53
  return ids.length ? ids : [BNCR_DEFAULT_ACCOUNT_ID];
54
54
  }
55
55
 
56
- export { CHANNEL_ID, BNCR_DEFAULT_ACCOUNT_ID };
56
+ export { BNCR_DEFAULT_ACCOUNT_ID, CHANNEL_ID };
@@ -142,7 +142,12 @@ export function getRevalidatedAttemptReason(args: {
142
142
  lastPushTimeoutAt <= lastAttemptAt &&
143
143
  lastSeenAt > lastPushTimeoutAt;
144
144
 
145
- if (!revalidatedByPreferred && !revalidatedByReady && !revalidatedByAck && !revalidatedByFreshReachability) {
145
+ if (
146
+ !revalidatedByPreferred &&
147
+ !revalidatedByReady &&
148
+ !revalidatedByAck &&
149
+ !revalidatedByFreshReachability
150
+ ) {
146
151
  return null;
147
152
  }
148
153
 
@@ -28,10 +28,10 @@ export function buildDownlinkHealth(input: DownlinkHealthInput) {
28
28
  ? Math.max(0, input.now - oldestPendingCreatedAt)
29
29
  : 0;
30
30
  const lastSignalAt =
31
- Math.max(finiteNumberOr(input.lastInboundAt, 0), finiteNumberOr(input.lastActivityAt, 0)) || null;
31
+ Math.max(finiteNumberOr(input.lastInboundAt, 0), finiteNumberOr(input.lastActivityAt, 0)) ||
32
+ null;
32
33
  const inboundHealthy = !!lastSignalAt && input.now - lastSignalAt <= 5 * 60 * 1000;
33
- const ackRecentlyHealthy =
34
- !!input.lastAckOkAt && input.now - input.lastAckOkAt <= 5 * 60 * 1000;
34
+ const ackRecentlyHealthy = !!input.lastAckOkAt && input.now - input.lastAckOkAt <= 5 * 60 * 1000;
35
35
  const ackTimeoutRecent =
36
36
  !!input.lastAckTimeoutAt && input.now - input.lastAckTimeoutAt <= 5 * 60 * 1000;
37
37
  const ackStalled =
@@ -22,6 +22,7 @@ type ExtendedDiagnosticsInput = {
22
22
  traceSummary: Record<string, any>;
23
23
  lastDriftSnapshot: any;
24
24
  };
25
+ outbound?: Record<string, any>;
25
26
  connection: {
26
27
  active: number;
27
28
  primaryLeaseId: string | null;
@@ -66,6 +67,7 @@ export function buildExtendedDiagnostics(input: ExtendedDiagnosticsInput) {
66
67
  ...input.connection,
67
68
  recent: input.connection.recent.map((entry) => ({ ...entry })),
68
69
  },
70
+ outbound: input.outbound ? { ...input.outbound } : undefined,
69
71
  protocol: {
70
72
  ...input.protocol,
71
73
  features: { ...input.protocol.features },
@@ -61,10 +61,7 @@ export function buildFileTransferAbortPayload(args: {
61
61
  };
62
62
  }
63
63
 
64
- export function buildFileTransferCompletePayload(args: {
65
- transferId: string;
66
- ts: number;
67
- }) {
64
+ export function buildFileTransferCompletePayload(args: { transferId: string; ts: number }) {
68
65
  return {
69
66
  transferId: args.transferId,
70
67
  ts: args.ts,