@xmoxmo/bncr 0.3.2 → 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.
- package/README.md +15 -1
- package/index.ts +5 -10
- package/package.json +4 -4
- package/scripts/check-pack.mjs +15 -5
- package/scripts/selfcheck.mjs +30 -2
- package/src/channel.ts +79 -182
- package/src/core/accounts.ts +1 -1
- package/src/core/connection-reachability.ts +6 -1
- package/src/core/downlink-health.ts +3 -3
- package/src/core/extended-diagnostics.ts +2 -0
- package/src/core/file-transfer-payloads.ts +1 -4
- package/src/core/outbox-entry-builders.ts +4 -2
- package/src/core/outbox-file-transfer-bookkeeping.ts +1 -1
- package/src/core/outbox-file-transfer-failure.ts +2 -5
- package/src/core/outbox-file-transfer-success.ts +1 -4
- package/src/core/outbox-text-push-failure.ts +2 -4
- package/src/core/outbox-text-push-success.ts +1 -1
- package/src/messaging/inbound/commands.ts +34 -25
- package/src/messaging/inbound/dispatch.ts +16 -18
- package/src/messaging/inbound/parse.ts +3 -3
- package/src/messaging/inbound/runtime-compat.ts +8 -2
- package/src/messaging/outbound/build-send-action.ts +1 -2
- package/src/messaging/outbound/durable-message-adapter.ts +16 -6
- package/src/messaging/outbound/durable-queue-adapter.ts +3 -1
- package/src/messaging/outbound/media.ts +2 -1
- package/src/messaging/outbound/queue-selectors.ts +19 -6
- package/src/messaging/outbound/reasons.ts +2 -0
- package/src/messaging/outbound/reply-enqueue.ts +5 -1
- package/src/messaging/outbound/retry-policy.ts +16 -8
- package/src/messaging/outbound/session-route.ts +1 -1
- package/src/openclaw/reply-runtime.ts +4 -5
- package/src/openclaw/routing-runtime.ts +0 -1
- package/src/openclaw/sdk-helpers.ts +4 -1
- package/src/plugin/messaging.ts +2 -9
- package/src/plugin/status.ts +5 -1
- package/src/runtime/outbound-flags.ts +1 -1
- package/src/runtime/outbox-transitions.ts +4 -4
- package/src/runtime/status-snapshots.ts +3 -1
- package/src/runtime/status-worker.ts +8 -2
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)
|
|
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
|
-
|
|
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;
|
package/src/core/accounts.ts
CHANGED
|
@@ -142,7 +142,12 @@ export function getRevalidatedAttemptReason(args: {
|
|
|
142
142
|
lastPushTimeoutAt <= lastAttemptAt &&
|
|
143
143
|
lastSeenAt > lastPushTimeoutAt;
|
|
144
144
|
|
|
145
|
-
if (
|
|
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)) ||
|
|
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,
|
|
@@ -35,7 +35,8 @@ export function buildFileTransferOutboxEntry(args: {
|
|
|
35
35
|
asVoice: args.asVoice === true,
|
|
36
36
|
audioAsVoice: args.audioAsVoice === true,
|
|
37
37
|
finalEvent: args.pushEvent,
|
|
38
|
-
replyToId:
|
|
38
|
+
replyToId:
|
|
39
|
+
normalizeOutboundReplyToId({ kind: args.kind, replyToId: args.replyToId }) || undefined,
|
|
39
40
|
messageKind: args.kind,
|
|
40
41
|
},
|
|
41
42
|
},
|
|
@@ -64,7 +65,8 @@ export function buildTextOutboxEntry(args: {
|
|
|
64
65
|
messageId,
|
|
65
66
|
idempotencyKey: messageId,
|
|
66
67
|
sessionKey: args.sessionKey,
|
|
67
|
-
replyToId:
|
|
68
|
+
replyToId:
|
|
69
|
+
normalizeOutboundReplyToId({ kind: args.kind, replyToId: args.replyToId }) || undefined,
|
|
68
70
|
message: {
|
|
69
71
|
platform: args.route.platform,
|
|
70
72
|
groupId: args.route.groupId,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { OutboxEntry } from './types.ts';
|
|
2
1
|
import { buildPushFailureArgs } from './outbox-push-args.ts';
|
|
2
|
+
import type { OutboxEntry } from './types.ts';
|
|
3
3
|
|
|
4
4
|
export function resolveFileTransferFailureState(args: {
|
|
5
5
|
entry: OutboxEntry;
|
|
@@ -13,10 +13,7 @@ export function resolveFileTransferFailureState(args: {
|
|
|
13
13
|
};
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
export function buildFileTransferPushFailureArgs(args: {
|
|
17
|
-
entry: OutboxEntry;
|
|
18
|
-
retryable: boolean;
|
|
19
|
-
}) {
|
|
16
|
+
export function buildFileTransferPushFailureArgs(args: { entry: OutboxEntry; retryable: boolean }) {
|
|
20
17
|
return buildPushFailureArgs({
|
|
21
18
|
entry: args.entry,
|
|
22
19
|
retryable: args.retryable,
|
|
@@ -1,8 +1,5 @@
|
|
|
1
|
+
import { buildPushBroadcastPayload, buildPushRouteSelectArgs } from './outbox-push-args.ts';
|
|
1
2
|
import type { BncrConnection, OutboxEntry } from './types.ts';
|
|
2
|
-
import {
|
|
3
|
-
buildPushBroadcastPayload,
|
|
4
|
-
buildPushRouteSelectArgs,
|
|
5
|
-
} from './outbox-push-args.ts';
|
|
6
3
|
|
|
7
4
|
export function buildFileTransferBroadcastPayload(args: {
|
|
8
5
|
frame: Record<string, unknown>;
|
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
import type { OutboxEntry } from './types.ts';
|
|
2
1
|
import { buildPushFailureArgs } from './outbox-push-args.ts';
|
|
2
|
+
import type { OutboxEntry } from './types.ts';
|
|
3
3
|
|
|
4
|
-
export function buildTextPushFailureArgs(args: {
|
|
5
|
-
entry: OutboxEntry;
|
|
6
|
-
}) {
|
|
4
|
+
export function buildTextPushFailureArgs(args: { entry: OutboxEntry }) {
|
|
7
5
|
return buildPushFailureArgs({
|
|
8
6
|
entry: args.entry,
|
|
9
7
|
});
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import type { BncrConnection, OutboxEntry } from './types.ts';
|
|
2
1
|
import {
|
|
3
2
|
buildPushBroadcastPayload,
|
|
4
3
|
buildPushOkArgs,
|
|
5
4
|
buildPushRouteSelectArgs,
|
|
6
5
|
} from './outbox-push-args.ts';
|
|
6
|
+
import type { BncrConnection, OutboxEntry } from './types.ts';
|
|
7
7
|
|
|
8
8
|
export function buildTextPushBroadcastPayload(args: {
|
|
9
9
|
payload: Record<string, unknown>;
|