@xmoxmo/bncr 0.2.7 → 0.2.9
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/package.json +1 -1
- package/scripts/check-pack.mjs +21 -5
- package/src/channel.ts +668 -766
- package/src/core/extended-diagnostics.ts +10 -0
- package/src/core/file-ack.ts +9 -0
- package/src/core/file-transfer-payloads.ts +72 -0
- package/src/core/register-trace.ts +79 -0
- package/src/messaging/inbound/runtime-compat.ts +3 -1
- package/src/messaging/outbound/diagnostics.ts +128 -6
- package/src/plugin/capabilities.ts +8 -0
- package/src/plugin/config.ts +35 -0
- package/src/plugin/gateway-methods.ts +12 -0
- package/src/plugin/gateway-runtime.ts +11 -0
- package/src/plugin/message-policy.ts +4 -0
- package/src/plugin/message-send.ts +13 -0
- package/src/plugin/messaging.ts +142 -0
- package/src/plugin/meta.ts +10 -0
- package/src/plugin/outbound.ts +51 -0
- package/src/plugin/setup.ts +24 -0
- package/src/plugin/status.ts +38 -0
- package/src/runtime/log-dedupe.ts +56 -0
- package/src/runtime/outbound-ack-timeout.ts +96 -0
- package/src/runtime/outbound-flags.ts +81 -0
- package/src/runtime/outbox-transitions.ts +119 -0
- package/src/runtime/status-snapshots.ts +108 -0
- package/src/runtime/status-worker.ts +172 -0
package/src/channel.ts
CHANGED
|
@@ -12,7 +12,6 @@ import {
|
|
|
12
12
|
listAccountIds,
|
|
13
13
|
normalizeAccountId,
|
|
14
14
|
resolveAccount,
|
|
15
|
-
resolveDefaultDisplayName,
|
|
16
15
|
} from './core/accounts.ts';
|
|
17
16
|
import { BncrConfigSchema } from './core/config-schema.ts';
|
|
18
17
|
import {
|
|
@@ -21,18 +20,33 @@ import {
|
|
|
21
20
|
clearOutboundCapability,
|
|
22
21
|
findCapabilityConnection,
|
|
23
22
|
} from './core/connection-capability.ts';
|
|
23
|
+
import {
|
|
24
|
+
getRevalidatedAttemptReason,
|
|
25
|
+
hasAlternativeLiveConnection as hasAlternativeLiveConnectionFromRuntime,
|
|
26
|
+
hasRecentInboundReachability as hasRecentInboundReachabilityFromRuntime,
|
|
27
|
+
isRecentlyReachableConn as isRecentlyReachableConnFromRuntime,
|
|
28
|
+
resolveRecentInboundConnIds as resolveRecentInboundConnIdsFromRuntime,
|
|
29
|
+
} from './core/connection-reachability.ts';
|
|
30
|
+
import { buildDiagnosticsPayload } from './core/diagnostics.ts';
|
|
31
|
+
import { buildDownlinkHealth as buildDownlinkHealthFromRuntime } from './core/downlink-health.ts';
|
|
32
|
+
import { buildExtendedDiagnostics as buildExtendedDiagnosticsFromRuntime } from './core/extended-diagnostics.ts';
|
|
33
|
+
import { buildFileAckKey } from './core/file-ack.ts';
|
|
34
|
+
import {
|
|
35
|
+
buildFileTransferAbortPayload,
|
|
36
|
+
buildFileTransferChunkPayload,
|
|
37
|
+
buildFileTransferCompletePayload,
|
|
38
|
+
buildFileTransferInitPayload,
|
|
39
|
+
} from './core/file-transfer-payloads.ts';
|
|
40
|
+
import {
|
|
41
|
+
matchesTransferOwner as matchesTransferOwnerFromRuntime,
|
|
42
|
+
observeLeaseState,
|
|
43
|
+
} from './core/lease-state.ts';
|
|
44
|
+
import { emitBncrLog, emitBncrLogLine } from './core/logging.ts';
|
|
45
|
+
import { buildOutboxEnqueueDebugInfo } from './core/outbox-enqueue.ts';
|
|
24
46
|
import {
|
|
25
47
|
buildFileTransferOutboxEntry as buildFileTransferOutboxEntryFromRuntime,
|
|
26
48
|
buildTextOutboxEntry as buildTextOutboxEntryFromRuntime,
|
|
27
49
|
} from './core/outbox-entry-builders.ts';
|
|
28
|
-
import { buildOutboxEnqueueDebugInfo } from './core/outbox-enqueue.ts';
|
|
29
|
-
import {
|
|
30
|
-
appendDeadLetter,
|
|
31
|
-
buildDeadLetterEntry,
|
|
32
|
-
collectDueOutboxEntries,
|
|
33
|
-
} from './core/outbox-queue.ts';
|
|
34
|
-
import { resolveFileTransferGuard } from './core/outbox-file-transfer-guards.ts';
|
|
35
|
-
import { prepareFileTransferRouteSelection } from './core/outbox-file-transfer-prep.ts';
|
|
36
50
|
import {
|
|
37
51
|
buildFileTransferPushOkArgs,
|
|
38
52
|
buildFileTransferPushSuccessArgs,
|
|
@@ -41,61 +55,36 @@ import {
|
|
|
41
55
|
buildFileTransferPushFailureArgs,
|
|
42
56
|
resolveFileTransferFailureState,
|
|
43
57
|
} from './core/outbox-file-transfer-failure.ts';
|
|
58
|
+
import { resolveFileTransferGuard } from './core/outbox-file-transfer-guards.ts';
|
|
59
|
+
import { prepareFileTransferRouteSelection } from './core/outbox-file-transfer-prep.ts';
|
|
44
60
|
import {
|
|
45
61
|
buildFileTransferBroadcastPayload,
|
|
46
62
|
buildFileTransferRouteSelectArgs,
|
|
47
63
|
} from './core/outbox-file-transfer-success.ts';
|
|
64
|
+
import {
|
|
65
|
+
appendDeadLetter,
|
|
66
|
+
buildDeadLetterEntry,
|
|
67
|
+
collectDueOutboxEntries,
|
|
68
|
+
} from './core/outbox-queue.ts';
|
|
48
69
|
import { summarizeOutboxEntry } from './core/outbox-summary.ts';
|
|
70
|
+
import { buildTextPushFailureArgs } from './core/outbox-text-push-failure.ts';
|
|
49
71
|
import { resolveTextPushGuard } from './core/outbox-text-push-guards.ts';
|
|
50
72
|
import { prepareTextPushRouteSelection } from './core/outbox-text-push-prep.ts';
|
|
51
|
-
import { buildTextPushFailureArgs } from './core/outbox-text-push-failure.ts';
|
|
52
73
|
import {
|
|
53
74
|
buildTextPushBroadcastPayload,
|
|
54
75
|
buildTextPushOkArgs,
|
|
55
76
|
buildTextPushRouteSelectArgs,
|
|
56
77
|
buildTextPushSuccessArgs,
|
|
57
78
|
} from './core/outbox-text-push-success.ts';
|
|
58
|
-
import {
|
|
59
|
-
getRevalidatedAttemptReason,
|
|
60
|
-
hasAlternativeLiveConnection as hasAlternativeLiveConnectionFromRuntime,
|
|
61
|
-
hasRecentInboundReachability as hasRecentInboundReachabilityFromRuntime,
|
|
62
|
-
isRecentlyReachableConn as isRecentlyReachableConnFromRuntime,
|
|
63
|
-
resolveRecentInboundConnIds as resolveRecentInboundConnIdsFromRuntime,
|
|
64
|
-
} from './core/connection-reachability.ts';
|
|
65
|
-
import { buildDiagnosticsPayload } from './core/diagnostics.ts';
|
|
66
|
-
import { buildDownlinkHealth as buildDownlinkHealthFromRuntime } from './core/downlink-health.ts';
|
|
67
|
-
import { buildExtendedDiagnostics as buildExtendedDiagnosticsFromRuntime } from './core/extended-diagnostics.ts';
|
|
68
|
-
import { observeLeaseState, matchesTransferOwner as matchesTransferOwnerFromRuntime } from './core/lease-state.ts';
|
|
69
|
-
import { emitBncrLog, emitBncrLogLine } from './core/logging.ts';
|
|
70
79
|
import { resolveBncrChannelPolicy, resolveBncrConfigWarnings } from './core/policy.ts';
|
|
71
80
|
import {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
import {
|
|
76
|
-
loadOpenClawWebMedia,
|
|
77
|
-
saveOpenClawChannelMediaBuffer,
|
|
78
|
-
type OpenClawLoadedMedia,
|
|
79
|
-
} from './openclaw/media-runtime.ts';
|
|
80
|
-
import { resolveOpenClawAgentRoute } from './openclaw/routing-runtime.ts';
|
|
81
|
-
import {
|
|
82
|
-
applyOpenClawAccountNameToChannelSection,
|
|
83
|
-
createOpenClawDefaultChannelRuntimeState,
|
|
84
|
-
extractOpenClawToolSend,
|
|
85
|
-
openClawJsonResult,
|
|
86
|
-
readOpenClawBooleanParam,
|
|
87
|
-
readOpenClawJsonFileWithFallback,
|
|
88
|
-
readOpenClawStringParam,
|
|
89
|
-
setOpenClawAccountEnabledInConfigSection,
|
|
90
|
-
writeOpenClawJsonFileAtomically,
|
|
91
|
-
} from './openclaw/sdk-helpers.ts';
|
|
92
|
-
import {
|
|
81
|
+
appendBoundedRegisterTrace,
|
|
82
|
+
buildRegisterDriftSnapshot,
|
|
83
|
+
buildRegisterTraceEntry,
|
|
93
84
|
buildRegisterTraceSummary as buildRegisterTraceSummaryFromEntries,
|
|
94
|
-
classifyRegisterTrace as classifyRegisterTraceFromStack,
|
|
95
85
|
} from './core/register-trace.ts';
|
|
96
86
|
import {
|
|
97
87
|
buildAccountRuntimeSnapshot,
|
|
98
|
-
buildAccountStatusSnapshot,
|
|
99
88
|
buildIntegratedDiagnostics as buildIntegratedDiagnosticsFromRuntime,
|
|
100
89
|
buildStatusHeadlineFromRuntime,
|
|
101
90
|
buildStatusMetaFromRuntime,
|
|
@@ -103,11 +92,9 @@ import {
|
|
|
103
92
|
import {
|
|
104
93
|
buildCanonicalBncrSessionKey,
|
|
105
94
|
formatDisplayScope,
|
|
106
|
-
formatTargetDisplay,
|
|
107
95
|
isLowerHex,
|
|
108
96
|
normalizeInboundSessionKey,
|
|
109
97
|
normalizeStoredSessionKey,
|
|
110
|
-
parseExplicitTarget,
|
|
111
98
|
parseRouteFromDisplayScope,
|
|
112
99
|
parseRouteFromHexScope,
|
|
113
100
|
parseRouteFromScope,
|
|
@@ -127,14 +114,12 @@ import {
|
|
|
127
114
|
reactBncrMessageAction,
|
|
128
115
|
sendBncrReplyAction,
|
|
129
116
|
} from './messaging/outbound/actions.ts';
|
|
130
|
-
import {
|
|
131
|
-
buildBncrMediaOutboundFrame,
|
|
132
|
-
resolveBncrOutboundMessageType,
|
|
133
|
-
} from './messaging/outbound/media.ts';
|
|
134
117
|
import {
|
|
135
118
|
buildEnqueueFromReplyDebugInfo,
|
|
136
119
|
buildFlushDebugInfo,
|
|
137
120
|
buildOutboxAckDebugInfo,
|
|
121
|
+
buildOutboxDrainSkipDebugInfo,
|
|
122
|
+
buildOutboxDrainStuckDebugInfo,
|
|
138
123
|
buildOutboxPushOkDebugInfo,
|
|
139
124
|
buildOutboxPushSkipDebugInfo,
|
|
140
125
|
buildOutboxRouteSelectDebugInfo,
|
|
@@ -143,6 +128,28 @@ import {
|
|
|
143
128
|
buildReplyMediaFallbackDebugInfo,
|
|
144
129
|
buildRetryRerouteDebugInfo,
|
|
145
130
|
} from './messaging/outbound/diagnostics.ts';
|
|
131
|
+
import {
|
|
132
|
+
buildBncrMediaOutboundFrame,
|
|
133
|
+
resolveBncrOutboundMessageType,
|
|
134
|
+
} from './messaging/outbound/media.ts';
|
|
135
|
+
import {
|
|
136
|
+
getOpenClawRuntimeConfig,
|
|
137
|
+
getOpenClawRuntimeConfigOrDefault,
|
|
138
|
+
} from './openclaw/config-runtime.ts';
|
|
139
|
+
import {
|
|
140
|
+
type OpenClawLoadedMedia,
|
|
141
|
+
loadOpenClawWebMedia,
|
|
142
|
+
saveOpenClawChannelMediaBuffer,
|
|
143
|
+
} from './openclaw/media-runtime.ts';
|
|
144
|
+
import { resolveOpenClawAgentRoute } from './openclaw/routing-runtime.ts';
|
|
145
|
+
import {
|
|
146
|
+
extractOpenClawToolSend,
|
|
147
|
+
openClawJsonResult,
|
|
148
|
+
readOpenClawBooleanParam,
|
|
149
|
+
readOpenClawJsonFileWithFallback,
|
|
150
|
+
readOpenClawStringParam,
|
|
151
|
+
writeOpenClawJsonFileAtomically,
|
|
152
|
+
} from './openclaw/sdk-helpers.ts';
|
|
146
153
|
|
|
147
154
|
function buildInboundAcceptedLifecycleDebugInfo(args: {
|
|
148
155
|
stage: 'accepted';
|
|
@@ -276,33 +283,13 @@ function buildInboundResponsePayload(
|
|
|
276
283
|
};
|
|
277
284
|
}
|
|
278
285
|
}
|
|
286
|
+
import { buildBncrDurableQueuedResult } from './messaging/outbound/durable-queue-adapter.ts';
|
|
279
287
|
import {
|
|
280
|
-
buildMediaTextFallback,
|
|
281
288
|
type MediaDedupeCacheEntry,
|
|
289
|
+
buildMediaTextFallback,
|
|
282
290
|
normalizeMessageText,
|
|
283
291
|
normalizeReplyToId,
|
|
284
292
|
} from './messaging/outbound/media-dedupe.ts';
|
|
285
|
-
import {
|
|
286
|
-
buildReplyTextOutboxEntry,
|
|
287
|
-
enqueueNormalizedReplyPayload,
|
|
288
|
-
enqueueReplyMediaFallbackTextEntry,
|
|
289
|
-
enqueueReplyMediaFileTransferEntry,
|
|
290
|
-
enqueueSingleReplyMediaEntry,
|
|
291
|
-
enqueueReplyTextEntry,
|
|
292
|
-
hasReplyMediaEntries,
|
|
293
|
-
normalizeReplyPayload,
|
|
294
|
-
type NormalizedReplyPayload,
|
|
295
|
-
type ReplyMediaEntriesParams,
|
|
296
|
-
type ReplyMediaFileTransferParams,
|
|
297
|
-
type ReplyPayloadInput,
|
|
298
|
-
} from './messaging/outbound/reply-enqueue.ts';
|
|
299
|
-
import {
|
|
300
|
-
OUTBOUND_DEGRADE_REASON,
|
|
301
|
-
OUTBOUND_FLUSH_REASON,
|
|
302
|
-
OUTBOUND_FLUSH_TRIGGER,
|
|
303
|
-
OUTBOUND_SCHEDULE_SOURCE,
|
|
304
|
-
OUTBOUND_TERMINAL_REASON,
|
|
305
|
-
} from './messaging/outbound/reasons.ts';
|
|
306
293
|
import {
|
|
307
294
|
buildOutboxOnlineDebugInfo,
|
|
308
295
|
clampOutboxDrainDelay,
|
|
@@ -315,17 +302,73 @@ import {
|
|
|
315
302
|
selectOutboxTargetAccounts,
|
|
316
303
|
updateMinOutboxDelay,
|
|
317
304
|
} from './messaging/outbound/queue-selectors.ts';
|
|
305
|
+
import {
|
|
306
|
+
OUTBOUND_DEGRADE_REASON,
|
|
307
|
+
OUTBOUND_FLUSH_REASON,
|
|
308
|
+
OUTBOUND_FLUSH_TRIGGER,
|
|
309
|
+
OUTBOUND_SCHEDULE_SOURCE,
|
|
310
|
+
OUTBOUND_TERMINAL_REASON,
|
|
311
|
+
} from './messaging/outbound/reasons.ts';
|
|
312
|
+
import {
|
|
313
|
+
type NormalizedReplyPayload,
|
|
314
|
+
type ReplyMediaEntriesParams,
|
|
315
|
+
type ReplyMediaFileTransferParams,
|
|
316
|
+
type ReplyPayloadInput,
|
|
317
|
+
buildReplyTextOutboxEntry,
|
|
318
|
+
enqueueNormalizedReplyPayload,
|
|
319
|
+
enqueueReplyMediaFallbackTextEntry,
|
|
320
|
+
enqueueReplyMediaFileTransferEntry,
|
|
321
|
+
enqueueReplyTextEntry,
|
|
322
|
+
enqueueSingleReplyMediaEntry,
|
|
323
|
+
hasReplyMediaEntries,
|
|
324
|
+
normalizeReplyPayload,
|
|
325
|
+
} from './messaging/outbound/reply-enqueue.ts';
|
|
318
326
|
import {
|
|
319
327
|
computePushFailureDecision,
|
|
320
328
|
computeRetryRerouteDecision,
|
|
321
329
|
} from './messaging/outbound/retry-policy.ts';
|
|
322
330
|
import { sendBncrMedia, sendBncrText } from './messaging/outbound/send.ts';
|
|
323
|
-
import {
|
|
324
|
-
import {
|
|
331
|
+
import { BNCR_CHANNEL_CAPABILITIES } from './plugin/capabilities.ts';
|
|
332
|
+
import { BNCR_CONFIG_SURFACE } from './plugin/config.ts';
|
|
333
|
+
import { BNCR_GATEWAY_METHODS } from './plugin/gateway-methods.ts';
|
|
334
|
+
import { createBncrGatewayRuntime } from './plugin/gateway-runtime.ts';
|
|
335
|
+
import { BNCR_MESSAGE_RECEIVE_POLICY } from './plugin/message-policy.ts';
|
|
336
|
+
import { createBncrMessageSend } from './plugin/message-send.ts';
|
|
337
|
+
import { createBncrMessagingSurface } from './plugin/messaging.ts';
|
|
338
|
+
import { BNCR_CHANNEL_META } from './plugin/meta.ts';
|
|
339
|
+
import { createBncrOutboundRuntime } from './plugin/outbound.ts';
|
|
340
|
+
import { BNCR_SETUP_SURFACE } from './plugin/setup.ts';
|
|
341
|
+
import { createBncrStatusSurface } from './plugin/status.ts';
|
|
342
|
+
import {
|
|
343
|
+
pruneLogDedupeState as pruneLogDedupeStateFromRuntime,
|
|
344
|
+
shouldEmitDedupLog as shouldEmitDedupLogFromRuntime,
|
|
345
|
+
} from './runtime/log-dedupe.ts';
|
|
346
|
+
import {
|
|
347
|
+
buildBncrRuntimeAckStrategy,
|
|
348
|
+
computeBncrRecommendedAckTimeoutMs,
|
|
349
|
+
computeBncrRecommendedAckTimeoutReason,
|
|
350
|
+
} from './runtime/outbound-ack-timeout.ts';
|
|
351
|
+
import {
|
|
352
|
+
buildBncrRuntimeFlags,
|
|
353
|
+
buildBncrRuntimeStatusInput,
|
|
354
|
+
resolveBncrOutboundAckRequired,
|
|
355
|
+
} from './runtime/outbound-flags.ts';
|
|
325
356
|
import {
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
357
|
+
applyBncrPushFailureDecisionToEntry,
|
|
358
|
+
applyBncrRetryRerouteDecisionToEntry,
|
|
359
|
+
buildBncrAckOkTelemetryPatch,
|
|
360
|
+
buildBncrAckRetryEntryPatch,
|
|
361
|
+
buildBncrOutboxFailureEntryPatch,
|
|
362
|
+
buildBncrOutboxPushSuccessEntryPatch,
|
|
363
|
+
} from './runtime/outbox-transitions.ts';
|
|
364
|
+
import { buildRuntimeStatusSnapshots } from './runtime/status-snapshots.ts';
|
|
365
|
+
import {
|
|
366
|
+
type ChannelAccountWorkerHandle,
|
|
367
|
+
clearAllBncrStatusWorkers,
|
|
368
|
+
clearBncrStatusWorker,
|
|
369
|
+
startBncrStatusWorker,
|
|
370
|
+
stopBncrStatusWorker,
|
|
371
|
+
} from './runtime/status-worker.ts';
|
|
329
372
|
const BRIDGE_VERSION = 2;
|
|
330
373
|
const BNCR_PUSH_EVENT = 'plugin.bncr.push';
|
|
331
374
|
const BNCR_FILE_INIT_EVENT = 'plugin.bncr.file.init';
|
|
@@ -341,6 +384,9 @@ const MAX_ACCOUNT_ACTIVITY_ENTRIES = 1000;
|
|
|
341
384
|
const PUSH_DRAIN_INTERVAL_MS = 500;
|
|
342
385
|
const PUSH_DRAIN_ACCOUNT_BUDGET = 5;
|
|
343
386
|
const PUSH_DRAIN_ACCOUNT_TIME_BUDGET_MS = 2_000;
|
|
387
|
+
const PUSH_DRAIN_EXCEPTION_RETRY_LIMIT = 3;
|
|
388
|
+
const PUSH_DRAIN_EXCEPTION_RETRY_DELAY_MS = 1_000;
|
|
389
|
+
const PUSH_DRAIN_STUCK_WARN_MS = 30_000;
|
|
344
390
|
const PUSH_ACK_TIMEOUT_MS = 30_000;
|
|
345
391
|
const ADAPTIVE_ACK_TIMEOUT_DEFAULT_ENABLED = true;
|
|
346
392
|
const RECOMMENDED_ACK_TIMEOUT_MIN_MS = PUSH_ACK_TIMEOUT_MS;
|
|
@@ -351,12 +397,11 @@ const ADAPTIVE_ACK_TIMEOUT_LOG_THROTTLE_MS = 5 * 60 * 1000;
|
|
|
351
397
|
const OUTBOUND_READY_TTL_MS = 30_000;
|
|
352
398
|
const PREFERRED_OUTBOUND_TTL_MS = 12_000;
|
|
353
399
|
const FILE_FORCE_CHUNK = true; // 统一走 WS 分块,保留 base64 仅作兜底
|
|
354
|
-
const LOG_DEDUPE_STATE_TTL_MS = 10 * 60 * 1000;
|
|
355
|
-
const LOG_DEDUPE_STATE_MAX_ENTRIES = 1_000;
|
|
356
400
|
const FILE_INLINE_THRESHOLD = 5 * 1024 * 1024; // fallback 阈值(仅 FILE_FORCE_CHUNK=false 时生效)
|
|
357
401
|
const FILE_CHUNK_SIZE = 256 * 1024; // 256KB
|
|
358
402
|
const INBOUND_FILE_TRANSFER_MAX_BYTES = 50 * 1024 * 1024;
|
|
359
|
-
const INBOUND_FILE_TRANSFER_MAX_CHUNKS =
|
|
403
|
+
const INBOUND_FILE_TRANSFER_MAX_CHUNKS =
|
|
404
|
+
Math.ceil(INBOUND_FILE_TRANSFER_MAX_BYTES / FILE_CHUNK_SIZE) + 1;
|
|
360
405
|
const FILE_CHUNK_RETRY = 3;
|
|
361
406
|
const FILE_ACK_TIMEOUT_MS = 30_000;
|
|
362
407
|
const FILE_TRANSFER_ACK_TTL_MS = 30_000;
|
|
@@ -389,12 +434,6 @@ type FileSendTransferState = {
|
|
|
389
434
|
error?: string;
|
|
390
435
|
};
|
|
391
436
|
|
|
392
|
-
type ChannelAccountWorkerHandle = {
|
|
393
|
-
timer: NodeJS.Timeout;
|
|
394
|
-
finish: (reason: string) => void;
|
|
395
|
-
cleanupAbortListener?: () => void;
|
|
396
|
-
};
|
|
397
|
-
|
|
398
437
|
type FileRecvTransferState = {
|
|
399
438
|
transferId: string;
|
|
400
439
|
accountId: string;
|
|
@@ -528,7 +567,6 @@ function normalizeBncrSendParams(input: {
|
|
|
528
567
|
};
|
|
529
568
|
}
|
|
530
569
|
|
|
531
|
-
|
|
532
570
|
function now() {
|
|
533
571
|
return Date.now();
|
|
534
572
|
}
|
|
@@ -725,7 +763,10 @@ class BncrBridgeRuntime {
|
|
|
725
763
|
private lastLateAckQueueLatencyMsByAccount = new Map<string, number>();
|
|
726
764
|
private lastLateAckPushLatencyMsByAccount = new Map<string, number>();
|
|
727
765
|
private adaptiveAckRecoveryOkCountByAccount = new Map<string, number>();
|
|
728
|
-
private adaptiveAckTimeoutLogStateByAccount = new Map<
|
|
766
|
+
private adaptiveAckTimeoutLogStateByAccount = new Map<
|
|
767
|
+
string,
|
|
768
|
+
{ at: number; timeoutMs: number; reason: string }
|
|
769
|
+
>();
|
|
729
770
|
private channelAccountWorkers = new Map<string, ChannelAccountWorkerHandle>();
|
|
730
771
|
private logDedupeState = new Map<string, { at: number; sig: string }>();
|
|
731
772
|
private canonicalAgentId: string | null = null;
|
|
@@ -743,6 +784,9 @@ class BncrBridgeRuntime {
|
|
|
743
784
|
private saveTimer: NodeJS.Timeout | null = null;
|
|
744
785
|
private pushTimer: NodeJS.Timeout | null = null;
|
|
745
786
|
private pushDrainRunningAccounts = new Set<string>();
|
|
787
|
+
private pushDrainRunningSinceByAccount = new Map<string, number>();
|
|
788
|
+
private pushDrainStuckWarnedAtByAccount = new Map<string, number>();
|
|
789
|
+
private pushDrainExceptionRetryCount = 0;
|
|
746
790
|
private messageAckWaiters = new Map<
|
|
747
791
|
// Refactor boundary note (message ACK runtime):
|
|
748
792
|
// These waiters are part of the outbound message-ack lifecycle, not just a utility map.
|
|
@@ -837,27 +881,17 @@ class BncrBridgeRuntime {
|
|
|
837
881
|
}
|
|
838
882
|
|
|
839
883
|
private pruneLogDedupeState(currentTime = now()) {
|
|
840
|
-
|
|
841
|
-
if (currentTime - entry.at > LOG_DEDUPE_STATE_TTL_MS) {
|
|
842
|
-
this.logDedupeState.delete(key);
|
|
843
|
-
}
|
|
844
|
-
}
|
|
845
|
-
|
|
846
|
-
while (this.logDedupeState.size > LOG_DEDUPE_STATE_MAX_ENTRIES) {
|
|
847
|
-
const oldestKey = this.logDedupeState.keys().next().value;
|
|
848
|
-
if (!oldestKey) break;
|
|
849
|
-
this.logDedupeState.delete(oldestKey);
|
|
850
|
-
}
|
|
884
|
+
pruneLogDedupeStateFromRuntime(this.logDedupeState, currentTime);
|
|
851
885
|
}
|
|
852
886
|
|
|
853
887
|
private shouldEmitDedupLog(key: string, sig: string, windowMs = 5 * 60 * 1000) {
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
888
|
+
return shouldEmitDedupLogFromRuntime({
|
|
889
|
+
state: this.logDedupeState,
|
|
890
|
+
key,
|
|
891
|
+
sig,
|
|
892
|
+
nowMs: now(),
|
|
893
|
+
windowMs,
|
|
894
|
+
});
|
|
861
895
|
}
|
|
862
896
|
|
|
863
897
|
private logInfoDedup(
|
|
@@ -954,39 +988,64 @@ class BncrBridgeRuntime {
|
|
|
954
988
|
);
|
|
955
989
|
}
|
|
956
990
|
|
|
991
|
+
private buildStatusWorkerRuntime() {
|
|
992
|
+
return {
|
|
993
|
+
workers: this.channelAccountWorkers,
|
|
994
|
+
bridgeId: this.bridgeId,
|
|
995
|
+
hooks: {
|
|
996
|
+
isOnline: (accountId: string) => this.isOnline(accountId),
|
|
997
|
+
hasRecentInboundReachability: (accountId: string) =>
|
|
998
|
+
this.hasRecentInboundReachability(accountId),
|
|
999
|
+
getLastActivityAt: (accountId: string, previous: Record<string, any>) =>
|
|
1000
|
+
this.lastActivityByAccount.get(accountId) ||
|
|
1001
|
+
this.lastInboundByAccount.get(accountId) ||
|
|
1002
|
+
this.lastOutboundByAccount.get(accountId) ||
|
|
1003
|
+
previous?.lastEventAt ||
|
|
1004
|
+
null,
|
|
1005
|
+
getActiveConnectionKey: (accountId: string) =>
|
|
1006
|
+
this.activeConnectionByAccount.get(accountId) || null,
|
|
1007
|
+
getActiveConnections: (accountId: string) =>
|
|
1008
|
+
Array.from(this.connections.values())
|
|
1009
|
+
.filter((c) => c.accountId === accountId)
|
|
1010
|
+
.map((c) => ({
|
|
1011
|
+
connId: c.connId,
|
|
1012
|
+
clientId: c.clientId,
|
|
1013
|
+
inboundOnly: c.inboundOnly === true,
|
|
1014
|
+
outboundReady: c.outboundReady === true,
|
|
1015
|
+
preferredForOutbound: c.preferredForOutbound === true,
|
|
1016
|
+
})),
|
|
1017
|
+
buildStatusMeta: (accountId: string) => this.buildStatusMeta(accountId),
|
|
1018
|
+
logInfo: (scope: string | undefined, message: string, options?: { debugOnly?: boolean }) =>
|
|
1019
|
+
this.logInfo(scope, message, options),
|
|
1020
|
+
logInfoDedup: (
|
|
1021
|
+
scope: string | undefined,
|
|
1022
|
+
message: string,
|
|
1023
|
+
options: { key: string; sig: string; debugOnly?: boolean; windowMs?: number },
|
|
1024
|
+
) => this.logInfoDedup(scope, message, options),
|
|
1025
|
+
},
|
|
1026
|
+
};
|
|
1027
|
+
}
|
|
1028
|
+
|
|
957
1029
|
private clearChannelAccountWorker(accountId: string, reason: string) {
|
|
958
|
-
|
|
959
|
-
if (!worker) return false;
|
|
960
|
-
worker.finish(reason);
|
|
961
|
-
this.logInfo(
|
|
962
|
-
'health',
|
|
963
|
-
`status-worker cleared ${JSON.stringify({ bridge: this.bridgeId, accountId, reason })}`,
|
|
964
|
-
{ debugOnly: true },
|
|
965
|
-
);
|
|
966
|
-
return true;
|
|
1030
|
+
return clearBncrStatusWorker(this.buildStatusWorkerRuntime(), accountId, reason);
|
|
967
1031
|
}
|
|
968
1032
|
|
|
969
1033
|
private clearAllChannelAccountWorkers(reason: string) {
|
|
970
|
-
|
|
971
|
-
this.clearChannelAccountWorker(accountId, reason);
|
|
972
|
-
}
|
|
1034
|
+
clearAllBncrStatusWorkers(this.buildStatusWorkerRuntime(), reason);
|
|
973
1035
|
}
|
|
974
1036
|
|
|
975
1037
|
private captureDriftSnapshot(
|
|
976
1038
|
summary: ReturnType<BncrBridgeRuntime['buildRegisterTraceSummary']>,
|
|
977
1039
|
) {
|
|
978
|
-
this.lastDriftSnapshot = {
|
|
1040
|
+
this.lastDriftSnapshot = buildRegisterDriftSnapshot({
|
|
979
1041
|
capturedAt: now(),
|
|
980
1042
|
registerCount: this.registerCount,
|
|
981
1043
|
apiGeneration: this.apiGeneration,
|
|
982
|
-
|
|
1044
|
+
summary,
|
|
983
1045
|
apiInstanceId: this.lastApiInstanceId,
|
|
984
1046
|
registryFingerprint: this.lastRegistryFingerprint,
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
traceWindowSize: this.registerTraceRecent.length,
|
|
988
|
-
traceRecent: this.registerTraceRecent.map((trace) => ({ ...trace })),
|
|
989
|
-
};
|
|
1047
|
+
traceRecent: this.registerTraceRecent,
|
|
1048
|
+
});
|
|
990
1049
|
this.scheduleSave();
|
|
991
1050
|
}
|
|
992
1051
|
|
|
@@ -1026,9 +1085,7 @@ class BncrBridgeRuntime {
|
|
|
1026
1085
|
.map((line) => line.trim())
|
|
1027
1086
|
.filter(Boolean)
|
|
1028
1087
|
.join(' <- ');
|
|
1029
|
-
const
|
|
1030
|
-
|
|
1031
|
-
const trace = {
|
|
1088
|
+
const trace = buildRegisterTraceEntry({
|
|
1032
1089
|
ts,
|
|
1033
1090
|
bridgeId: this.bridgeId,
|
|
1034
1091
|
gatewayPid: this.gatewayPid,
|
|
@@ -1040,11 +1097,8 @@ class BncrBridgeRuntime {
|
|
|
1040
1097
|
source: this.pluginSource,
|
|
1041
1098
|
pluginVersion: this.pluginVersion,
|
|
1042
1099
|
stack,
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
this.registerTraceRecent.push(trace);
|
|
1046
|
-
if (this.registerTraceRecent.length > 12)
|
|
1047
|
-
this.registerTraceRecent.splice(0, this.registerTraceRecent.length - 12);
|
|
1100
|
+
});
|
|
1101
|
+
appendBoundedRegisterTrace(this.registerTraceRecent, trace, 12);
|
|
1048
1102
|
|
|
1049
1103
|
const summary = this.buildRegisterTraceSummary();
|
|
1050
1104
|
if (summary.postWarmupRegisterCount > 0) this.captureDriftSnapshot(summary);
|
|
@@ -1147,11 +1201,29 @@ class BncrBridgeRuntime {
|
|
|
1147
1201
|
return matchesTransferOwnerFromRuntime(params);
|
|
1148
1202
|
}
|
|
1149
1203
|
|
|
1204
|
+
private buildRuntimeSurfaceDiagnostics() {
|
|
1205
|
+
const channelRuntime = (this.api as any)?.runtime?.channel;
|
|
1206
|
+
const surfaces = {
|
|
1207
|
+
inbound: Boolean(channelRuntime?.inbound),
|
|
1208
|
+
media: Boolean(channelRuntime?.media),
|
|
1209
|
+
reply: Boolean(channelRuntime?.reply),
|
|
1210
|
+
routing: Boolean(channelRuntime?.routing),
|
|
1211
|
+
session: Boolean(channelRuntime?.session),
|
|
1212
|
+
};
|
|
1213
|
+
return {
|
|
1214
|
+
channel: surfaces,
|
|
1215
|
+
missing: Object.entries(surfaces)
|
|
1216
|
+
.filter(([, present]) => !present)
|
|
1217
|
+
.map(([name]) => name),
|
|
1218
|
+
};
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1150
1221
|
private buildExtendedDiagnostics(accountId: string) {
|
|
1151
1222
|
const acc = normalizeAccountId(accountId);
|
|
1152
1223
|
const diagnostics = this.buildIntegratedDiagnostics(acc) as Record<string, any>;
|
|
1153
1224
|
return buildExtendedDiagnosticsFromRuntime({
|
|
1154
1225
|
diagnostics,
|
|
1226
|
+
runtimeSurface: this.buildRuntimeSurfaceDiagnostics(),
|
|
1155
1227
|
register: {
|
|
1156
1228
|
bridgeId: this.bridgeId,
|
|
1157
1229
|
gatewayPid: this.gatewayPid,
|
|
@@ -1745,7 +1817,9 @@ class BncrBridgeRuntime {
|
|
|
1745
1817
|
const primaryKey = this.activeConnectionByAccount.get(acc);
|
|
1746
1818
|
const primary = primaryKey ? this.connections.get(primaryKey) : null;
|
|
1747
1819
|
|
|
1748
|
-
const isEligible = (
|
|
1820
|
+
const isEligible = (
|
|
1821
|
+
conn: BncrConnection | null | undefined,
|
|
1822
|
+
): conn is BncrConnection & {
|
|
1749
1823
|
outboundReadyUntil?: number;
|
|
1750
1824
|
preferredForOutboundUntil?: number;
|
|
1751
1825
|
inboundOnly?: boolean;
|
|
@@ -1790,10 +1864,13 @@ class BncrBridgeRuntime {
|
|
|
1790
1864
|
const sb = candidateScore(b);
|
|
1791
1865
|
if (sb.preferred !== sa.preferred) return sb.preferred - sa.preferred;
|
|
1792
1866
|
if (sb.ready !== sa.ready) return sb.ready - sa.ready;
|
|
1793
|
-
if (sa.recentTimeoutPenalty !== sb.recentTimeoutPenalty)
|
|
1794
|
-
|
|
1867
|
+
if (sa.recentTimeoutPenalty !== sb.recentTimeoutPenalty)
|
|
1868
|
+
return sa.recentTimeoutPenalty - sb.recentTimeoutPenalty;
|
|
1869
|
+
if (sa.pushFailureScore !== sb.pushFailureScore)
|
|
1870
|
+
return sa.pushFailureScore - sb.pushFailureScore;
|
|
1795
1871
|
if (sb.lastAckOkAt !== sa.lastAckOkAt) return sb.lastAckOkAt - sa.lastAckOkAt;
|
|
1796
|
-
if (sa.lastPushTimeoutAt !== sb.lastPushTimeoutAt)
|
|
1872
|
+
if (sa.lastPushTimeoutAt !== sb.lastPushTimeoutAt)
|
|
1873
|
+
return sa.lastPushTimeoutAt - sb.lastPushTimeoutAt;
|
|
1797
1874
|
if (sb.recentInbound !== sa.recentInbound) return sb.recentInbound - sa.recentInbound;
|
|
1798
1875
|
if (sb.lastSeenAt !== sa.lastSeenAt) return sb.lastSeenAt - sa.lastSeenAt;
|
|
1799
1876
|
return sb.connectedAt - sa.connectedAt;
|
|
@@ -1841,7 +1918,9 @@ class BncrBridgeRuntime {
|
|
|
1841
1918
|
const t = now();
|
|
1842
1919
|
const connIds = new Set<string>();
|
|
1843
1920
|
|
|
1844
|
-
const isEligible = (
|
|
1921
|
+
const isEligible = (
|
|
1922
|
+
conn: BncrConnection | null | undefined,
|
|
1923
|
+
): conn is BncrConnection & {
|
|
1845
1924
|
outboundReadyUntil?: number;
|
|
1846
1925
|
preferredForOutboundUntil?: number;
|
|
1847
1926
|
inboundOnly?: boolean;
|
|
@@ -1889,10 +1968,13 @@ class BncrBridgeRuntime {
|
|
|
1889
1968
|
const sb = candidateScore(b);
|
|
1890
1969
|
if (sb.preferred !== sa.preferred) return sb.preferred - sa.preferred;
|
|
1891
1970
|
if (sb.ready !== sa.ready) return sb.ready - sa.ready;
|
|
1892
|
-
if (sa.recentTimeoutPenalty !== sb.recentTimeoutPenalty)
|
|
1893
|
-
|
|
1971
|
+
if (sa.recentTimeoutPenalty !== sb.recentTimeoutPenalty)
|
|
1972
|
+
return sa.recentTimeoutPenalty - sb.recentTimeoutPenalty;
|
|
1973
|
+
if (sa.pushFailureScore !== sb.pushFailureScore)
|
|
1974
|
+
return sa.pushFailureScore - sb.pushFailureScore;
|
|
1894
1975
|
if (sb.lastAckOkAt !== sa.lastAckOkAt) return sb.lastAckOkAt - sa.lastAckOkAt;
|
|
1895
|
-
if (sa.lastPushTimeoutAt !== sb.lastPushTimeoutAt)
|
|
1976
|
+
if (sa.lastPushTimeoutAt !== sb.lastPushTimeoutAt)
|
|
1977
|
+
return sa.lastPushTimeoutAt - sb.lastPushTimeoutAt;
|
|
1896
1978
|
if (sb.recentInbound !== sa.recentInbound) return sb.recentInbound - sa.recentInbound;
|
|
1897
1979
|
if (sb.lastSeenAt !== sa.lastSeenAt) return sb.lastSeenAt - sa.lastSeenAt;
|
|
1898
1980
|
return sb.connectedAt - sa.connectedAt;
|
|
@@ -1976,10 +2058,7 @@ class BncrBridgeRuntime {
|
|
|
1976
2058
|
|
|
1977
2059
|
private tryAdoptTransferOwner(args: {
|
|
1978
2060
|
accountId: string;
|
|
1979
|
-
transfer:
|
|
1980
|
-
| FileSendTransferState
|
|
1981
|
-
| FileRecvTransferState
|
|
1982
|
-
| undefined;
|
|
2061
|
+
transfer: FileSendTransferState | FileRecvTransferState | undefined;
|
|
1983
2062
|
connId: string;
|
|
1984
2063
|
clientId?: string;
|
|
1985
2064
|
}): boolean {
|
|
@@ -2111,6 +2190,7 @@ class BncrBridgeRuntime {
|
|
|
2111
2190
|
this.recordOutboxPrePushFailure({
|
|
2112
2191
|
entry: args.entry,
|
|
2113
2192
|
lastError: args.guard.lastError,
|
|
2193
|
+
persist: true,
|
|
2114
2194
|
});
|
|
2115
2195
|
if (args.guard.reason === 'media-url-missing') {
|
|
2116
2196
|
this.logOutboxPushFailure({
|
|
@@ -2126,9 +2206,12 @@ class BncrBridgeRuntime {
|
|
|
2126
2206
|
messageId: args.entry.messageId,
|
|
2127
2207
|
accountId: args.entry.accountId,
|
|
2128
2208
|
kind: 'file-transfer',
|
|
2129
|
-
reason:
|
|
2209
|
+
reason:
|
|
2210
|
+
args.guard.reason === 'no-gateway-context' ? 'no-gateway-context' : 'no-active-connection',
|
|
2130
2211
|
recentInboundReachable:
|
|
2131
|
-
args.guard.reason === 'no-active-connection'
|
|
2212
|
+
args.guard.reason === 'no-active-connection'
|
|
2213
|
+
? args.guard.recentInboundReachable
|
|
2214
|
+
: undefined,
|
|
2132
2215
|
});
|
|
2133
2216
|
}
|
|
2134
2217
|
|
|
@@ -2421,12 +2504,24 @@ class BncrBridgeRuntime {
|
|
|
2421
2504
|
routeSelection: selection,
|
|
2422
2505
|
});
|
|
2423
2506
|
if (!guard.ok) {
|
|
2507
|
+
this.recordOutboxPrePushFailure({
|
|
2508
|
+
entry,
|
|
2509
|
+
lastError:
|
|
2510
|
+
guard.reason === 'no-gateway-context'
|
|
2511
|
+
? 'gateway context unavailable'
|
|
2512
|
+
: 'no active bncr client',
|
|
2513
|
+
persist: true,
|
|
2514
|
+
});
|
|
2424
2515
|
this.logOutboxPushSkip({
|
|
2425
2516
|
messageId: entry.messageId,
|
|
2426
2517
|
accountId: entry.accountId,
|
|
2427
2518
|
reason: guard.reason,
|
|
2428
2519
|
recentInboundReachable:
|
|
2429
2520
|
guard.reason === 'no-active-connection' ? guard.recentInboundReachable : undefined,
|
|
2521
|
+
routeReason: selection.routeReason,
|
|
2522
|
+
connIds: selection.connIds,
|
|
2523
|
+
ownerConnId: selection.ownerConnId,
|
|
2524
|
+
ownerClientId: owner?.clientId,
|
|
2430
2525
|
});
|
|
2431
2526
|
return false;
|
|
2432
2527
|
}
|
|
@@ -2458,10 +2553,24 @@ class BncrBridgeRuntime {
|
|
|
2458
2553
|
kind?: 'file-transfer';
|
|
2459
2554
|
reason: string;
|
|
2460
2555
|
recentInboundReachable?: boolean;
|
|
2556
|
+
routeReason?: string;
|
|
2557
|
+
connIds?: Iterable<string>;
|
|
2558
|
+
ownerConnId?: string;
|
|
2559
|
+
ownerClientId?: string;
|
|
2461
2560
|
}) {
|
|
2561
|
+
this.logInfo(
|
|
2562
|
+
'outbox push skip',
|
|
2563
|
+
`mid=${args.messageId}|q=${this.outbox.size}|reason=${args.reason}${args.kind ? `|kind=${args.kind}` : ''}`,
|
|
2564
|
+
);
|
|
2462
2565
|
this.logInfo(
|
|
2463
2566
|
'outbox',
|
|
2464
|
-
`push-skip ${JSON.stringify(
|
|
2567
|
+
`push-skip ${JSON.stringify(
|
|
2568
|
+
buildOutboxPushSkipDebugInfo({
|
|
2569
|
+
...args,
|
|
2570
|
+
activeConnectionCount: this.activeConnectionCount(args.accountId),
|
|
2571
|
+
connections: this.connections.values(),
|
|
2572
|
+
}),
|
|
2573
|
+
)}`,
|
|
2465
2574
|
{ debugOnly: true },
|
|
2466
2575
|
);
|
|
2467
2576
|
}
|
|
@@ -2492,11 +2601,9 @@ class BncrBridgeRuntime {
|
|
|
2492
2601
|
retryable?: boolean;
|
|
2493
2602
|
lastError?: string;
|
|
2494
2603
|
}) {
|
|
2495
|
-
this.logInfo(
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
{ debugOnly: true },
|
|
2499
|
-
);
|
|
2604
|
+
this.logInfo('outbox', `push-fail ${JSON.stringify(buildPushFailureDebugInfo(args))}`, {
|
|
2605
|
+
debugOnly: true,
|
|
2606
|
+
});
|
|
2500
2607
|
}
|
|
2501
2608
|
|
|
2502
2609
|
private logOutboxPushOkSummary(messageId: string) {
|
|
@@ -2549,14 +2656,18 @@ class BncrBridgeRuntime {
|
|
|
2549
2656
|
sessionKey: args.entry.sessionKey,
|
|
2550
2657
|
to: formatDisplayScope(args.entry.route),
|
|
2551
2658
|
kind:
|
|
2552
|
-
isPlainObject(args.entry.payload?._meta) &&
|
|
2659
|
+
isPlainObject(args.entry.payload?._meta) &&
|
|
2660
|
+
args.entry.payload?._meta?.kind === 'file-transfer'
|
|
2553
2661
|
? 'file-transfer'
|
|
2554
2662
|
: undefined,
|
|
2555
2663
|
requireAck: args.requireAck,
|
|
2556
2664
|
ackResult: args.ackResult,
|
|
2557
2665
|
ackStage: 'message',
|
|
2558
2666
|
ackOutcome: args.ackResult,
|
|
2559
|
-
reason:
|
|
2667
|
+
reason:
|
|
2668
|
+
args.ackResult === 'timeout'
|
|
2669
|
+
? OUTBOUND_TERMINAL_REASON.PUSH_ACK_TIMEOUT
|
|
2670
|
+
: 'message-acked',
|
|
2560
2671
|
ackTimeoutMs: typeof args.ackTimeoutMs === 'number' ? args.ackTimeoutMs : undefined,
|
|
2561
2672
|
adaptiveAckTimeoutEnabled: ADAPTIVE_ACK_TIMEOUT_DEFAULT_ENABLED,
|
|
2562
2673
|
onlineNow: args.onlineNow,
|
|
@@ -2581,16 +2692,13 @@ class BncrBridgeRuntime {
|
|
|
2581
2692
|
localNextDelay: number | null;
|
|
2582
2693
|
ackTimeoutMs?: number | null;
|
|
2583
2694
|
}) {
|
|
2584
|
-
this.logOutboxAckSummary(
|
|
2585
|
-
args.
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
waitMs: args.requireAck ? args.ackTimeoutMs : undefined,
|
|
2592
|
-
},
|
|
2593
|
-
);
|
|
2695
|
+
this.logOutboxAckSummary(args.requireAck ? 'outbox ack timeout' : 'outbox ack retry', {
|
|
2696
|
+
messageId: args.entry.messageId,
|
|
2697
|
+
connId: args.entry.lastPushConnId,
|
|
2698
|
+
clientId: args.entry.lastPushClientId,
|
|
2699
|
+
err: args.requireAck ? undefined : args.entry.lastError,
|
|
2700
|
+
waitMs: args.requireAck ? args.ackTimeoutMs : undefined,
|
|
2701
|
+
});
|
|
2594
2702
|
this.logInfo(
|
|
2595
2703
|
'outbox',
|
|
2596
2704
|
`retry-reroute ${JSON.stringify(
|
|
@@ -2626,12 +2734,7 @@ class BncrBridgeRuntime {
|
|
|
2626
2734
|
stale: boolean,
|
|
2627
2735
|
result: { ok: true; movedToDeadLetter?: true; willRetry?: true },
|
|
2628
2736
|
) {
|
|
2629
|
-
respond(
|
|
2630
|
-
true,
|
|
2631
|
-
stale
|
|
2632
|
-
? { ...result, stale: true, staleAccepted: true }
|
|
2633
|
-
: result,
|
|
2634
|
-
);
|
|
2737
|
+
respond(true, stale ? { ...result, stale: true, staleAccepted: true } : result);
|
|
2635
2738
|
}
|
|
2636
2739
|
|
|
2637
2740
|
private prepareAckHandling(args: {
|
|
@@ -2733,19 +2836,18 @@ class BncrBridgeRuntime {
|
|
|
2733
2836
|
outboundReady: true,
|
|
2734
2837
|
preferredForOutbound: true,
|
|
2735
2838
|
});
|
|
2736
|
-
const
|
|
2839
|
+
const telemetryPatch = buildBncrAckOkTelemetryPatch({
|
|
2840
|
+
entry: args.entry,
|
|
2841
|
+
ackAt: now(),
|
|
2842
|
+
defaultAckTimeoutMs: PUSH_ACK_TIMEOUT_MS,
|
|
2843
|
+
});
|
|
2844
|
+
const { ackAt, ackQueueLatencyMs, ackPushLatencyMs, lateAccepted } = telemetryPatch;
|
|
2737
2845
|
this.lastAckOkByAccount.set(args.accountId, ackAt);
|
|
2738
|
-
const ackQueueLatencyMs = Math.max(0, ackAt - finiteNumberOr(args.entry.createdAt, ackAt));
|
|
2739
|
-
const ackPushLatencyMs =
|
|
2740
|
-
typeof args.entry.lastPushAt === 'number'
|
|
2741
|
-
? Math.max(0, ackAt - args.entry.lastPushAt)
|
|
2742
|
-
: null;
|
|
2743
2846
|
this.lastAckQueueLatencyMsByAccount.set(args.accountId, ackQueueLatencyMs);
|
|
2744
2847
|
if (typeof ackPushLatencyMs === 'number') {
|
|
2745
2848
|
this.lastAckPushLatencyMsByAccount.set(args.accountId, ackPushLatencyMs);
|
|
2746
2849
|
}
|
|
2747
|
-
|
|
2748
|
-
if (lateAccepted) {
|
|
2850
|
+
if (telemetryPatch.shouldResetAdaptiveAckRecovery) {
|
|
2749
2851
|
this.adaptiveAckRecoveryOkCountByAccount.set(args.accountId, 0);
|
|
2750
2852
|
this.lateAckOkCountByAccount.set(
|
|
2751
2853
|
args.accountId,
|
|
@@ -2758,7 +2860,7 @@ class BncrBridgeRuntime {
|
|
|
2758
2860
|
}
|
|
2759
2861
|
args.entry.awaitingRetryPush = false;
|
|
2760
2862
|
args.entry.lastError = undefined;
|
|
2761
|
-
} else if (
|
|
2863
|
+
} else if (telemetryPatch.shouldIncrementAdaptiveAckRecovery) {
|
|
2762
2864
|
this.adaptiveAckRecoveryOkCountByAccount.set(
|
|
2763
2865
|
args.accountId,
|
|
2764
2866
|
this.getCounter(this.adaptiveAckRecoveryOkCountByAccount, args.accountId) + 1,
|
|
@@ -2800,16 +2902,18 @@ class BncrBridgeRuntime {
|
|
|
2800
2902
|
clientId?: string;
|
|
2801
2903
|
error: string;
|
|
2802
2904
|
}) {
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2905
|
+
const nextEntry = buildBncrAckRetryEntryPatch({
|
|
2906
|
+
entry: args.entry,
|
|
2907
|
+
error: args.error,
|
|
2908
|
+
nextAttemptAt: now() + 1_000,
|
|
2909
|
+
});
|
|
2910
|
+
this.outbox.set(args.messageId, nextEntry);
|
|
2807
2911
|
this.scheduleSave();
|
|
2808
2912
|
this.logOutboxAckSummary('outbox ack retry', {
|
|
2809
2913
|
messageId: args.messageId,
|
|
2810
2914
|
connId: args.connId,
|
|
2811
2915
|
clientId: args.clientId,
|
|
2812
|
-
err:
|
|
2916
|
+
err: nextEntry.lastError,
|
|
2813
2917
|
});
|
|
2814
2918
|
}
|
|
2815
2919
|
|
|
@@ -2837,7 +2941,7 @@ class BncrBridgeRuntime {
|
|
|
2837
2941
|
entry,
|
|
2838
2942
|
});
|
|
2839
2943
|
this.respondAckResult(respond, staleObserved.stale, { ok: true });
|
|
2840
|
-
this.
|
|
2944
|
+
this.flushPushQueueBestEffort({
|
|
2841
2945
|
accountId,
|
|
2842
2946
|
trigger: OUTBOUND_FLUSH_TRIGGER.ACK_OK,
|
|
2843
2947
|
reason: OUTBOUND_FLUSH_REASON.MESSAGE_ACKED,
|
|
@@ -2978,7 +3082,15 @@ class BncrBridgeRuntime {
|
|
|
2978
3082
|
inboundOnly: boolean;
|
|
2979
3083
|
context: GatewayRequestHandlerOptions['context'];
|
|
2980
3084
|
}) {
|
|
2981
|
-
const {
|
|
3085
|
+
const {
|
|
3086
|
+
accountId,
|
|
3087
|
+
connId,
|
|
3088
|
+
clientId,
|
|
3089
|
+
outboundReady,
|
|
3090
|
+
preferredForOutbound,
|
|
3091
|
+
inboundOnly,
|
|
3092
|
+
context,
|
|
3093
|
+
} = args;
|
|
2982
3094
|
this.refreshAcceptedFileTransferLiveState({
|
|
2983
3095
|
accountId,
|
|
2984
3096
|
connId,
|
|
@@ -3017,19 +3129,23 @@ class BncrBridgeRuntime {
|
|
|
3017
3129
|
recentInboundReachable: boolean;
|
|
3018
3130
|
event: string;
|
|
3019
3131
|
}) {
|
|
3020
|
-
this.logInfo(
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
{ debugOnly: true },
|
|
3024
|
-
);
|
|
3132
|
+
this.logInfo('outbox', `push ${JSON.stringify(buildOutboxPushOkDebugInfo(args))}`, {
|
|
3133
|
+
debugOnly: true,
|
|
3134
|
+
});
|
|
3025
3135
|
}
|
|
3026
3136
|
|
|
3027
3137
|
private recordOutboxPrePushFailure(args: {
|
|
3028
3138
|
entry: OutboxEntry;
|
|
3029
3139
|
lastError: string;
|
|
3140
|
+
persist?: boolean;
|
|
3030
3141
|
}) {
|
|
3031
|
-
|
|
3032
|
-
|
|
3142
|
+
const nextEntry = buildBncrOutboxFailureEntryPatch({
|
|
3143
|
+
entry: args.entry,
|
|
3144
|
+
lastError: args.lastError,
|
|
3145
|
+
});
|
|
3146
|
+
Object.assign(args.entry, nextEntry);
|
|
3147
|
+
this.outbox.set(nextEntry.messageId, args.entry);
|
|
3148
|
+
if (args.persist) this.scheduleSave();
|
|
3033
3149
|
}
|
|
3034
3150
|
|
|
3035
3151
|
private recordOutboxPushFailure(args: {
|
|
@@ -3038,8 +3154,12 @@ class BncrBridgeRuntime {
|
|
|
3038
3154
|
fallbackError: string;
|
|
3039
3155
|
persist?: boolean;
|
|
3040
3156
|
}) {
|
|
3041
|
-
|
|
3042
|
-
|
|
3157
|
+
const nextEntry = buildBncrOutboxFailureEntryPatch({
|
|
3158
|
+
entry: args.entry,
|
|
3159
|
+
lastError: asString((args.error as any)?.message || args.error || args.fallbackError),
|
|
3160
|
+
});
|
|
3161
|
+
Object.assign(args.entry, nextEntry);
|
|
3162
|
+
this.outbox.set(nextEntry.messageId, args.entry);
|
|
3043
3163
|
if (args.persist) this.scheduleSave();
|
|
3044
3164
|
}
|
|
3045
3165
|
|
|
@@ -3050,23 +3170,19 @@ class BncrBridgeRuntime {
|
|
|
3050
3170
|
ownerClientId?: string;
|
|
3051
3171
|
clearLastError?: boolean;
|
|
3052
3172
|
}) {
|
|
3053
|
-
const
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
args.
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
)
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
if (args.clearLastError) args.entry.lastError = undefined;
|
|
3067
|
-
this.outbox.set(args.entry.messageId, args.entry);
|
|
3068
|
-
this.lastOutboundByAccount.set(args.entry.accountId, args.entry.lastPushAt);
|
|
3069
|
-
this.markActivity(args.entry.accountId, args.entry.lastPushAt);
|
|
3173
|
+
const pushedAt = now();
|
|
3174
|
+
const nextEntry = buildBncrOutboxPushSuccessEntryPatch({
|
|
3175
|
+
entry: args.entry,
|
|
3176
|
+
connIds: args.connIds,
|
|
3177
|
+
pushedAt,
|
|
3178
|
+
ownerConnId: args.ownerConnId,
|
|
3179
|
+
ownerClientId: args.ownerClientId,
|
|
3180
|
+
clearLastError: args.clearLastError,
|
|
3181
|
+
});
|
|
3182
|
+
Object.assign(args.entry, nextEntry);
|
|
3183
|
+
this.outbox.set(nextEntry.messageId, args.entry);
|
|
3184
|
+
this.lastOutboundByAccount.set(nextEntry.accountId, pushedAt);
|
|
3185
|
+
this.markActivity(nextEntry.accountId, pushedAt);
|
|
3070
3186
|
this.scheduleSave();
|
|
3071
3187
|
}
|
|
3072
3188
|
|
|
@@ -3093,49 +3209,107 @@ class BncrBridgeRuntime {
|
|
|
3093
3209
|
this.pushTimer = setTimeout(() => {
|
|
3094
3210
|
this.pushTimer = null;
|
|
3095
3211
|
if (this.stopped) return;
|
|
3096
|
-
|
|
3212
|
+
this.flushPushQueueBestEffort({
|
|
3097
3213
|
trigger: OUTBOUND_FLUSH_TRIGGER.TIMER,
|
|
3098
3214
|
reason: OUTBOUND_FLUSH_REASON.SCHEDULED_DRAIN,
|
|
3099
3215
|
});
|
|
3100
3216
|
}, delay);
|
|
3101
3217
|
}
|
|
3102
3218
|
|
|
3219
|
+
private flushPushQueueBestEffort(args?: {
|
|
3220
|
+
accountId?: string;
|
|
3221
|
+
trigger?: string;
|
|
3222
|
+
reason?: string;
|
|
3223
|
+
}) {
|
|
3224
|
+
void this.flushPushQueue(args)
|
|
3225
|
+
.then(() => {
|
|
3226
|
+
this.pushDrainExceptionRetryCount = 0;
|
|
3227
|
+
})
|
|
3228
|
+
.catch((error) => {
|
|
3229
|
+
const accountId = args?.accountId ? normalizeAccountId(args.accountId) : '';
|
|
3230
|
+
const reason = asString(args?.reason || args?.trigger || 'flush-error');
|
|
3231
|
+
const err = asString((error as any)?.message || error || 'flush-error');
|
|
3232
|
+
const nextRetryCount = this.pushDrainExceptionRetryCount + 1;
|
|
3233
|
+
const willRetry = nextRetryCount <= PUSH_DRAIN_EXCEPTION_RETRY_LIMIT;
|
|
3234
|
+
this.pushDrainExceptionRetryCount = nextRetryCount;
|
|
3235
|
+
this.logError(
|
|
3236
|
+
'outbox drain fail',
|
|
3237
|
+
`accountId=${accountId || '-'}|reason=${reason}|err=${err}|retry=${willRetry ? nextRetryCount : 'false'}|limit=${PUSH_DRAIN_EXCEPTION_RETRY_LIMIT}`,
|
|
3238
|
+
);
|
|
3239
|
+
if (willRetry) {
|
|
3240
|
+
this.schedulePushDrain(PUSH_DRAIN_EXCEPTION_RETRY_DELAY_MS);
|
|
3241
|
+
}
|
|
3242
|
+
});
|
|
3243
|
+
}
|
|
3244
|
+
|
|
3103
3245
|
private isOutboundAckRequired(accountId?: string) {
|
|
3104
|
-
|
|
3105
|
-
const cfg = getOpenClawRuntimeConfig(this.api);
|
|
3106
|
-
const channelCfg = (cfg as any)?.channels?.[CHANNEL_ID];
|
|
3107
|
-
const accountCfg =
|
|
3108
|
-
accountId && channelCfg?.accounts && typeof channelCfg.accounts === 'object'
|
|
3109
|
-
? (channelCfg.accounts as Record<string, any>)[normalizeAccountId(accountId)]
|
|
3110
|
-
: null;
|
|
3111
|
-
const scoped = accountCfg?.outboundRequireAck;
|
|
3112
|
-
const global = channelCfg?.outboundRequireAck;
|
|
3113
|
-
if (typeof scoped === 'boolean') return scoped;
|
|
3114
|
-
if (typeof global === 'boolean') return global;
|
|
3115
|
-
return true;
|
|
3116
|
-
} catch {
|
|
3117
|
-
return true;
|
|
3118
|
-
}
|
|
3246
|
+
return resolveBncrOutboundAckRequired({ api: this.api, accountId });
|
|
3119
3247
|
}
|
|
3120
3248
|
|
|
3121
3249
|
private buildRuntimeFlags(accountId?: string) {
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
if (typeof global === 'boolean') ackPolicySource = 'channel';
|
|
3127
|
-
} catch {
|
|
3128
|
-
// keep default source
|
|
3129
|
-
}
|
|
3130
|
-
return {
|
|
3131
|
-
outboundRequireAck: this.isOutboundAckRequired(accountId),
|
|
3132
|
-
ackPolicySource,
|
|
3133
|
-
messageAckTimeoutMs: this.resolveMessageAckTimeoutMs(accountId),
|
|
3250
|
+
return buildBncrRuntimeFlags({
|
|
3251
|
+
api: this.api,
|
|
3252
|
+
accountId,
|
|
3253
|
+
resolveMessageAckTimeoutMs: (acc?: string) => this.resolveMessageAckTimeoutMs(acc),
|
|
3134
3254
|
adaptiveAckTimeoutEnabled: ADAPTIVE_ACK_TIMEOUT_DEFAULT_ENABLED,
|
|
3135
3255
|
defaultMessageAckTimeoutMs: PUSH_ACK_TIMEOUT_MS,
|
|
3136
3256
|
fileAckTimeoutMs: FILE_ACK_TIMEOUT_MS,
|
|
3137
3257
|
debugVerbose: BNCR_DEBUG_VERBOSE,
|
|
3138
|
-
};
|
|
3258
|
+
});
|
|
3259
|
+
}
|
|
3260
|
+
|
|
3261
|
+
private getAccountPendingOutboxEntries(accountId: string) {
|
|
3262
|
+
const acc = normalizeAccountId(accountId);
|
|
3263
|
+
return Array.from(this.outbox.values()).filter((entry) => entry.accountId === acc);
|
|
3264
|
+
}
|
|
3265
|
+
|
|
3266
|
+
private maybeLogOutboxDrainStuck(args: {
|
|
3267
|
+
accountId: string;
|
|
3268
|
+
trigger: string;
|
|
3269
|
+
reason: string;
|
|
3270
|
+
}) {
|
|
3271
|
+
const acc = normalizeAccountId(args.accountId);
|
|
3272
|
+
const startedAt = this.pushDrainRunningSinceByAccount.get(acc) || 0;
|
|
3273
|
+
if (!startedAt) return;
|
|
3274
|
+
|
|
3275
|
+
const t = now();
|
|
3276
|
+
const runningMs = Math.max(0, t - startedAt);
|
|
3277
|
+
if (runningMs < PUSH_DRAIN_STUCK_WARN_MS) return;
|
|
3278
|
+
|
|
3279
|
+
const lastWarnedAt = this.pushDrainStuckWarnedAtByAccount.get(acc) || 0;
|
|
3280
|
+
if (lastWarnedAt && t - lastWarnedAt < PUSH_DRAIN_STUCK_WARN_MS) return;
|
|
3281
|
+
|
|
3282
|
+
const pendingEntries = this.getAccountPendingOutboxEntries(acc);
|
|
3283
|
+
const pending = pendingEntries.length;
|
|
3284
|
+
if (!pending) return;
|
|
3285
|
+
|
|
3286
|
+
this.pushDrainStuckWarnedAtByAccount.set(acc, t);
|
|
3287
|
+
this.logWarn(
|
|
3288
|
+
'outbox drain stuck',
|
|
3289
|
+
`accountId=${acc}|pending=${pending}|runningMs=${runningMs}|waiters=${this.messageAckWaiters.size}/${this.fileAckWaiters.size}`,
|
|
3290
|
+
);
|
|
3291
|
+
this.logInfo(
|
|
3292
|
+
'outbox',
|
|
3293
|
+
`drain-stuck ${JSON.stringify(
|
|
3294
|
+
buildOutboxDrainStuckDebugInfo({
|
|
3295
|
+
bridgeId: this.bridgeId,
|
|
3296
|
+
accountId: acc,
|
|
3297
|
+
reason: args.reason,
|
|
3298
|
+
trigger: args.trigger,
|
|
3299
|
+
outboxSize: this.outbox.size,
|
|
3300
|
+
pending,
|
|
3301
|
+
runningMs,
|
|
3302
|
+
runningSince: startedAt,
|
|
3303
|
+
hasGatewayContext: Boolean(this.gatewayContext),
|
|
3304
|
+
activeConnectionCount: this.activeConnectionCount(acc),
|
|
3305
|
+
messageAckWaiters: this.messageAckWaiters.size,
|
|
3306
|
+
fileAckWaiters: this.fileAckWaiters.size,
|
|
3307
|
+
pendingEntries,
|
|
3308
|
+
connections: this.connections.values(),
|
|
3309
|
+
}),
|
|
3310
|
+
)}`,
|
|
3311
|
+
{ debugOnly: true },
|
|
3312
|
+
);
|
|
3139
3313
|
}
|
|
3140
3314
|
|
|
3141
3315
|
private async flushPushQueue(args?: {
|
|
@@ -3195,7 +3369,28 @@ class BncrBridgeRuntime {
|
|
|
3195
3369
|
let globalNextDelay: number | null = null;
|
|
3196
3370
|
|
|
3197
3371
|
for (const acc of targetAccounts) {
|
|
3198
|
-
if (!acc
|
|
3372
|
+
if (!acc) continue;
|
|
3373
|
+
if (this.pushDrainRunningAccounts.has(acc)) {
|
|
3374
|
+
this.logInfo(
|
|
3375
|
+
'outbox',
|
|
3376
|
+
`drain-skip ${JSON.stringify(
|
|
3377
|
+
buildOutboxDrainSkipDebugInfo({
|
|
3378
|
+
bridgeId: this.bridgeId,
|
|
3379
|
+
accountId: acc,
|
|
3380
|
+
reason: 'already-running',
|
|
3381
|
+
outboxSize: this.outbox.size,
|
|
3382
|
+
trigger,
|
|
3383
|
+
}),
|
|
3384
|
+
)}`,
|
|
3385
|
+
{ debugOnly: true },
|
|
3386
|
+
);
|
|
3387
|
+
this.maybeLogOutboxDrainStuck({
|
|
3388
|
+
accountId: acc,
|
|
3389
|
+
trigger,
|
|
3390
|
+
reason: reason || 'already-running',
|
|
3391
|
+
});
|
|
3392
|
+
continue;
|
|
3393
|
+
}
|
|
3199
3394
|
const online = this.isOnline(acc);
|
|
3200
3395
|
const recentInboundReachable = this.hasRecentInboundReachability(acc);
|
|
3201
3396
|
this.logInfo(
|
|
@@ -3212,6 +3407,8 @@ class BncrBridgeRuntime {
|
|
|
3212
3407
|
{ debugOnly: true },
|
|
3213
3408
|
);
|
|
3214
3409
|
this.pushDrainRunningAccounts.add(acc);
|
|
3410
|
+
this.pushDrainRunningSinceByAccount.set(acc, now());
|
|
3411
|
+
this.pushDrainStuckWarnedAtByAccount.delete(acc);
|
|
3215
3412
|
try {
|
|
3216
3413
|
let localNextDelay: number | null = null;
|
|
3217
3414
|
let processedThisRun = 0;
|
|
@@ -3219,7 +3416,10 @@ class BncrBridgeRuntime {
|
|
|
3219
3416
|
|
|
3220
3417
|
while (true) {
|
|
3221
3418
|
if (this.stopped) break;
|
|
3222
|
-
if (
|
|
3419
|
+
if (
|
|
3420
|
+
processedThisRun > 0 &&
|
|
3421
|
+
now() - accountDrainStartedAt >= PUSH_DRAIN_ACCOUNT_TIME_BUDGET_MS
|
|
3422
|
+
) {
|
|
3223
3423
|
localNextDelay = updateMinOutboxDelay(localNextDelay, 0);
|
|
3224
3424
|
this.logInfo(
|
|
3225
3425
|
'outbox',
|
|
@@ -3286,14 +3486,63 @@ class BncrBridgeRuntime {
|
|
|
3286
3486
|
|
|
3287
3487
|
const onlineNow = this.isOnline(acc);
|
|
3288
3488
|
const recentInboundReachable = this.hasRecentInboundReachability(acc);
|
|
3289
|
-
|
|
3489
|
+
let pushed = false;
|
|
3490
|
+
try {
|
|
3491
|
+
pushed = await this.tryPushEntry(entry);
|
|
3492
|
+
} catch (error) {
|
|
3493
|
+
const meta = isPlainObject(entry.payload?._meta) ? entry.payload._meta : null;
|
|
3494
|
+
if (meta?.kind === 'file-transfer') {
|
|
3495
|
+
this.handleFileTransferPushFailure({
|
|
3496
|
+
entry,
|
|
3497
|
+
error,
|
|
3498
|
+
});
|
|
3499
|
+
} else {
|
|
3500
|
+
this.handleTextPushFailure({
|
|
3501
|
+
entry,
|
|
3502
|
+
error,
|
|
3503
|
+
});
|
|
3504
|
+
}
|
|
3505
|
+
pushed = false;
|
|
3506
|
+
}
|
|
3290
3507
|
processedThisRun += 1;
|
|
3291
3508
|
if (pushed) {
|
|
3292
3509
|
const requireAck = this.isOutboundAckRequired(acc);
|
|
3293
3510
|
const ackTimeoutMs = requireAck ? this.resolveMessageAckTimeoutMs(acc) : null;
|
|
3294
3511
|
let ackResult: 'acked' | 'timeout' = requireAck ? 'timeout' : 'acked';
|
|
3295
3512
|
if (onlineNow && requireAck) {
|
|
3296
|
-
|
|
3513
|
+
this.logInfo(
|
|
3514
|
+
'outbox',
|
|
3515
|
+
`ack wait-start ${JSON.stringify(
|
|
3516
|
+
buildOutboxAckDebugInfo({
|
|
3517
|
+
messageId: entry.messageId,
|
|
3518
|
+
accountId: entry.accountId,
|
|
3519
|
+
sessionKey: entry.sessionKey,
|
|
3520
|
+
to: formatDisplayScope(entry.route),
|
|
3521
|
+
kind:
|
|
3522
|
+
isPlainObject(entry.payload?._meta) &&
|
|
3523
|
+
entry.payload?._meta?.kind === 'file-transfer'
|
|
3524
|
+
? 'file-transfer'
|
|
3525
|
+
: undefined,
|
|
3526
|
+
requireAck,
|
|
3527
|
+
ackResult: 'timeout',
|
|
3528
|
+
ackStage: 'message',
|
|
3529
|
+
ackOutcome: 'waiting',
|
|
3530
|
+
ackTimeoutMs: ackTimeoutMs || PUSH_ACK_TIMEOUT_MS,
|
|
3531
|
+
adaptiveAckTimeoutEnabled: ADAPTIVE_ACK_TIMEOUT_DEFAULT_ENABLED,
|
|
3532
|
+
onlineNow,
|
|
3533
|
+
recentInboundReachable,
|
|
3534
|
+
connIds: entry.lastPushConnId ? [entry.lastPushConnId] : [],
|
|
3535
|
+
ownerConnId: entry.lastPushConnId,
|
|
3536
|
+
ownerClientId: entry.lastPushClientId,
|
|
3537
|
+
event: BNCR_PUSH_EVENT,
|
|
3538
|
+
}),
|
|
3539
|
+
)}`,
|
|
3540
|
+
{ debugOnly: true },
|
|
3541
|
+
);
|
|
3542
|
+
ackResult = await this.waitForMessageAck(
|
|
3543
|
+
entry.messageId,
|
|
3544
|
+
ackTimeoutMs || PUSH_ACK_TIMEOUT_MS,
|
|
3545
|
+
);
|
|
3297
3546
|
}
|
|
3298
3547
|
|
|
3299
3548
|
this.logOutboxAckWait({
|
|
@@ -3356,14 +3605,8 @@ class BncrBridgeRuntime {
|
|
|
3356
3605
|
continue;
|
|
3357
3606
|
}
|
|
3358
3607
|
|
|
3359
|
-
|
|
3360
|
-
entry.
|
|
3361
|
-
entry.retryCount = decision.nextRetryCount;
|
|
3362
|
-
entry.lastAttemptAt = decision.lastAttemptAt;
|
|
3363
|
-
entry.nextAttemptAt = decision.nextAttemptAt;
|
|
3364
|
-
entry.lastError = decision.lastError;
|
|
3365
|
-
entry.routeAttemptRound = decision.routeAttemptRound;
|
|
3366
|
-
this.outbox.set(entry.messageId, entry);
|
|
3608
|
+
const nextEntry = applyBncrRetryRerouteDecisionToEntry(entry, decision);
|
|
3609
|
+
this.outbox.set(entry.messageId, nextEntry);
|
|
3367
3610
|
this.scheduleSave();
|
|
3368
3611
|
if (requireAck) {
|
|
3369
3612
|
this.lastAckTimeoutByAccount.set(acc, now());
|
|
@@ -3377,7 +3620,7 @@ class BncrBridgeRuntime {
|
|
|
3377
3620
|
localNextDelay = updateMinOutboxDelay(localNextDelay, wait);
|
|
3378
3621
|
this.logOutboxAckReroute({
|
|
3379
3622
|
accountId: acc,
|
|
3380
|
-
entry,
|
|
3623
|
+
entry: nextEntry,
|
|
3381
3624
|
requireAck,
|
|
3382
3625
|
currentConnId,
|
|
3383
3626
|
availableConnIds,
|
|
@@ -3408,11 +3651,8 @@ class BncrBridgeRuntime {
|
|
|
3408
3651
|
continue;
|
|
3409
3652
|
}
|
|
3410
3653
|
|
|
3411
|
-
|
|
3412
|
-
entry.
|
|
3413
|
-
entry.nextAttemptAt = decision.nextAttemptAt;
|
|
3414
|
-
entry.lastError = decision.lastError;
|
|
3415
|
-
this.outbox.set(entry.messageId, entry);
|
|
3654
|
+
const nextEntry = applyBncrPushFailureDecisionToEntry(entry, decision);
|
|
3655
|
+
this.outbox.set(entry.messageId, nextEntry);
|
|
3416
3656
|
this.scheduleSave();
|
|
3417
3657
|
|
|
3418
3658
|
const wait = computeOutboxRetryWait(decision.nextAttemptAt, t);
|
|
@@ -3452,6 +3692,8 @@ class BncrBridgeRuntime {
|
|
|
3452
3692
|
}
|
|
3453
3693
|
} finally {
|
|
3454
3694
|
this.pushDrainRunningAccounts.delete(acc);
|
|
3695
|
+
this.pushDrainRunningSinceByAccount.delete(acc);
|
|
3696
|
+
this.pushDrainStuckWarnedAtByAccount.delete(acc);
|
|
3455
3697
|
}
|
|
3456
3698
|
}
|
|
3457
3699
|
|
|
@@ -3474,12 +3716,7 @@ class BncrBridgeRuntime {
|
|
|
3474
3716
|
|
|
3475
3717
|
private async waitForMessageAck(messageId: string, waitMs: number): Promise<'acked' | 'timeout'> {
|
|
3476
3718
|
const key = asString(messageId).trim();
|
|
3477
|
-
const timeoutMs = clampFiniteNumber(
|
|
3478
|
-
waitMs,
|
|
3479
|
-
0,
|
|
3480
|
-
0,
|
|
3481
|
-
RECOMMENDED_ACK_TIMEOUT_MAX_MS,
|
|
3482
|
-
);
|
|
3719
|
+
const timeoutMs = clampFiniteNumber(waitMs, 0, 0, RECOMMENDED_ACK_TIMEOUT_MAX_MS);
|
|
3483
3720
|
if (!key || !timeoutMs) return 'timeout';
|
|
3484
3721
|
|
|
3485
3722
|
const existing = this.messageAckWaiters.get(key);
|
|
@@ -3581,7 +3818,9 @@ class BncrBridgeRuntime {
|
|
|
3581
3818
|
const t = now();
|
|
3582
3819
|
const prev = this.connections.get(key);
|
|
3583
3820
|
const previousActiveKey = this.activeConnectionByAccount.get(acc) || null;
|
|
3584
|
-
const previousActiveConn = previousActiveKey
|
|
3821
|
+
const previousActiveConn = previousActiveKey
|
|
3822
|
+
? this.connections.get(previousActiveKey) || null
|
|
3823
|
+
: null;
|
|
3585
3824
|
|
|
3586
3825
|
const nextConn = {
|
|
3587
3826
|
accountId: acc,
|
|
@@ -3966,9 +4205,7 @@ class BncrBridgeRuntime {
|
|
|
3966
4205
|
}
|
|
3967
4206
|
|
|
3968
4207
|
private fileAckKey(transferId: string, stage: string, chunkIndex?: number): string {
|
|
3969
|
-
|
|
3970
|
-
const idx = Number.isInteger(n) && n >= 0 ? String(n) : '-';
|
|
3971
|
-
return `${transferId}|${stage}|${idx}`;
|
|
4208
|
+
return buildFileAckKey({ transferId, stage, chunkIndex });
|
|
3972
4209
|
}
|
|
3973
4210
|
|
|
3974
4211
|
private fileAckOwnerInfo(transferId: string) {
|
|
@@ -4003,8 +4240,9 @@ class BncrBridgeRuntime {
|
|
|
4003
4240
|
ackStage: stage,
|
|
4004
4241
|
ackOutcome: cached.ok ? 'acked' : 'failed',
|
|
4005
4242
|
waiterReused: false,
|
|
4006
|
-
chunkIndex:
|
|
4007
|
-
|
|
4243
|
+
chunkIndex: Number.isFinite(Number(params.chunkIndex))
|
|
4244
|
+
? Number(params.chunkIndex)
|
|
4245
|
+
: undefined,
|
|
4008
4246
|
key,
|
|
4009
4247
|
...ownerInfo,
|
|
4010
4248
|
ok: cached.ok,
|
|
@@ -4031,8 +4269,9 @@ class BncrBridgeRuntime {
|
|
|
4031
4269
|
ackStage: stage,
|
|
4032
4270
|
ackOutcome: 'waiter-reused',
|
|
4033
4271
|
waiterReused: true,
|
|
4034
|
-
chunkIndex:
|
|
4035
|
-
|
|
4272
|
+
chunkIndex: Number.isFinite(Number(params.chunkIndex))
|
|
4273
|
+
? Number(params.chunkIndex)
|
|
4274
|
+
: undefined,
|
|
4036
4275
|
key,
|
|
4037
4276
|
...ownerInfo,
|
|
4038
4277
|
}),
|
|
@@ -4050,8 +4289,9 @@ class BncrBridgeRuntime {
|
|
|
4050
4289
|
ackStage: stage,
|
|
4051
4290
|
ackOutcome: 'waiting',
|
|
4052
4291
|
waiterReused: false,
|
|
4053
|
-
chunkIndex:
|
|
4054
|
-
|
|
4292
|
+
chunkIndex: Number.isFinite(Number(params.chunkIndex))
|
|
4293
|
+
? Number(params.chunkIndex)
|
|
4294
|
+
: undefined,
|
|
4055
4295
|
key,
|
|
4056
4296
|
...ownerInfo,
|
|
4057
4297
|
timeoutMs,
|
|
@@ -4076,8 +4316,9 @@ class BncrBridgeRuntime {
|
|
|
4076
4316
|
ackStage: stage,
|
|
4077
4317
|
ackOutcome: 'timeout',
|
|
4078
4318
|
waiterReused: false,
|
|
4079
|
-
chunkIndex:
|
|
4080
|
-
|
|
4319
|
+
chunkIndex: Number.isFinite(Number(params.chunkIndex))
|
|
4320
|
+
? Number(params.chunkIndex)
|
|
4321
|
+
: undefined,
|
|
4081
4322
|
key,
|
|
4082
4323
|
...ownerInfo,
|
|
4083
4324
|
timeoutMs,
|
|
@@ -4123,8 +4364,9 @@ class BncrBridgeRuntime {
|
|
|
4123
4364
|
ackStage: stage,
|
|
4124
4365
|
ackOutcome: params.ok ? 'early-acked' : 'early-failed',
|
|
4125
4366
|
waiterReused: false,
|
|
4126
|
-
chunkIndex:
|
|
4127
|
-
|
|
4367
|
+
chunkIndex: Number.isFinite(Number(params.chunkIndex))
|
|
4368
|
+
? Number(params.chunkIndex)
|
|
4369
|
+
: undefined,
|
|
4128
4370
|
key,
|
|
4129
4371
|
...ownerInfo,
|
|
4130
4372
|
ok: params.ok,
|
|
@@ -4146,8 +4388,9 @@ class BncrBridgeRuntime {
|
|
|
4146
4388
|
ackStage: stage,
|
|
4147
4389
|
ackOutcome: params.ok ? 'acked' : 'failed',
|
|
4148
4390
|
waiterReused: false,
|
|
4149
|
-
chunkIndex:
|
|
4150
|
-
|
|
4391
|
+
chunkIndex: Number.isFinite(Number(params.chunkIndex))
|
|
4392
|
+
? Number(params.chunkIndex)
|
|
4393
|
+
: undefined,
|
|
4151
4394
|
key,
|
|
4152
4395
|
...ownerInfo,
|
|
4153
4396
|
ok: params.ok,
|
|
@@ -4196,31 +4439,6 @@ class BncrBridgeRuntime {
|
|
|
4196
4439
|
return mt || 'file';
|
|
4197
4440
|
}
|
|
4198
4441
|
|
|
4199
|
-
|
|
4200
|
-
private buildRuntimeQueueSnapshot(accountId: string) {
|
|
4201
|
-
const pending = Array.from(this.outbox.values()).filter((v) => v.accountId === accountId).length;
|
|
4202
|
-
const deadLetter = this.deadLetter.filter((v) => v.accountId === accountId).length;
|
|
4203
|
-
const sessionRoutesCount = Array.from(this.sessionRoutes.values()).filter(
|
|
4204
|
-
(v) => v.accountId === accountId,
|
|
4205
|
-
).length;
|
|
4206
|
-
return {
|
|
4207
|
-
pending,
|
|
4208
|
-
deadLetter,
|
|
4209
|
-
sessionRoutesCount,
|
|
4210
|
-
invalidOutboxSessionKeys: this.countInvalidOutboxSessionKeys(accountId),
|
|
4211
|
-
legacyAccountResidue: this.countLegacyAccountResidue(accountId),
|
|
4212
|
-
};
|
|
4213
|
-
}
|
|
4214
|
-
|
|
4215
|
-
private buildRuntimeEventCounters(accountId: string) {
|
|
4216
|
-
return {
|
|
4217
|
-
connectEvents: this.getCounter(this.connectEventsByAccount, accountId),
|
|
4218
|
-
inboundEvents: this.getCounter(this.inboundEventsByAccount, accountId),
|
|
4219
|
-
activityEvents: this.getCounter(this.activityEventsByAccount, accountId),
|
|
4220
|
-
ackEvents: this.getCounter(this.ackEventsByAccount, accountId),
|
|
4221
|
-
};
|
|
4222
|
-
}
|
|
4223
|
-
|
|
4224
4442
|
private computeRecommendedAckTimeoutReason(args: {
|
|
4225
4443
|
lateAckOkCount: number;
|
|
4226
4444
|
recentAckTimeoutCount: number;
|
|
@@ -4230,26 +4448,15 @@ class BncrBridgeRuntime {
|
|
|
4230
4448
|
recommendedAckTimeoutMs?: number;
|
|
4231
4449
|
nowMs?: number;
|
|
4232
4450
|
}) {
|
|
4233
|
-
|
|
4234
|
-
|
|
4235
|
-
|
|
4236
|
-
|
|
4237
|
-
|
|
4238
|
-
|
|
4239
|
-
|
|
4240
|
-
|
|
4241
|
-
|
|
4242
|
-
) {
|
|
4243
|
-
return 'late-ack-expired';
|
|
4244
|
-
}
|
|
4245
|
-
if (
|
|
4246
|
-
typeof args.adaptiveAckRecoveryOkCount === 'number' &&
|
|
4247
|
-
args.adaptiveAckRecoveryOkCount >= ADAPTIVE_ACK_TIMEOUT_RECOVERY_OK_THRESHOLD
|
|
4248
|
-
) {
|
|
4249
|
-
return 'recovered';
|
|
4250
|
-
}
|
|
4251
|
-
if (args.recommendedAckTimeoutMs === RECOMMENDED_ACK_TIMEOUT_MAX_MS) return 'capped-max';
|
|
4252
|
-
return 'late-ack-observed';
|
|
4451
|
+
return computeBncrRecommendedAckTimeoutReason({
|
|
4452
|
+
...args,
|
|
4453
|
+
nowMs: typeof args.nowMs === 'number' ? args.nowMs : now(),
|
|
4454
|
+
defaultAckTimeoutMs: PUSH_ACK_TIMEOUT_MS,
|
|
4455
|
+
minAckTimeoutMs: RECOMMENDED_ACK_TIMEOUT_MIN_MS,
|
|
4456
|
+
maxAckTimeoutMs: RECOMMENDED_ACK_TIMEOUT_MAX_MS,
|
|
4457
|
+
lateAckObservationTtlMs: ADAPTIVE_ACK_TIMEOUT_OBSERVATION_TTL_MS,
|
|
4458
|
+
recoveryOkThreshold: ADAPTIVE_ACK_TIMEOUT_RECOVERY_OK_THRESHOLD,
|
|
4459
|
+
});
|
|
4253
4460
|
}
|
|
4254
4461
|
|
|
4255
4462
|
private computeRecommendedAckTimeoutMs(args: {
|
|
@@ -4260,29 +4467,15 @@ class BncrBridgeRuntime {
|
|
|
4260
4467
|
adaptiveAckRecoveryOkCount?: number;
|
|
4261
4468
|
nowMs?: number;
|
|
4262
4469
|
}) {
|
|
4263
|
-
|
|
4264
|
-
|
|
4265
|
-
|
|
4266
|
-
|
|
4267
|
-
|
|
4268
|
-
|
|
4269
|
-
|
|
4270
|
-
|
|
4271
|
-
|
|
4272
|
-
if (
|
|
4273
|
-
args.lateAckOkCount <= 0 ||
|
|
4274
|
-
args.recentAckTimeoutCount <= 0 ||
|
|
4275
|
-
typeof args.lastLateAckPushLatencyMs !== 'number' ||
|
|
4276
|
-
lateAckExpired ||
|
|
4277
|
-
recovered
|
|
4278
|
-
) {
|
|
4279
|
-
return PUSH_ACK_TIMEOUT_MS;
|
|
4280
|
-
}
|
|
4281
|
-
const recommended = Math.ceil(args.lastLateAckPushLatencyMs * 1.25);
|
|
4282
|
-
return Math.min(
|
|
4283
|
-
RECOMMENDED_ACK_TIMEOUT_MAX_MS,
|
|
4284
|
-
Math.max(RECOMMENDED_ACK_TIMEOUT_MIN_MS, recommended),
|
|
4285
|
-
);
|
|
4470
|
+
return computeBncrRecommendedAckTimeoutMs({
|
|
4471
|
+
...args,
|
|
4472
|
+
nowMs: typeof args.nowMs === 'number' ? args.nowMs : now(),
|
|
4473
|
+
defaultAckTimeoutMs: PUSH_ACK_TIMEOUT_MS,
|
|
4474
|
+
minAckTimeoutMs: RECOMMENDED_ACK_TIMEOUT_MIN_MS,
|
|
4475
|
+
maxAckTimeoutMs: RECOMMENDED_ACK_TIMEOUT_MAX_MS,
|
|
4476
|
+
lateAckObservationTtlMs: ADAPTIVE_ACK_TIMEOUT_OBSERVATION_TTL_MS,
|
|
4477
|
+
recoveryOkThreshold: ADAPTIVE_ACK_TIMEOUT_RECOVERY_OK_THRESHOLD,
|
|
4478
|
+
});
|
|
4286
4479
|
}
|
|
4287
4480
|
|
|
4288
4481
|
private maybeLogAdaptiveAckTimeout(args: {
|
|
@@ -4327,7 +4520,10 @@ class BncrBridgeRuntime {
|
|
|
4327
4520
|
const recentAckTimeoutCount = this.getCounter(this.ackTimeoutCountByAccount, acc);
|
|
4328
4521
|
const lastLateAckPushLatencyMs = this.lastLateAckPushLatencyMsByAccount.get(acc) || null;
|
|
4329
4522
|
const lastLateAckOkAt = this.lastLateAckOkByAccount.get(acc) || null;
|
|
4330
|
-
const adaptiveAckRecoveryOkCount = this.getCounter(
|
|
4523
|
+
const adaptiveAckRecoveryOkCount = this.getCounter(
|
|
4524
|
+
this.adaptiveAckRecoveryOkCountByAccount,
|
|
4525
|
+
acc,
|
|
4526
|
+
);
|
|
4331
4527
|
const nowMs = now();
|
|
4332
4528
|
const timeoutMs = this.computeRecommendedAckTimeoutMs({
|
|
4333
4529
|
lateAckOkCount,
|
|
@@ -4364,12 +4560,18 @@ class BncrBridgeRuntime {
|
|
|
4364
4560
|
const lastLateAckOkAt = this.lastLateAckOkByAccount.get(acc) || null;
|
|
4365
4561
|
const nowMs = now();
|
|
4366
4562
|
const lastLateAckAgeMs =
|
|
4367
|
-
typeof lastLateAckOkAt === 'number' && lastLateAckOkAt > 0
|
|
4563
|
+
typeof lastLateAckOkAt === 'number' && lastLateAckOkAt > 0
|
|
4564
|
+
? Math.max(0, nowMs - lastLateAckOkAt)
|
|
4565
|
+
: null;
|
|
4368
4566
|
const lateAckObservationTtlMs = ADAPTIVE_ACK_TIMEOUT_OBSERVATION_TTL_MS;
|
|
4369
4567
|
const lateAckObservationExpired =
|
|
4370
4568
|
typeof lastLateAckAgeMs === 'number' && lastLateAckAgeMs > lateAckObservationTtlMs;
|
|
4371
|
-
const adaptiveAckRecoveryOkCount = this.getCounter(
|
|
4372
|
-
|
|
4569
|
+
const adaptiveAckRecoveryOkCount = this.getCounter(
|
|
4570
|
+
this.adaptiveAckRecoveryOkCountByAccount,
|
|
4571
|
+
acc,
|
|
4572
|
+
);
|
|
4573
|
+
const adaptiveAckRecovered =
|
|
4574
|
+
adaptiveAckRecoveryOkCount >= ADAPTIVE_ACK_TIMEOUT_RECOVERY_OK_THRESHOLD;
|
|
4373
4575
|
const recommendedAckTimeoutMs = this.computeRecommendedAckTimeoutMs({
|
|
4374
4576
|
lateAckOkCount,
|
|
4375
4577
|
recentAckTimeoutCount,
|
|
@@ -4412,44 +4614,42 @@ class BncrBridgeRuntime {
|
|
|
4412
4614
|
}
|
|
4413
4615
|
|
|
4414
4616
|
private buildRuntimeAckStrategy(ackObservability: Record<string, any>) {
|
|
4415
|
-
|
|
4416
|
-
|
|
4417
|
-
|
|
4418
|
-
|
|
4419
|
-
|
|
4420
|
-
currentMs,
|
|
4421
|
-
defaultMs,
|
|
4422
|
-
maxMs: RECOMMENDED_ACK_TIMEOUT_MAX_MS,
|
|
4423
|
-
reason,
|
|
4424
|
-
active: currentMs > defaultMs,
|
|
4425
|
-
lastLateAckAgeMs: ackObservability.lastLateAckAgeMs ?? null,
|
|
4426
|
-
lateAckObservationTtlMs: ackObservability.lateAckObservationTtlMs ?? null,
|
|
4427
|
-
recovered: ackObservability.adaptiveAckRecovered === true,
|
|
4428
|
-
};
|
|
4429
|
-
}
|
|
4430
|
-
|
|
4431
|
-
private buildRuntimeActivitySnapshot(accountId: string) {
|
|
4432
|
-
return {
|
|
4433
|
-
activeConnections: this.activeConnectionCount(accountId),
|
|
4434
|
-
lastSession: this.lastSessionByAccount.get(accountId) || null,
|
|
4435
|
-
lastActivityAt: this.lastActivityByAccount.get(accountId) || null,
|
|
4436
|
-
lastInboundAt: this.lastInboundByAccount.get(accountId) || null,
|
|
4437
|
-
lastOutboundAt: this.lastOutboundByAccount.get(accountId) || null,
|
|
4438
|
-
};
|
|
4617
|
+
return buildBncrRuntimeAckStrategy({
|
|
4618
|
+
ackObservability,
|
|
4619
|
+
defaultAckTimeoutMs: PUSH_ACK_TIMEOUT_MS,
|
|
4620
|
+
maxAckTimeoutMs: RECOMMENDED_ACK_TIMEOUT_MAX_MS,
|
|
4621
|
+
});
|
|
4439
4622
|
}
|
|
4440
4623
|
|
|
4441
4624
|
private buildRuntimeStatusInput(accountId: string, overrides: { running?: boolean } = {}) {
|
|
4442
4625
|
const acc = normalizeAccountId(accountId);
|
|
4443
|
-
|
|
4626
|
+
const snapshots = buildRuntimeStatusSnapshots({
|
|
4627
|
+
accountId: acc,
|
|
4628
|
+
outboxEntries: this.outbox.values(),
|
|
4629
|
+
deadLetterEntries: this.deadLetter,
|
|
4630
|
+
sessionRouteEntries: this.sessionRoutes.values(),
|
|
4631
|
+
countInvalidOutboxSessionKeys: (snapshotAccountId) =>
|
|
4632
|
+
this.countInvalidOutboxSessionKeys(snapshotAccountId),
|
|
4633
|
+
countLegacyAccountResidue: (snapshotAccountId) =>
|
|
4634
|
+
this.countLegacyAccountResidue(snapshotAccountId),
|
|
4635
|
+
connectEventsByAccount: this.connectEventsByAccount,
|
|
4636
|
+
inboundEventsByAccount: this.inboundEventsByAccount,
|
|
4637
|
+
activityEventsByAccount: this.activityEventsByAccount,
|
|
4638
|
+
ackEventsByAccount: this.ackEventsByAccount,
|
|
4639
|
+
activeConnectionCount: (snapshotAccountId) => this.activeConnectionCount(snapshotAccountId),
|
|
4640
|
+
lastSessionByAccount: this.lastSessionByAccount,
|
|
4641
|
+
lastActivityByAccount: this.lastActivityByAccount,
|
|
4642
|
+
lastInboundByAccount: this.lastInboundByAccount,
|
|
4643
|
+
lastOutboundByAccount: this.lastOutboundByAccount,
|
|
4644
|
+
});
|
|
4645
|
+
return buildBncrRuntimeStatusInput({
|
|
4444
4646
|
accountId: acc,
|
|
4445
4647
|
connected: this.isOnline(acc),
|
|
4446
|
-
...
|
|
4447
|
-
...this.buildRuntimeEventCounters(acc),
|
|
4448
|
-
...this.buildRuntimeActivitySnapshot(acc),
|
|
4648
|
+
...snapshots,
|
|
4449
4649
|
startedAt: this.startedAt,
|
|
4450
4650
|
running: overrides.running,
|
|
4451
4651
|
channelRoot: path.join(process.cwd(), 'plugins', 'bncr'),
|
|
4452
|
-
};
|
|
4652
|
+
});
|
|
4453
4653
|
}
|
|
4454
4654
|
|
|
4455
4655
|
private buildStatusMeta(accountId: string) {
|
|
@@ -4457,7 +4657,9 @@ class BncrBridgeRuntime {
|
|
|
4457
4657
|
}
|
|
4458
4658
|
|
|
4459
4659
|
getAccountRuntimeSnapshot(accountId: string) {
|
|
4460
|
-
const snapshot = buildAccountRuntimeSnapshot(
|
|
4660
|
+
const snapshot = buildAccountRuntimeSnapshot(
|
|
4661
|
+
this.buildRuntimeStatusInput(accountId, { running: true }),
|
|
4662
|
+
);
|
|
4461
4663
|
const ackObservability = this.buildRuntimeAckObservability(accountId);
|
|
4462
4664
|
const ackStrategy = this.buildRuntimeAckStrategy(ackObservability);
|
|
4463
4665
|
return {
|
|
@@ -4531,7 +4733,7 @@ class BncrBridgeRuntime {
|
|
|
4531
4733
|
this.logOutboundSummary(entry);
|
|
4532
4734
|
this.outbox.set(entry.messageId, entry);
|
|
4533
4735
|
this.scheduleSave();
|
|
4534
|
-
this.
|
|
4736
|
+
this.flushPushQueueBestEffort({ accountId: entry.accountId });
|
|
4535
4737
|
}
|
|
4536
4738
|
|
|
4537
4739
|
private moveToDeadLetter(entry: OutboxEntry, reason: string) {
|
|
@@ -4825,34 +5027,6 @@ class BncrBridgeRuntime {
|
|
|
4825
5027
|
);
|
|
4826
5028
|
}
|
|
4827
5029
|
|
|
4828
|
-
private buildFileTransferInitPayload(args: {
|
|
4829
|
-
transferId: string;
|
|
4830
|
-
sessionKey: string;
|
|
4831
|
-
route: BncrRoute;
|
|
4832
|
-
fileName: string;
|
|
4833
|
-
mimeType?: string;
|
|
4834
|
-
fileSize: number;
|
|
4835
|
-
chunkSize: number;
|
|
4836
|
-
totalChunks: number;
|
|
4837
|
-
fileSha256: string;
|
|
4838
|
-
}) {
|
|
4839
|
-
return {
|
|
4840
|
-
transferId: args.transferId,
|
|
4841
|
-
direction: 'oc2bncr' as const,
|
|
4842
|
-
sessionKey: args.sessionKey,
|
|
4843
|
-
platform: args.route.platform,
|
|
4844
|
-
groupId: args.route.groupId,
|
|
4845
|
-
userId: args.route.userId,
|
|
4846
|
-
fileName: args.fileName,
|
|
4847
|
-
mimeType: args.mimeType,
|
|
4848
|
-
fileSize: args.fileSize,
|
|
4849
|
-
chunkSize: args.chunkSize,
|
|
4850
|
-
totalChunks: args.totalChunks,
|
|
4851
|
-
fileSha256: args.fileSha256,
|
|
4852
|
-
ts: now(),
|
|
4853
|
-
};
|
|
4854
|
-
}
|
|
4855
|
-
|
|
4856
5030
|
private buildInitialFileSendTransferState(args: {
|
|
4857
5031
|
transferId: string;
|
|
4858
5032
|
accountId: string;
|
|
@@ -5043,7 +5217,7 @@ class BncrBridgeRuntime {
|
|
|
5043
5217
|
|
|
5044
5218
|
ctx.broadcastToConnIds(
|
|
5045
5219
|
BNCR_FILE_INIT_EVENT,
|
|
5046
|
-
|
|
5220
|
+
buildFileTransferInitPayload({
|
|
5047
5221
|
transferId,
|
|
5048
5222
|
sessionKey: params.sessionKey,
|
|
5049
5223
|
route: params.route,
|
|
@@ -5053,6 +5227,7 @@ class BncrBridgeRuntime {
|
|
|
5053
5227
|
chunkSize,
|
|
5054
5228
|
totalChunks,
|
|
5055
5229
|
fileSha256,
|
|
5230
|
+
ts: now(),
|
|
5056
5231
|
}),
|
|
5057
5232
|
connIds,
|
|
5058
5233
|
);
|
|
@@ -5069,7 +5244,7 @@ class BncrBridgeRuntime {
|
|
|
5069
5244
|
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
5070
5245
|
ctx.broadcastToConnIds(
|
|
5071
5246
|
BNCR_FILE_CHUNK_EVENT,
|
|
5072
|
-
{
|
|
5247
|
+
buildFileTransferChunkPayload({
|
|
5073
5248
|
transferId,
|
|
5074
5249
|
chunkIndex: idx,
|
|
5075
5250
|
offset: start,
|
|
@@ -5077,7 +5252,7 @@ class BncrBridgeRuntime {
|
|
|
5077
5252
|
chunkSha256,
|
|
5078
5253
|
base64: slice.toString('base64'),
|
|
5079
5254
|
ts: now(),
|
|
5080
|
-
},
|
|
5255
|
+
}),
|
|
5081
5256
|
connIds,
|
|
5082
5257
|
);
|
|
5083
5258
|
|
|
@@ -5125,11 +5300,11 @@ class BncrBridgeRuntime {
|
|
|
5125
5300
|
this.fileSendTransfers.set(transferId, st);
|
|
5126
5301
|
ctx.broadcastToConnIds(
|
|
5127
5302
|
BNCR_FILE_ABORT_EVENT,
|
|
5128
|
-
{
|
|
5303
|
+
buildFileTransferAbortPayload({
|
|
5129
5304
|
transferId,
|
|
5130
5305
|
reason: st.error,
|
|
5131
5306
|
ts: now(),
|
|
5132
|
-
},
|
|
5307
|
+
}),
|
|
5133
5308
|
connIds,
|
|
5134
5309
|
);
|
|
5135
5310
|
throw new Error(st.error);
|
|
@@ -5138,10 +5313,10 @@ class BncrBridgeRuntime {
|
|
|
5138
5313
|
|
|
5139
5314
|
ctx.broadcastToConnIds(
|
|
5140
5315
|
BNCR_FILE_COMPLETE_EVENT,
|
|
5141
|
-
{
|
|
5316
|
+
buildFileTransferCompletePayload({
|
|
5142
5317
|
transferId,
|
|
5143
5318
|
ts: now(),
|
|
5144
|
-
},
|
|
5319
|
+
}),
|
|
5145
5320
|
connIds,
|
|
5146
5321
|
);
|
|
5147
5322
|
|
|
@@ -5330,7 +5505,7 @@ class BncrBridgeRuntime {
|
|
|
5330
5505
|
});
|
|
5331
5506
|
|
|
5332
5507
|
// WS 一旦在线,立即尝试把离线期间积压队列直推出去
|
|
5333
|
-
this.
|
|
5508
|
+
this.flushPushQueueBestEffort({
|
|
5334
5509
|
accountId,
|
|
5335
5510
|
trigger: OUTBOUND_FLUSH_TRIGGER.CONNECT,
|
|
5336
5511
|
reason: OUTBOUND_FLUSH_REASON.WS_ONLINE,
|
|
@@ -5412,7 +5587,7 @@ class BncrBridgeRuntime {
|
|
|
5412
5587
|
deadLetter: this.deadLetter.filter((v) => v.accountId === accountId).length,
|
|
5413
5588
|
now: now(),
|
|
5414
5589
|
});
|
|
5415
|
-
this.
|
|
5590
|
+
this.flushPushQueueBestEffort({
|
|
5416
5591
|
accountId,
|
|
5417
5592
|
trigger: OUTBOUND_FLUSH_TRIGGER.ACTIVITY,
|
|
5418
5593
|
reason: OUTBOUND_FLUSH_REASON.ACTIVITY_HEARTBEAT,
|
|
@@ -5575,8 +5750,21 @@ class BncrBridgeRuntime {
|
|
|
5575
5750
|
respond(false, { error: 'transfer not found' });
|
|
5576
5751
|
return;
|
|
5577
5752
|
}
|
|
5753
|
+
if (st.status === 'completed') {
|
|
5754
|
+
respond(true, {
|
|
5755
|
+
ok: true,
|
|
5756
|
+
transferId,
|
|
5757
|
+
status: 'completed',
|
|
5758
|
+
path: st.completedPath,
|
|
5759
|
+
ignored: true,
|
|
5760
|
+
terminal: true,
|
|
5761
|
+
});
|
|
5762
|
+
return;
|
|
5763
|
+
}
|
|
5578
5764
|
if (chunkIndex >= st.totalChunks) {
|
|
5579
|
-
respond(false, {
|
|
5765
|
+
respond(false, {
|
|
5766
|
+
error: `chunkIndex out of range index=${chunkIndex} total=${st.totalChunks}`,
|
|
5767
|
+
});
|
|
5580
5768
|
return;
|
|
5581
5769
|
}
|
|
5582
5770
|
|
|
@@ -5778,6 +5966,17 @@ class BncrBridgeRuntime {
|
|
|
5778
5966
|
respond(true, { ok: true, transferId, message: 'not-found' });
|
|
5779
5967
|
return;
|
|
5780
5968
|
}
|
|
5969
|
+
if (st.status === 'completed') {
|
|
5970
|
+
respond(true, {
|
|
5971
|
+
ok: true,
|
|
5972
|
+
transferId,
|
|
5973
|
+
status: 'completed',
|
|
5974
|
+
path: st.completedPath,
|
|
5975
|
+
ignored: true,
|
|
5976
|
+
terminal: true,
|
|
5977
|
+
});
|
|
5978
|
+
return;
|
|
5979
|
+
}
|
|
5781
5980
|
|
|
5782
5981
|
const staleObserved = this.observeLease('file.abort', params ?? {});
|
|
5783
5982
|
if (staleObserved.stale) {
|
|
@@ -5874,6 +6073,30 @@ class BncrBridgeRuntime {
|
|
|
5874
6073
|
? 'file.abort'
|
|
5875
6074
|
: 'file.complete';
|
|
5876
6075
|
const staleObserved = this.observeLease(staleKind, params ?? {});
|
|
6076
|
+
if (st?.status === 'completed' || st?.status === 'aborted') {
|
|
6077
|
+
respond(
|
|
6078
|
+
true,
|
|
6079
|
+
staleObserved.stale
|
|
6080
|
+
? {
|
|
6081
|
+
ok: true,
|
|
6082
|
+
transferId,
|
|
6083
|
+
stage,
|
|
6084
|
+
state: st.status,
|
|
6085
|
+
stale: true,
|
|
6086
|
+
ignored: true,
|
|
6087
|
+
terminal: true,
|
|
6088
|
+
}
|
|
6089
|
+
: {
|
|
6090
|
+
ok: true,
|
|
6091
|
+
transferId,
|
|
6092
|
+
stage,
|
|
6093
|
+
state: st.status,
|
|
6094
|
+
ignored: true,
|
|
6095
|
+
terminal: true,
|
|
6096
|
+
},
|
|
6097
|
+
);
|
|
6098
|
+
return;
|
|
6099
|
+
}
|
|
5877
6100
|
if (staleObserved.stale) {
|
|
5878
6101
|
const sameConn = !!st?.ownerConnId && st.ownerConnId === connId;
|
|
5879
6102
|
const sameClient =
|
|
@@ -6100,7 +6323,7 @@ class BncrBridgeRuntime {
|
|
|
6100
6323
|
taskKey: extracted.taskKey ?? null,
|
|
6101
6324
|
}),
|
|
6102
6325
|
);
|
|
6103
|
-
this.
|
|
6326
|
+
this.flushPushQueueBestEffort({
|
|
6104
6327
|
accountId,
|
|
6105
6328
|
trigger: OUTBOUND_FLUSH_TRIGGER.INBOUND,
|
|
6106
6329
|
reason: OUTBOUND_FLUSH_REASON.INBOUND_ACCEPTED,
|
|
@@ -6130,126 +6353,11 @@ class BncrBridgeRuntime {
|
|
|
6130
6353
|
};
|
|
6131
6354
|
|
|
6132
6355
|
channelStartAccount = async (ctx: any) => {
|
|
6133
|
-
|
|
6134
|
-
this.clearChannelAccountWorker(accountId, 'start-replace');
|
|
6135
|
-
|
|
6136
|
-
const tick = () => {
|
|
6137
|
-
const previous = ctx.getStatus?.() || {};
|
|
6138
|
-
const onlineByConn = this.isOnline(accountId);
|
|
6139
|
-
const recentInboundReachable = this.hasRecentInboundReachability(accountId);
|
|
6140
|
-
const connected = onlineByConn || recentInboundReachable;
|
|
6141
|
-
const lastActAt =
|
|
6142
|
-
this.lastActivityByAccount.get(accountId) ||
|
|
6143
|
-
this.lastInboundByAccount.get(accountId) ||
|
|
6144
|
-
this.lastOutboundByAccount.get(accountId) ||
|
|
6145
|
-
previous?.lastEventAt ||
|
|
6146
|
-
null;
|
|
6147
|
-
const healthSig = JSON.stringify({
|
|
6148
|
-
bridge: this.bridgeId,
|
|
6149
|
-
accountId,
|
|
6150
|
-
connected,
|
|
6151
|
-
onlineByConn,
|
|
6152
|
-
recentInboundReachable,
|
|
6153
|
-
activeConnectionKey: this.activeConnectionByAccount.get(accountId) || null,
|
|
6154
|
-
activeConnections: Array.from(this.connections.values())
|
|
6155
|
-
.filter((c) => c.accountId === accountId)
|
|
6156
|
-
.map((c) => ({
|
|
6157
|
-
connId: c.connId,
|
|
6158
|
-
clientId: c.clientId,
|
|
6159
|
-
inboundOnly: c.inboundOnly === true,
|
|
6160
|
-
outboundReady: c.outboundReady === true,
|
|
6161
|
-
preferredForOutbound: c.preferredForOutbound === true,
|
|
6162
|
-
})),
|
|
6163
|
-
});
|
|
6164
|
-
const conns = Array.from(this.connections.values()).filter((c) => c.accountId === accountId).length;
|
|
6165
|
-
this.logInfoDedup(
|
|
6166
|
-
'health',
|
|
6167
|
-
`status-tick ${accountId}|changed|${connected ? 'linked' : 'configured'}|onlineByConn=${onlineByConn}|recentInboundReachable=${recentInboundReachable}|conns=${conns}`,
|
|
6168
|
-
{
|
|
6169
|
-
key: `health-status-tick:${accountId}`,
|
|
6170
|
-
sig: healthSig,
|
|
6171
|
-
},
|
|
6172
|
-
);
|
|
6173
|
-
this.logInfoDedup('health', `status-tick ${healthSig}`, {
|
|
6174
|
-
key: `health-status-tick-debug:${accountId}`,
|
|
6175
|
-
sig: healthSig,
|
|
6176
|
-
debugOnly: true,
|
|
6177
|
-
});
|
|
6178
|
-
|
|
6179
|
-
ctx.setStatus?.({
|
|
6180
|
-
...previous,
|
|
6181
|
-
accountId,
|
|
6182
|
-
running: true,
|
|
6183
|
-
connected,
|
|
6184
|
-
lastEventAt: lastActAt,
|
|
6185
|
-
// 状态映射:在线=linked,离线=configured
|
|
6186
|
-
mode: connected ? 'linked' : 'configured',
|
|
6187
|
-
lastError: previous?.lastError ?? null,
|
|
6188
|
-
meta: this.buildStatusMeta(accountId),
|
|
6189
|
-
});
|
|
6190
|
-
};
|
|
6191
|
-
|
|
6192
|
-
tick();
|
|
6193
|
-
const timer = setInterval(tick, 5_000);
|
|
6194
|
-
let worker!: ChannelAccountWorkerHandle;
|
|
6195
|
-
const done = new Promise<void>((resolve) => {
|
|
6196
|
-
let settled = false;
|
|
6197
|
-
const finish = (reason: string) => {
|
|
6198
|
-
if (settled) return;
|
|
6199
|
-
settled = true;
|
|
6200
|
-
const activeWorker = this.channelAccountWorkers.get(accountId);
|
|
6201
|
-
if (activeWorker === worker) {
|
|
6202
|
-
this.channelAccountWorkers.delete(accountId);
|
|
6203
|
-
}
|
|
6204
|
-
clearInterval(timer);
|
|
6205
|
-
worker.cleanupAbortListener?.();
|
|
6206
|
-
worker.cleanupAbortListener = undefined;
|
|
6207
|
-
this.logInfo(
|
|
6208
|
-
'health',
|
|
6209
|
-
`status-worker finished ${JSON.stringify({ bridge: this.bridgeId, accountId, reason })}`,
|
|
6210
|
-
{ debugOnly: true },
|
|
6211
|
-
);
|
|
6212
|
-
this.logInfo('health', `status-worker finished ${accountId}|${reason}`);
|
|
6213
|
-
resolve();
|
|
6214
|
-
};
|
|
6215
|
-
|
|
6216
|
-
worker = { timer, finish };
|
|
6217
|
-
this.channelAccountWorkers.set(accountId, worker);
|
|
6218
|
-
|
|
6219
|
-
const onAbort = () => finish('abort');
|
|
6220
|
-
const abortSignal = ctx.abortSignal;
|
|
6221
|
-
|
|
6222
|
-
if (abortSignal?.aborted) {
|
|
6223
|
-
onAbort();
|
|
6224
|
-
return;
|
|
6225
|
-
}
|
|
6226
|
-
|
|
6227
|
-
abortSignal?.addEventListener?.('abort', onAbort, { once: true });
|
|
6228
|
-
if (abortSignal?.removeEventListener) {
|
|
6229
|
-
worker.cleanupAbortListener = () => abortSignal.removeEventListener('abort', onAbort);
|
|
6230
|
-
}
|
|
6231
|
-
});
|
|
6232
|
-
await done;
|
|
6356
|
+
await startBncrStatusWorker(this.buildStatusWorkerRuntime(), ctx);
|
|
6233
6357
|
};
|
|
6234
6358
|
|
|
6235
6359
|
channelStopAccount = async (ctx: any) => {
|
|
6236
|
-
|
|
6237
|
-
const cleared = this.clearChannelAccountWorker(accountId, 'explicit-stop');
|
|
6238
|
-
const previous = ctx?.getStatus?.() || {};
|
|
6239
|
-
ctx?.setStatus?.({
|
|
6240
|
-
...previous,
|
|
6241
|
-
accountId,
|
|
6242
|
-
running: false,
|
|
6243
|
-
restartPending: false,
|
|
6244
|
-
lastStopAt: Date.now(),
|
|
6245
|
-
meta: this.buildStatusMeta(accountId),
|
|
6246
|
-
});
|
|
6247
|
-
this.logInfo(
|
|
6248
|
-
'health',
|
|
6249
|
-
`status-stop ${JSON.stringify({ bridge: this.bridgeId, accountId, cleared })}`,
|
|
6250
|
-
{ debugOnly: true },
|
|
6251
|
-
);
|
|
6252
|
-
this.logInfo('health', `status-stop ${accountId}|cleared=${cleared}`);
|
|
6360
|
+
await stopBncrStatusWorker(this.buildStatusWorkerRuntime(), ctx);
|
|
6253
6361
|
};
|
|
6254
6362
|
|
|
6255
6363
|
private logChannelSendEntry(args: {
|
|
@@ -6380,7 +6488,9 @@ class BncrBridgeRuntime {
|
|
|
6380
6488
|
payload,
|
|
6381
6489
|
mediaLocalRoots: ctx.mediaLocalRoots,
|
|
6382
6490
|
});
|
|
6383
|
-
const entries = Array.from(this.outbox.values()).filter(
|
|
6491
|
+
const entries = Array.from(this.outbox.values()).filter(
|
|
6492
|
+
(entry) => !before.has(entry.messageId),
|
|
6493
|
+
);
|
|
6384
6494
|
if (!entries.length) {
|
|
6385
6495
|
throw new Error('bncr channel.message handoff did not enqueue an outbox entry');
|
|
6386
6496
|
}
|
|
@@ -6421,7 +6531,9 @@ class BncrBridgeRuntime {
|
|
|
6421
6531
|
asVoice: payload.asVoice === true,
|
|
6422
6532
|
audioAsVoice: payload.audioAsVoice === true,
|
|
6423
6533
|
kind: payload.kind,
|
|
6424
|
-
replyToId:
|
|
6534
|
+
replyToId:
|
|
6535
|
+
asString(payload.replyToId || ctx?.replyToId || ctx?.replyToMessageId || '').trim() ||
|
|
6536
|
+
undefined,
|
|
6425
6537
|
});
|
|
6426
6538
|
return buildBncrDurableQueuedResult({ entry });
|
|
6427
6539
|
};
|
|
@@ -6460,7 +6572,7 @@ export function createBncrChannelPlugin(getBridge: () => BncrBridgeRuntime) {
|
|
|
6460
6572
|
};
|
|
6461
6573
|
},
|
|
6462
6574
|
supportsAction: ({ action }) => action === 'send',
|
|
6463
|
-
extractToolSend: ({ args })
|
|
6575
|
+
extractToolSend: ({ args }) => extractOpenClawToolSend(args, 'sendMessage'),
|
|
6464
6576
|
handleAction: async ({ action, params, accountId, mediaLocalRoots }) => {
|
|
6465
6577
|
if (action !== 'send')
|
|
6466
6578
|
throw new Error(`Action ${action} is not supported for provider ${CHANNEL_ID}.`);
|
|
@@ -6504,231 +6616,21 @@ export function createBncrChannelPlugin(getBridge: () => BncrBridgeRuntime) {
|
|
|
6504
6616
|
|
|
6505
6617
|
const plugin = {
|
|
6506
6618
|
id: CHANNEL_ID,
|
|
6507
|
-
meta:
|
|
6508
|
-
id: CHANNEL_ID,
|
|
6509
|
-
label: 'Bncr',
|
|
6510
|
-
selectionLabel: 'Bncr Client',
|
|
6511
|
-
docsPath: '/channels/bncr',
|
|
6512
|
-
blurb: 'Bncr Channel.',
|
|
6513
|
-
aliases: ['bncr'],
|
|
6514
|
-
},
|
|
6619
|
+
meta: BNCR_CHANNEL_META,
|
|
6515
6620
|
actions: messageActions,
|
|
6516
6621
|
message: {
|
|
6517
|
-
receive:
|
|
6518
|
-
|
|
6519
|
-
supportedAckPolicies: ['manual'] as const,
|
|
6520
|
-
},
|
|
6521
|
-
send: {
|
|
6522
|
-
text: async (ctx: any) => getBridge().channelMessageSendText(ctx),
|
|
6523
|
-
media: async (ctx: any) => getBridge().channelMessageSendMedia(ctx),
|
|
6524
|
-
payload: async (ctx: any) => getBridge().channelMessageSendPayload(ctx),
|
|
6525
|
-
},
|
|
6526
|
-
},
|
|
6527
|
-
capabilities: {
|
|
6528
|
-
chatTypes: ['direct'] as ChatType[],
|
|
6529
|
-
media: true,
|
|
6530
|
-
reply: true,
|
|
6531
|
-
nativeCommands: true,
|
|
6532
|
-
},
|
|
6533
|
-
messaging: {
|
|
6534
|
-
// 接收任意标签输入;不在 normalize 阶段做格式门槛,统一下沉到发送前验证。
|
|
6535
|
-
normalizeTarget: (raw: string) => {
|
|
6536
|
-
const input = asString(raw).trim();
|
|
6537
|
-
return input || undefined;
|
|
6538
|
-
},
|
|
6539
|
-
parseExplicitTarget: ({ raw, accountId, cfg }: any) => {
|
|
6540
|
-
const resolvedAccountId = normalizeAccountId(
|
|
6541
|
-
asString(accountId || BNCR_DEFAULT_ACCOUNT_ID),
|
|
6542
|
-
);
|
|
6543
|
-
const runtimeBridge = getBridge();
|
|
6544
|
-
const canonicalAgentId =
|
|
6545
|
-
runtimeBridge.canonicalAgentId ||
|
|
6546
|
-
runtimeBridge.ensureCanonicalAgentId({ cfg, accountId: resolvedAccountId });
|
|
6547
|
-
return parseExplicitTarget(asString(raw).trim(), { canonicalAgentId });
|
|
6548
|
-
},
|
|
6549
|
-
formatTargetDisplay: ({ target }: any) => {
|
|
6550
|
-
return formatTargetDisplay(target);
|
|
6551
|
-
},
|
|
6552
|
-
resolveSessionTarget: ({ id, accountId, cfg }: any) => {
|
|
6553
|
-
const raw = asString(id).trim();
|
|
6554
|
-
if (!raw) return undefined;
|
|
6555
|
-
const resolvedAccountId = normalizeAccountId(
|
|
6556
|
-
asString(accountId || BNCR_DEFAULT_ACCOUNT_ID),
|
|
6557
|
-
);
|
|
6558
|
-
const runtimeBridge = getBridge();
|
|
6559
|
-
const canonicalAgentId =
|
|
6560
|
-
runtimeBridge.canonicalAgentId ||
|
|
6561
|
-
runtimeBridge.ensureCanonicalAgentId({ cfg, accountId: resolvedAccountId });
|
|
6562
|
-
|
|
6563
|
-
let parsed = parseExplicitTarget(raw, { canonicalAgentId });
|
|
6564
|
-
if (!parsed) {
|
|
6565
|
-
const route = runtimeBridge.resolveRouteBySession(raw, resolvedAccountId);
|
|
6566
|
-
if (route) {
|
|
6567
|
-
parsed = parseExplicitTarget(formatDisplayScope(route), { canonicalAgentId });
|
|
6568
|
-
}
|
|
6569
|
-
}
|
|
6570
|
-
return parsed?.displayScope || undefined;
|
|
6571
|
-
},
|
|
6572
|
-
resolveOutboundSessionRoute: (params: any) => {
|
|
6573
|
-
const accountId = normalizeAccountId(
|
|
6574
|
-
asString(params?.accountId || BNCR_DEFAULT_ACCOUNT_ID),
|
|
6575
|
-
);
|
|
6576
|
-
const runtimeBridge = getBridge();
|
|
6577
|
-
const canonicalAgentId =
|
|
6578
|
-
runtimeBridge.canonicalAgentId ||
|
|
6579
|
-
runtimeBridge.ensureCanonicalAgentId({ cfg: params?.cfg, accountId });
|
|
6580
|
-
return resolveBncrOutboundSessionRoute({
|
|
6581
|
-
...params,
|
|
6582
|
-
canonicalAgentId,
|
|
6583
|
-
resolveRouteBySession: (raw: string, acc: string) =>
|
|
6584
|
-
runtimeBridge.resolveRouteBySession(raw, acc),
|
|
6585
|
-
});
|
|
6586
|
-
},
|
|
6587
|
-
targetResolver: {
|
|
6588
|
-
looksLikeId: (raw: string, normalized?: string) => {
|
|
6589
|
-
return looksLikeBncrExplicitTarget(asString(normalized || raw).trim());
|
|
6590
|
-
},
|
|
6591
|
-
resolveTarget: async ({ accountId, input, normalized }) => {
|
|
6592
|
-
const runtimeBridge = getBridge();
|
|
6593
|
-
const resolved = resolveBncrOutboundTarget({
|
|
6594
|
-
target: asString(normalized || input).trim(),
|
|
6595
|
-
accountId: normalizeAccountId(asString(accountId || BNCR_DEFAULT_ACCOUNT_ID)),
|
|
6596
|
-
resolveRouteBySession: (raw: string, acc: string) =>
|
|
6597
|
-
runtimeBridge.resolveRouteBySession(raw, acc),
|
|
6598
|
-
});
|
|
6599
|
-
if (!resolved) return null;
|
|
6600
|
-
return {
|
|
6601
|
-
to: resolved.displayScope,
|
|
6602
|
-
kind: resolved.kind,
|
|
6603
|
-
display: resolved.displayScope,
|
|
6604
|
-
source: 'normalized' as const,
|
|
6605
|
-
};
|
|
6606
|
-
},
|
|
6607
|
-
hint: 'Standard to=Bncr:<platform>:<group>:<user> or Bncr:<platform>:<user>; sessionKey keeps existing strict/legacy compatibility, canonical sessionKey=agent:<agentId>:bncr:direct:<hex>',
|
|
6608
|
-
},
|
|
6622
|
+
receive: BNCR_MESSAGE_RECEIVE_POLICY,
|
|
6623
|
+
send: createBncrMessageSend(getBridge),
|
|
6609
6624
|
},
|
|
6625
|
+
capabilities: BNCR_CHANNEL_CAPABILITIES,
|
|
6626
|
+
messaging: createBncrMessagingSurface(getBridge),
|
|
6610
6627
|
configSchema: BncrConfigSchema,
|
|
6611
|
-
config:
|
|
6612
|
-
|
|
6613
|
-
|
|
6614
|
-
|
|
6615
|
-
|
|
6616
|
-
|
|
6617
|
-
sectionKey: CHANNEL_ID,
|
|
6618
|
-
accountId,
|
|
6619
|
-
enabled,
|
|
6620
|
-
allowTopLevel: true,
|
|
6621
|
-
}),
|
|
6622
|
-
isEnabled: (account: any, cfg: any) => {
|
|
6623
|
-
const policy = resolveBncrChannelPolicy(cfg?.channels?.[CHANNEL_ID] || {});
|
|
6624
|
-
return policy.enabled !== false && account?.enabled !== false;
|
|
6625
|
-
},
|
|
6626
|
-
isConfigured: () => true,
|
|
6627
|
-
describeAccount: (account: any) => {
|
|
6628
|
-
const displayName = resolveDefaultDisplayName(account?.name, account?.accountId);
|
|
6629
|
-
return {
|
|
6630
|
-
accountId: account.accountId,
|
|
6631
|
-
name: displayName,
|
|
6632
|
-
enabled: account.enabled !== false,
|
|
6633
|
-
configured: true,
|
|
6634
|
-
};
|
|
6635
|
-
},
|
|
6636
|
-
},
|
|
6637
|
-
setup: {
|
|
6638
|
-
applyAccountName: ({ cfg, accountId, name }: any) =>
|
|
6639
|
-
applyOpenClawAccountNameToChannelSection({
|
|
6640
|
-
cfg,
|
|
6641
|
-
channelKey: CHANNEL_ID,
|
|
6642
|
-
accountId,
|
|
6643
|
-
name,
|
|
6644
|
-
alwaysUseAccounts: true,
|
|
6645
|
-
}),
|
|
6646
|
-
applyAccountConfig: ({ cfg, accountId }: any) => {
|
|
6647
|
-
const next = { ...(cfg || {}) } as any;
|
|
6648
|
-
next.channels = next.channels || {};
|
|
6649
|
-
next.channels[CHANNEL_ID] = next.channels[CHANNEL_ID] || {};
|
|
6650
|
-
next.channels[CHANNEL_ID].accounts = next.channels[CHANNEL_ID].accounts || {};
|
|
6651
|
-
next.channels[CHANNEL_ID].accounts[accountId] = {
|
|
6652
|
-
...(next.channels[CHANNEL_ID].accounts[accountId] || {}),
|
|
6653
|
-
enabled: true,
|
|
6654
|
-
};
|
|
6655
|
-
return next;
|
|
6656
|
-
},
|
|
6657
|
-
},
|
|
6658
|
-
outbound: {
|
|
6659
|
-
deliveryMode: 'gateway' as const,
|
|
6660
|
-
sendText: async (ctx: any) => getBridge().channelSendText(ctx),
|
|
6661
|
-
sendMedia: async (ctx: any) => getBridge().channelSendMedia(ctx),
|
|
6662
|
-
replyAction: async (ctx: any) =>
|
|
6663
|
-
sendBncrReplyAction({
|
|
6664
|
-
accountId: normalizeAccountId(ctx?.accountId),
|
|
6665
|
-
to: asString(ctx?.to || '').trim(),
|
|
6666
|
-
text: asString(ctx?.text || ''),
|
|
6667
|
-
replyToMessageId:
|
|
6668
|
-
asString(ctx?.replyToId || ctx?.replyToMessageId || '').trim() || undefined,
|
|
6669
|
-
sendText: async ({ accountId, to, text }) =>
|
|
6670
|
-
getBridge().channelSendText({ accountId, to, text }),
|
|
6671
|
-
}),
|
|
6672
|
-
deleteAction: async (ctx: any) =>
|
|
6673
|
-
deleteBncrMessageAction({
|
|
6674
|
-
accountId: normalizeAccountId(ctx?.accountId),
|
|
6675
|
-
targetMessageId: asString(ctx?.messageId || ctx?.targetMessageId || '').trim(),
|
|
6676
|
-
}),
|
|
6677
|
-
reactAction: async (ctx: any) =>
|
|
6678
|
-
reactBncrMessageAction({
|
|
6679
|
-
accountId: normalizeAccountId(ctx?.accountId),
|
|
6680
|
-
targetMessageId: asString(ctx?.messageId || ctx?.targetMessageId || '').trim(),
|
|
6681
|
-
emoji: asString(ctx?.emoji || '').trim(),
|
|
6682
|
-
}),
|
|
6683
|
-
editAction: async (ctx: any) =>
|
|
6684
|
-
editBncrMessageAction({
|
|
6685
|
-
accountId: normalizeAccountId(ctx?.accountId),
|
|
6686
|
-
targetMessageId: asString(ctx?.messageId || ctx?.targetMessageId || '').trim(),
|
|
6687
|
-
text: asString(ctx?.text || ''),
|
|
6688
|
-
}),
|
|
6689
|
-
},
|
|
6690
|
-
status: {
|
|
6691
|
-
defaultRuntime: createOpenClawDefaultChannelRuntimeState(BNCR_DEFAULT_ACCOUNT_ID, {
|
|
6692
|
-
mode: 'ws-offline',
|
|
6693
|
-
}),
|
|
6694
|
-
buildChannelSummary: async ({ defaultAccountId }: any) => {
|
|
6695
|
-
return getBridge().getChannelSummary(defaultAccountId || BNCR_DEFAULT_ACCOUNT_ID);
|
|
6696
|
-
},
|
|
6697
|
-
buildAccountSnapshot: async ({ account, runtime }: any) => {
|
|
6698
|
-
const runtimeBridge = getBridge();
|
|
6699
|
-
const rt = runtime || runtimeBridge.getAccountRuntimeSnapshot(account?.accountId);
|
|
6700
|
-
return buildAccountStatusSnapshot({
|
|
6701
|
-
account,
|
|
6702
|
-
runtime: rt,
|
|
6703
|
-
healthSummary: runtimeBridge.getStatusHeadline(account?.accountId),
|
|
6704
|
-
// default 名不可隐藏时,统一展示稳定默认值
|
|
6705
|
-
displayName: resolveDefaultDisplayName(account?.name, account?.accountId),
|
|
6706
|
-
});
|
|
6707
|
-
},
|
|
6708
|
-
resolveAccountState: ({ enabled, configured, account, cfg, runtime }: any) => {
|
|
6709
|
-
if (!enabled) return 'disabled';
|
|
6710
|
-
const resolved = resolveAccount(cfg, account?.accountId);
|
|
6711
|
-
if (!(resolved.enabled && configured)) return 'not configured';
|
|
6712
|
-
const rt = runtime || getBridge().getAccountRuntimeSnapshot(account?.accountId);
|
|
6713
|
-
return rt?.connected ? 'linked' : 'configured';
|
|
6714
|
-
},
|
|
6715
|
-
},
|
|
6716
|
-
gatewayMethods: [
|
|
6717
|
-
'bncr.connect',
|
|
6718
|
-
'bncr.inbound',
|
|
6719
|
-
'bncr.activity',
|
|
6720
|
-
'bncr.ack',
|
|
6721
|
-
'bncr.diagnostics',
|
|
6722
|
-
'bncr.file.init',
|
|
6723
|
-
'bncr.file.chunk',
|
|
6724
|
-
'bncr.file.complete',
|
|
6725
|
-
'bncr.file.abort',
|
|
6726
|
-
'bncr.file.ack',
|
|
6727
|
-
],
|
|
6728
|
-
gateway: {
|
|
6729
|
-
startAccount: async (ctx: any) => getBridge().channelStartAccount(ctx),
|
|
6730
|
-
stopAccount: async (ctx: any) => getBridge().channelStopAccount(ctx),
|
|
6731
|
-
},
|
|
6628
|
+
config: BNCR_CONFIG_SURFACE,
|
|
6629
|
+
setup: BNCR_SETUP_SURFACE,
|
|
6630
|
+
outbound: createBncrOutboundRuntime(getBridge),
|
|
6631
|
+
status: createBncrStatusSurface(getBridge),
|
|
6632
|
+
gatewayMethods: BNCR_GATEWAY_METHODS,
|
|
6633
|
+
gateway: createBncrGatewayRuntime(getBridge),
|
|
6732
6634
|
};
|
|
6733
6635
|
|
|
6734
6636
|
return plugin;
|