@xmoxmo/bncr 0.3.3 → 0.3.5
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/dist/index.js +7 -3
- package/index.ts +11 -10
- package/openclaw.plugin.json +21 -0
- package/package.json +4 -4
- package/scripts/check-pack.mjs +112 -22
- package/scripts/check-register-drift.mjs +91 -65
- package/scripts/selfcheck.mjs +79 -3
- package/src/channel.ts +549 -810
- package/src/core/accounts.ts +1 -1
- package/src/core/connection-capability.ts +2 -2
- package/src/core/connection-reachability.ts +112 -1
- package/src/core/dead-letter-diagnostics.ts +91 -0
- package/src/core/diagnostic-counters.ts +61 -0
- package/src/core/diagnostics.ts +9 -5
- package/src/core/downlink-health.ts +15 -10
- package/src/core/extended-diagnostics.ts +4 -0
- package/src/core/file-transfer-payloads.ts +1 -4
- package/src/core/logging.ts +98 -0
- package/src/core/outbox-entry-builders.ts +15 -2
- package/src/core/outbox-file-transfer-bookkeeping.ts +1 -1
- package/src/core/outbox-file-transfer-failure.ts +2 -5
- package/src/core/outbox-file-transfer-success.ts +1 -4
- package/src/core/outbox-text-push-failure.ts +2 -4
- package/src/core/outbox-text-push-success.ts +1 -1
- package/src/core/persisted-outbox-entry.ts +53 -0
- package/src/core/probe.ts +33 -13
- package/src/core/register-trace.ts +48 -0
- package/src/core/status-meta.ts +77 -0
- package/src/core/status.ts +50 -57
- package/src/messaging/inbound/commands.ts +42 -94
- package/src/messaging/inbound/dispatch.ts +25 -54
- package/src/messaging/inbound/last-route.ts +46 -0
- package/src/messaging/inbound/native-command.ts +49 -0
- package/src/messaging/inbound/native-reply-delivery.ts +43 -0
- package/src/messaging/inbound/parse.ts +3 -3
- package/src/messaging/inbound/runtime-compat.ts +8 -2
- package/src/messaging/outbound/build-send-action.ts +1 -2
- package/src/messaging/outbound/diagnostics.ts +221 -2
- package/src/messaging/outbound/durable-message-adapter.ts +15 -5
- package/src/messaging/outbound/durable-queue-adapter.ts +3 -1
- package/src/messaging/outbound/media.ts +2 -1
- package/src/messaging/outbound/queue-selectors.ts +19 -6
- package/src/messaging/outbound/reasons.ts +2 -0
- package/src/messaging/outbound/reply-enqueue.ts +29 -2
- package/src/messaging/outbound/reply-target-policy.ts +4 -1
- package/src/messaging/outbound/retry-policy.ts +16 -8
- package/src/messaging/outbound/send-params.ts +56 -0
- package/src/messaging/outbound/session-route.ts +1 -1
- package/src/openclaw/reply-runtime.ts +4 -5
- package/src/openclaw/routing-runtime.ts +0 -1
- package/src/openclaw/runtime-surface.ts +29 -0
- package/src/openclaw/sdk-helpers.ts +4 -1
- package/src/plugin/gateway-methods.ts +2 -0
- package/src/plugin/messaging.ts +2 -9
- package/src/plugin/status.ts +15 -5
- package/src/runtime/outbound-ack-timeout.ts +73 -0
- package/src/runtime/outbound-flags.ts +1 -1
- package/src/runtime/outbox-transitions.ts +4 -4
- package/src/runtime/register-trace-runtime.ts +102 -0
- package/src/runtime/status-snapshots.ts +10 -4
- package/src/runtime/status-worker.ts +78 -13
|
@@ -6,30 +6,29 @@ import {
|
|
|
6
6
|
normalizeInboundSessionKey,
|
|
7
7
|
withTaskSessionKey,
|
|
8
8
|
} from '../../core/targets.ts';
|
|
9
|
-
import { handleBncrNativeCommand } from './commands.ts';
|
|
10
9
|
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
import { wrapBncrInboundRecordSessionLabelCorrection } from './session-label.ts';
|
|
10
|
+
readBncrSessionUpdatedAt,
|
|
11
|
+
recordBncrInboundSession,
|
|
12
|
+
resolveBncrInboundSessionStorePath,
|
|
13
|
+
resolveBncrPinnedMainDmOwnerFromAllowlist,
|
|
14
|
+
} from '../../openclaw/inbound-session-runtime.ts';
|
|
17
15
|
import { saveOpenClawChannelMediaBuffer } from '../../openclaw/media-runtime.ts';
|
|
18
16
|
import {
|
|
19
17
|
dispatchOpenClawReplyWithBufferedBlockDispatcher,
|
|
20
18
|
formatOpenClawAgentEnvelope,
|
|
21
19
|
resolveOpenClawEnvelopeFormatOptions,
|
|
22
20
|
} from '../../openclaw/reply-runtime.ts';
|
|
21
|
+
import { resolveOpenClawAgentRoute } from '../../openclaw/routing-runtime.ts';
|
|
22
|
+
import type { OutboundReplyTargetPolicy } from '../outbound/reply-target-policy.ts';
|
|
23
|
+
import { handleBncrNativeCommand } from './commands.ts';
|
|
23
24
|
import {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
} from '
|
|
27
|
-
import {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
resolveBncrPinnedMainDmOwnerFromAllowlist,
|
|
32
|
-
} from '../../openclaw/inbound-session-runtime.ts';
|
|
25
|
+
buildBncrPromptVisibleContextFacts,
|
|
26
|
+
buildBncrStructuredContextFactsFromInboundParts,
|
|
27
|
+
} from './context-facts.ts';
|
|
28
|
+
import { buildBncrInboundRecordUpdateLastRoute } from './last-route.ts';
|
|
29
|
+
import { buildBncrReplyConfig } from './reply-config.ts';
|
|
30
|
+
import { resolveBncrChannelInboundRuntime } from './runtime-compat.ts';
|
|
31
|
+
import { wrapBncrInboundRecordSessionLabelCorrection } from './session-label.ts';
|
|
33
32
|
|
|
34
33
|
type ParsedInbound = ReturnType<typeof import('./parse.ts')['parseBncrInboundParams']>;
|
|
35
34
|
|
|
@@ -68,10 +67,7 @@ export function estimateBase64DecodedBytes(value: string): number {
|
|
|
68
67
|
return Math.max(0, Math.floor((normalized.length * 3) / 4) - padding);
|
|
69
68
|
}
|
|
70
69
|
|
|
71
|
-
export function assertInboundMediaBase64Size(
|
|
72
|
-
value: string,
|
|
73
|
-
maxBytes = INBOUND_MEDIA_MAX_BYTES,
|
|
74
|
-
) {
|
|
70
|
+
export function assertInboundMediaBase64Size(value: string, maxBytes = INBOUND_MEDIA_MAX_BYTES) {
|
|
75
71
|
const estimatedBytes = estimateBase64DecodedBytes(value);
|
|
76
72
|
if (estimatedBytes > maxBytes) {
|
|
77
73
|
throw new Error(
|
|
@@ -161,7 +157,8 @@ async function prepareBncrInboundSessionContext(args: {
|
|
|
161
157
|
groupId,
|
|
162
158
|
userId,
|
|
163
159
|
} = parsed;
|
|
164
|
-
const { accountId, route, resolvedRoute, baseSessionKey, taskSessionKey, dispatchSessionKey } =
|
|
160
|
+
const { accountId, route, resolvedRoute, baseSessionKey, taskSessionKey, dispatchSessionKey } =
|
|
161
|
+
resolution;
|
|
165
162
|
|
|
166
163
|
rememberSessionRoute(baseSessionKey, accountId, route);
|
|
167
164
|
if (taskSessionKey && taskSessionKey !== baseSessionKey) {
|
|
@@ -332,36 +329,6 @@ function buildBncrInboundTurnContext(args: {
|
|
|
332
329
|
});
|
|
333
330
|
}
|
|
334
331
|
|
|
335
|
-
function buildBncrInboundRecordUpdateLastRoute(args: {
|
|
336
|
-
channelId: string;
|
|
337
|
-
peer: ParsedInbound['peer'];
|
|
338
|
-
senderIdForContext: string;
|
|
339
|
-
resolution: BncrInboundConversationResolution;
|
|
340
|
-
pinnedMainDmOwner: string | null;
|
|
341
|
-
}) {
|
|
342
|
-
const { channelId, peer, senderIdForContext, resolution, pinnedMainDmOwner } = args;
|
|
343
|
-
if (peer.kind !== 'direct') return undefined;
|
|
344
|
-
|
|
345
|
-
const sessionKey = resolveOpenClawInboundLastRouteSessionKey({
|
|
346
|
-
route: resolution.resolvedRoute,
|
|
347
|
-
sessionKey: resolution.dispatchSessionKey,
|
|
348
|
-
});
|
|
349
|
-
|
|
350
|
-
return {
|
|
351
|
-
sessionKey,
|
|
352
|
-
channel: channelId,
|
|
353
|
-
to: resolution.canonicalTo,
|
|
354
|
-
accountId: resolution.accountId,
|
|
355
|
-
mainDmOwnerPin:
|
|
356
|
-
sessionKey === resolution.resolvedRoute.mainSessionKey && pinnedMainDmOwner
|
|
357
|
-
? {
|
|
358
|
-
ownerRecipient: pinnedMainDmOwner,
|
|
359
|
-
senderRecipient: senderIdForContext,
|
|
360
|
-
}
|
|
361
|
-
: undefined,
|
|
362
|
-
};
|
|
363
|
-
}
|
|
364
|
-
|
|
365
332
|
function buildBncrInboundReplyRouteFact(
|
|
366
333
|
resolution: BncrInboundConversationResolution,
|
|
367
334
|
): BncrInboundReplyRouteFact {
|
|
@@ -388,6 +355,7 @@ export async function dispatchBncrInbound(params: {
|
|
|
388
355
|
route: any;
|
|
389
356
|
payload: { text?: string; mediaUrl?: string; mediaUrls?: string[] };
|
|
390
357
|
mediaLocalRoots?: readonly string[];
|
|
358
|
+
replyTargetPolicy?: OutboundReplyTargetPolicy;
|
|
391
359
|
}) => Promise<void>;
|
|
392
360
|
setInboundActivity: (accountId: string, at: number) => void;
|
|
393
361
|
scheduleSave: () => void;
|
|
@@ -444,7 +412,7 @@ export async function dispatchBncrInbound(params: {
|
|
|
444
412
|
resolution,
|
|
445
413
|
rememberSessionRoute,
|
|
446
414
|
});
|
|
447
|
-
const { storePath, mediaPath, rawBody
|
|
415
|
+
const { storePath, mediaPath, rawBody } = prepared;
|
|
448
416
|
const replyRouteFact = buildBncrInboundReplyRouteFact(resolution);
|
|
449
417
|
if (!clientId) {
|
|
450
418
|
emitBncrLogLine(
|
|
@@ -480,9 +448,12 @@ export async function dispatchBncrInbound(params: {
|
|
|
480
448
|
: null;
|
|
481
449
|
const updateLastRoute = buildBncrInboundRecordUpdateLastRoute({
|
|
482
450
|
channelId,
|
|
483
|
-
peer,
|
|
451
|
+
peerKind: peer.kind,
|
|
484
452
|
senderIdForContext,
|
|
485
|
-
resolution,
|
|
453
|
+
accountId: resolution.accountId,
|
|
454
|
+
to: resolution.canonicalTo,
|
|
455
|
+
resolvedRoute: resolution.resolvedRoute,
|
|
456
|
+
sessionKey: resolution.dispatchSessionKey,
|
|
486
457
|
pinnedMainDmOwner,
|
|
487
458
|
});
|
|
488
459
|
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { resolveOpenClawInboundLastRouteSessionKey } from '../../openclaw/routing-runtime.ts';
|
|
2
|
+
|
|
3
|
+
export function buildBncrInboundRecordUpdateLastRoute(args: {
|
|
4
|
+
channelId: string;
|
|
5
|
+
peerKind: 'direct' | 'group';
|
|
6
|
+
senderIdForContext: string;
|
|
7
|
+
accountId: string;
|
|
8
|
+
to: string;
|
|
9
|
+
resolvedRoute: {
|
|
10
|
+
sessionKey: string;
|
|
11
|
+
mainSessionKey?: string;
|
|
12
|
+
};
|
|
13
|
+
sessionKey: string;
|
|
14
|
+
pinnedMainDmOwner: string | null;
|
|
15
|
+
}) {
|
|
16
|
+
const {
|
|
17
|
+
channelId,
|
|
18
|
+
peerKind,
|
|
19
|
+
senderIdForContext,
|
|
20
|
+
accountId,
|
|
21
|
+
to,
|
|
22
|
+
resolvedRoute,
|
|
23
|
+
sessionKey,
|
|
24
|
+
pinnedMainDmOwner,
|
|
25
|
+
} = args;
|
|
26
|
+
if (peerKind !== 'direct') return undefined;
|
|
27
|
+
|
|
28
|
+
const inboundLastRouteSessionKey = resolveOpenClawInboundLastRouteSessionKey({
|
|
29
|
+
route: resolvedRoute,
|
|
30
|
+
sessionKey,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
sessionKey: inboundLastRouteSessionKey,
|
|
35
|
+
channel: channelId,
|
|
36
|
+
to,
|
|
37
|
+
accountId,
|
|
38
|
+
mainDmOwnerPin:
|
|
39
|
+
inboundLastRouteSessionKey === resolvedRoute.mainSessionKey && pinnedMainDmOwner
|
|
40
|
+
? {
|
|
41
|
+
ownerRecipient: pinnedMainDmOwner,
|
|
42
|
+
senderRecipient: senderIdForContext,
|
|
43
|
+
}
|
|
44
|
+
: undefined,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export type NativeCommand = {
|
|
2
|
+
command: string;
|
|
3
|
+
raw: string;
|
|
4
|
+
body: string;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export type NativeVerboseCommand = {
|
|
8
|
+
handled: true;
|
|
9
|
+
verboseLevel?: 'on' | 'off' | 'full';
|
|
10
|
+
text: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function parseBncrNativeCommand(text: string): NativeCommand | null {
|
|
14
|
+
const raw = String(text || '').trim();
|
|
15
|
+
if (!raw.startsWith('/')) return null;
|
|
16
|
+
const match = raw.match(/^\/([^\s]+)(?:\s+([\s\S]*))?$/i);
|
|
17
|
+
if (!match) return null;
|
|
18
|
+
|
|
19
|
+
const command = String(match[1] || '')
|
|
20
|
+
.trim()
|
|
21
|
+
.toLowerCase();
|
|
22
|
+
if (!command) return null;
|
|
23
|
+
|
|
24
|
+
const rest = String(match[2] || '').trim();
|
|
25
|
+
const body = command === 'help' ? ['/commands', rest].filter(Boolean).join(' ') : raw;
|
|
26
|
+
return { command, raw, body };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function resolveBncrNativeVerboseCommand(
|
|
30
|
+
command: NativeCommand,
|
|
31
|
+
): NativeVerboseCommand | null {
|
|
32
|
+
if (command.command !== 'verbose') return null;
|
|
33
|
+
const rawLevel = String(command.raw.slice('/verbose'.length) || '')
|
|
34
|
+
.trim()
|
|
35
|
+
.toLowerCase();
|
|
36
|
+
if (!rawLevel || rawLevel === 'status') {
|
|
37
|
+
return { handled: true, text: 'Current verbose level is unchanged.' };
|
|
38
|
+
}
|
|
39
|
+
if (rawLevel === 'on')
|
|
40
|
+
return { handled: true, verboseLevel: 'on', text: 'Verbose logging enabled.' };
|
|
41
|
+
if (rawLevel === 'off')
|
|
42
|
+
return { handled: true, verboseLevel: 'off', text: 'Verbose logging disabled.' };
|
|
43
|
+
if (rawLevel === 'full')
|
|
44
|
+
return { handled: true, verboseLevel: 'full', text: 'Verbose logging set to full.' };
|
|
45
|
+
return {
|
|
46
|
+
handled: true,
|
|
47
|
+
text: `Unrecognized verbose level "${rawLevel}". Valid levels: off, on, full.`,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
type BncrNativeReplyKind = 'tool' | 'block' | 'final';
|
|
2
|
+
|
|
3
|
+
export type BncrNativeReplyDeliveryPayload = {
|
|
4
|
+
text?: string;
|
|
5
|
+
mediaUrl?: string;
|
|
6
|
+
mediaUrls?: string[];
|
|
7
|
+
audioAsVoice?: boolean;
|
|
8
|
+
replyToId?: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function buildBncrNativeReplyDeliveryPayload(args: {
|
|
12
|
+
payload?: {
|
|
13
|
+
text?: string;
|
|
14
|
+
mediaUrl?: string;
|
|
15
|
+
mediaUrls?: string[];
|
|
16
|
+
audioAsVoice?: boolean;
|
|
17
|
+
};
|
|
18
|
+
kind?: BncrNativeReplyKind;
|
|
19
|
+
effectiveReply: {
|
|
20
|
+
blockStreaming: boolean;
|
|
21
|
+
allowTool: boolean;
|
|
22
|
+
};
|
|
23
|
+
msgId?: string;
|
|
24
|
+
}): BncrNativeReplyDeliveryPayload | null {
|
|
25
|
+
const { payload, kind, effectiveReply, msgId } = args;
|
|
26
|
+
const shouldForwardTool = effectiveReply.blockStreaming && effectiveReply.allowTool;
|
|
27
|
+
|
|
28
|
+
if (kind === 'tool' && !shouldForwardTool) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const hasPayload = Boolean(
|
|
33
|
+
payload?.text ||
|
|
34
|
+
payload?.mediaUrl ||
|
|
35
|
+
(Array.isArray(payload?.mediaUrls) && payload.mediaUrls.length > 0),
|
|
36
|
+
);
|
|
37
|
+
if (!hasPayload) return null;
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
...payload,
|
|
41
|
+
replyToId: msgId || undefined,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -49,9 +49,9 @@ export function parseBncrInboundParams(params: any) {
|
|
|
49
49
|
const groupId = asString(params?.groupId || '0').trim() || '0';
|
|
50
50
|
const userId = asString(params?.userId || '').trim();
|
|
51
51
|
const sessionKeyfromroute = asString(params?.sessionKey || '').trim();
|
|
52
|
-
const providedOriginatingTo =
|
|
53
|
-
params?.originatingTo || params?.providedOriginatingTo || params?.to || ''
|
|
54
|
-
|
|
52
|
+
const providedOriginatingTo =
|
|
53
|
+
asString(params?.originatingTo || params?.providedOriginatingTo || params?.to || '').trim() ||
|
|
54
|
+
undefined;
|
|
55
55
|
const clientId = asString(params?.clientId || '').trim() || undefined;
|
|
56
56
|
|
|
57
57
|
const route: BncrRoute = {
|
|
@@ -20,8 +20,14 @@ export function resolveBncrChannelInboundRuntime(api: any): ChannelRuntimeCompat
|
|
|
20
20
|
if (legacyTurnRuntime?.buildContext && legacyTurnRuntime?.run) {
|
|
21
21
|
if (!warnedLegacyTurnRuntime) {
|
|
22
22
|
warnedLegacyTurnRuntime = true;
|
|
23
|
-
const channelRuntimeKeys =
|
|
24
|
-
|
|
23
|
+
const channelRuntimeKeys =
|
|
24
|
+
Object.keys(channelRuntime ?? {})
|
|
25
|
+
.sort()
|
|
26
|
+
.join(',') || 'none';
|
|
27
|
+
const inboundRuntimeKeys =
|
|
28
|
+
Object.keys(inboundRuntime ?? {})
|
|
29
|
+
.sort()
|
|
30
|
+
.join(',') || 'none';
|
|
25
31
|
emitBncrLogLine(
|
|
26
32
|
'warn',
|
|
27
33
|
`[bncr] inbound runtime fallback=turn|preferred=inbound|channelKeys=${channelRuntimeKeys}|inboundKeys=${inboundRuntimeKeys}`,
|
|
@@ -56,8 +56,7 @@ export function buildBncrMessageAction(input: MinimalBncrSendInput): BuiltBncrMe
|
|
|
56
56
|
|
|
57
57
|
const channel = asString(input.channel || 'bncr').trim() || 'bncr';
|
|
58
58
|
const action = asString(input.action || 'send').trim() || 'send';
|
|
59
|
-
const idempotencyKey =
|
|
60
|
-
asString(input.idempotencyKey || '').trim() || `bncr-${randomUUID()}`;
|
|
59
|
+
const idempotencyKey = asString(input.idempotencyKey || '').trim() || `bncr-${randomUUID()}`;
|
|
61
60
|
const accountId =
|
|
62
61
|
asString(pickFirstString(paramsObj.accountId, input.accountId) || '').trim() || undefined;
|
|
63
62
|
|
|
@@ -1,7 +1,226 @@
|
|
|
1
|
-
import type { BncrConnection } from '../../core/types.ts';
|
|
1
|
+
import type { BncrConnection, OutboxEntry } from '../../core/types.ts';
|
|
2
2
|
import { OUTBOUND_TERMINAL_REASON, type OutboundScheduleSource } from './reasons.ts';
|
|
3
3
|
import type { RetryRerouteDecision } from './retry-policy.ts';
|
|
4
4
|
|
|
5
|
+
type OutboxIncidentSummaryInput = {
|
|
6
|
+
pending: number;
|
|
7
|
+
oldestPendingAt?: number | null;
|
|
8
|
+
lastAttemptAt?: number | null;
|
|
9
|
+
lastPushAt?: number | null;
|
|
10
|
+
lastPushError?: string | null;
|
|
11
|
+
hasGatewayContext: boolean;
|
|
12
|
+
activeOutboundConnection: boolean;
|
|
13
|
+
activeOutboundConnectionCount: number;
|
|
14
|
+
prePushGuardSkipCount?: number;
|
|
15
|
+
lastPrePushGuardSkipAt?: number | null;
|
|
16
|
+
lastPrePushGuardSkipReason?: string | null;
|
|
17
|
+
lastAckQueueLatencyMs?: number | null;
|
|
18
|
+
lastAckPushLatencyMs?: number | null;
|
|
19
|
+
lastLateAckQueueLatencyMs?: number | null;
|
|
20
|
+
lastLateAckPushLatencyMs?: number | null;
|
|
21
|
+
lastLateAckOkAt?: number | null;
|
|
22
|
+
adaptiveAckTimeoutMs?: number | null;
|
|
23
|
+
adaptiveAckTimeoutReason?: string | null;
|
|
24
|
+
nowMs?: number;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
function finiteNumberOrNull(value: unknown): number | null {
|
|
28
|
+
if (value == null) return null;
|
|
29
|
+
const n = Number(value);
|
|
30
|
+
return Number.isFinite(n) ? n : null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function positiveAgeMs(nowMs: number, at: unknown): number | null {
|
|
34
|
+
const n = finiteNumberOrNull(at);
|
|
35
|
+
if (n === null || n < 0) return null;
|
|
36
|
+
return Math.max(0, nowMs - n);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function buildOutboxIncidentSummary(input: OutboxIncidentSummaryInput) {
|
|
40
|
+
const pending = Math.max(0, Math.floor(finiteNumberOrNull(input.pending) || 0));
|
|
41
|
+
const nowMs = finiteNumberOrNull(input.nowMs) || Date.now();
|
|
42
|
+
const lastPushError =
|
|
43
|
+
typeof input.lastPushError === 'string' && input.lastPushError ? input.lastPushError : null;
|
|
44
|
+
const lastPrePushGuardSkipReason =
|
|
45
|
+
typeof input.lastPrePushGuardSkipReason === 'string' && input.lastPrePushGuardSkipReason
|
|
46
|
+
? input.lastPrePushGuardSkipReason
|
|
47
|
+
: null;
|
|
48
|
+
const oldestPendingAgeMs = positiveAgeMs(nowMs, input.oldestPendingAt);
|
|
49
|
+
const lastLateAckAgeMs = positiveAgeMs(nowMs, input.lastLateAckOkAt);
|
|
50
|
+
const adaptiveAckTimeoutReason =
|
|
51
|
+
typeof input.adaptiveAckTimeoutReason === 'string' && input.adaptiveAckTimeoutReason
|
|
52
|
+
? input.adaptiveAckTimeoutReason
|
|
53
|
+
: null;
|
|
54
|
+
const hasRecentLateAck = lastLateAckAgeMs !== null && lastLateAckAgeMs <= 3_600_000;
|
|
55
|
+
const hasActiveAdaptiveAck =
|
|
56
|
+
adaptiveAckTimeoutReason !== null &&
|
|
57
|
+
![
|
|
58
|
+
'no-timeout-evidence',
|
|
59
|
+
'no-late-ack-evidence',
|
|
60
|
+
'missing-latency',
|
|
61
|
+
'late-ack-expired',
|
|
62
|
+
'recovered',
|
|
63
|
+
].includes(adaptiveAckTimeoutReason);
|
|
64
|
+
|
|
65
|
+
let type = 'none';
|
|
66
|
+
let severity: 'ok' | 'warning' | 'critical' = 'ok';
|
|
67
|
+
let recommendedAction = 'none';
|
|
68
|
+
|
|
69
|
+
if (pending > 0 && !input.hasGatewayContext) {
|
|
70
|
+
type = 'no-gateway-context';
|
|
71
|
+
severity = 'critical';
|
|
72
|
+
recommendedAction = 'check-channel-message-runtime-context';
|
|
73
|
+
} else if (pending > 0 && !input.activeOutboundConnection) {
|
|
74
|
+
type = 'no-active-outbound-connection';
|
|
75
|
+
severity = 'critical';
|
|
76
|
+
recommendedAction = 'reconnect-bncr-client';
|
|
77
|
+
} else if (pending > 0 && lastPrePushGuardSkipReason) {
|
|
78
|
+
type = lastPrePushGuardSkipReason;
|
|
79
|
+
severity = 'warning';
|
|
80
|
+
recommendedAction = 'inspect-pre-push-guard';
|
|
81
|
+
} else if (pending > 0 && lastPushError) {
|
|
82
|
+
type =
|
|
83
|
+
lastPushError.includes('ack') || lastPushError.includes('timeout')
|
|
84
|
+
? 'ack-timeout'
|
|
85
|
+
: lastPushError;
|
|
86
|
+
severity = type === 'ack-timeout' ? 'critical' : 'warning';
|
|
87
|
+
recommendedAction = 'inspect-ack-and-route-state';
|
|
88
|
+
} else if (pending > 0 && oldestPendingAgeMs !== null && oldestPendingAgeMs >= 120_000) {
|
|
89
|
+
type = 'outbox-backlog';
|
|
90
|
+
severity = 'warning';
|
|
91
|
+
recommendedAction = 'inspect-outbox-drain';
|
|
92
|
+
} else if (pending > 0) {
|
|
93
|
+
type = 'pending-outbox';
|
|
94
|
+
severity = 'warning';
|
|
95
|
+
recommendedAction = 'inspect-outbox-drain';
|
|
96
|
+
} else if (hasRecentLateAck || hasActiveAdaptiveAck) {
|
|
97
|
+
type = 'slow-or-late-ack';
|
|
98
|
+
severity = 'warning';
|
|
99
|
+
recommendedAction = 'inspect-ack-latency';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
active: type !== 'none',
|
|
104
|
+
type,
|
|
105
|
+
severity,
|
|
106
|
+
recommendedAction,
|
|
107
|
+
pending,
|
|
108
|
+
oldestPendingAgeMs,
|
|
109
|
+
lastAttemptAgeMs: positiveAgeMs(nowMs, input.lastAttemptAt),
|
|
110
|
+
lastPushAgeMs: positiveAgeMs(nowMs, input.lastPushAt),
|
|
111
|
+
lastPushError,
|
|
112
|
+
hasGatewayContext: input.hasGatewayContext,
|
|
113
|
+
activeOutboundConnection: input.activeOutboundConnection,
|
|
114
|
+
activeOutboundConnectionCount: Math.max(
|
|
115
|
+
0,
|
|
116
|
+
Math.floor(finiteNumberOrNull(input.activeOutboundConnectionCount) || 0),
|
|
117
|
+
),
|
|
118
|
+
prePushGuardSkipCount: Math.max(
|
|
119
|
+
0,
|
|
120
|
+
Math.floor(finiteNumberOrNull(input.prePushGuardSkipCount) || 0),
|
|
121
|
+
),
|
|
122
|
+
lastPrePushGuardSkipAgeMs: positiveAgeMs(nowMs, input.lastPrePushGuardSkipAt),
|
|
123
|
+
lastPrePushGuardSkipReason,
|
|
124
|
+
ack: {
|
|
125
|
+
lastQueueLatencyMs: finiteNumberOrNull(input.lastAckQueueLatencyMs),
|
|
126
|
+
lastPushLatencyMs: finiteNumberOrNull(input.lastAckPushLatencyMs),
|
|
127
|
+
lastLateQueueLatencyMs: finiteNumberOrNull(input.lastLateAckQueueLatencyMs),
|
|
128
|
+
lastLatePushLatencyMs: finiteNumberOrNull(input.lastLateAckPushLatencyMs),
|
|
129
|
+
lastLateAckAgeMs,
|
|
130
|
+
adaptiveTimeoutMs: finiteNumberOrNull(input.adaptiveAckTimeoutMs),
|
|
131
|
+
adaptiveTimeoutReason: adaptiveAckTimeoutReason,
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function buildExtendedOutboundDiagnostics(input: {
|
|
137
|
+
outbox: Record<string, any>;
|
|
138
|
+
enqueueCount: number;
|
|
139
|
+
lastEnqueueAt?: number | null;
|
|
140
|
+
prePushGuardSkipCount: number;
|
|
141
|
+
lastPrePushGuardSkipAt?: number | null;
|
|
142
|
+
lastPrePushGuardSkipReason?: string | null;
|
|
143
|
+
hasGatewayContext: boolean;
|
|
144
|
+
lastGatewayContextAt?: number | null;
|
|
145
|
+
ackObservability: Record<string, any>;
|
|
146
|
+
nowMs?: number;
|
|
147
|
+
}) {
|
|
148
|
+
const lastEnqueueAt = finiteNumberOrNull(input.lastEnqueueAt);
|
|
149
|
+
const lastPrePushGuardSkipAt = finiteNumberOrNull(input.lastPrePushGuardSkipAt);
|
|
150
|
+
const lastGatewayContextAt = finiteNumberOrNull(input.lastGatewayContextAt);
|
|
151
|
+
return {
|
|
152
|
+
...input.outbox,
|
|
153
|
+
enqueueCount: input.enqueueCount,
|
|
154
|
+
lastEnqueueAt,
|
|
155
|
+
prePushGuardSkipCount: input.prePushGuardSkipCount,
|
|
156
|
+
lastPrePushGuardSkipAt,
|
|
157
|
+
lastPrePushGuardSkipReason: input.lastPrePushGuardSkipReason || null,
|
|
158
|
+
hasGatewayContext: input.hasGatewayContext,
|
|
159
|
+
lastGatewayContextAt,
|
|
160
|
+
incident: buildOutboxIncidentSummary({
|
|
161
|
+
...input.outbox,
|
|
162
|
+
hasGatewayContext: input.hasGatewayContext,
|
|
163
|
+
prePushGuardSkipCount: input.prePushGuardSkipCount,
|
|
164
|
+
lastPrePushGuardSkipAt,
|
|
165
|
+
lastPrePushGuardSkipReason: input.lastPrePushGuardSkipReason || null,
|
|
166
|
+
lastAckQueueLatencyMs: input.ackObservability.lastAckQueueLatencyMs,
|
|
167
|
+
lastAckPushLatencyMs: input.ackObservability.lastAckPushLatencyMs,
|
|
168
|
+
lastLateAckQueueLatencyMs: input.ackObservability.lastLateAckQueueLatencyMs,
|
|
169
|
+
lastLateAckPushLatencyMs: input.ackObservability.lastLateAckPushLatencyMs,
|
|
170
|
+
lastLateAckOkAt: input.ackObservability.lastLateAckOkAt,
|
|
171
|
+
adaptiveAckTimeoutMs: input.ackObservability.currentAckTimeoutMs,
|
|
172
|
+
adaptiveAckTimeoutReason: input.ackObservability.recommendedAckTimeoutReason,
|
|
173
|
+
nowMs: input.nowMs,
|
|
174
|
+
}),
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function buildOutboxQueueDiagnostics(args: {
|
|
179
|
+
accountId: string;
|
|
180
|
+
outboxEntries: Iterable<OutboxEntry>;
|
|
181
|
+
pendingAllAccounts: number;
|
|
182
|
+
pushConnIds: Iterable<string>;
|
|
183
|
+
}) {
|
|
184
|
+
const accountEntries = Array.from(args.outboxEntries).filter(
|
|
185
|
+
(entry) => entry.accountId === args.accountId,
|
|
186
|
+
);
|
|
187
|
+
let oldestPendingAt: number | null = null;
|
|
188
|
+
let newestPendingAt: number | null = null;
|
|
189
|
+
let lastAttemptAt: number | null = null;
|
|
190
|
+
let lastPushAt: number | null = null;
|
|
191
|
+
let lastError: string | null = null;
|
|
192
|
+
|
|
193
|
+
for (const entry of accountEntries) {
|
|
194
|
+
const createdAt = Number(entry.createdAt);
|
|
195
|
+
if (Number.isFinite(createdAt)) {
|
|
196
|
+
oldestPendingAt = oldestPendingAt === null ? createdAt : Math.min(oldestPendingAt, createdAt);
|
|
197
|
+
newestPendingAt = newestPendingAt === null ? createdAt : Math.max(newestPendingAt, createdAt);
|
|
198
|
+
}
|
|
199
|
+
const attemptAt = Number(entry.lastAttemptAt);
|
|
200
|
+
if (Number.isFinite(attemptAt) && (lastAttemptAt === null || attemptAt > lastAttemptAt)) {
|
|
201
|
+
lastAttemptAt = attemptAt;
|
|
202
|
+
}
|
|
203
|
+
const pushAt = Number(entry.lastPushAt);
|
|
204
|
+
if (Number.isFinite(pushAt) && (lastPushAt === null || pushAt > lastPushAt)) {
|
|
205
|
+
lastPushAt = pushAt;
|
|
206
|
+
lastError = entry.lastError || null;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const pushConnIds = Array.from(args.pushConnIds);
|
|
211
|
+
return {
|
|
212
|
+
pending: accountEntries.length,
|
|
213
|
+
pendingAllAccounts: args.pendingAllAccounts,
|
|
214
|
+
oldestPendingAt,
|
|
215
|
+
newestPendingAt,
|
|
216
|
+
lastAttemptAt,
|
|
217
|
+
lastPushAt,
|
|
218
|
+
lastPushError: lastError,
|
|
219
|
+
activeOutboundConnection: pushConnIds.length > 0,
|
|
220
|
+
activeOutboundConnectionCount: pushConnIds.length,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
5
224
|
export function buildOutboxScheduleDebugInfo(args: {
|
|
6
225
|
bridgeId: string;
|
|
7
226
|
accountId?: string | null;
|
|
@@ -187,7 +406,7 @@ export function buildOutboxDrainStuckDebugInfo(args: {
|
|
|
187
406
|
outboxSize: args.outboxSize,
|
|
188
407
|
pending: args.pending,
|
|
189
408
|
runningMs: args.runningMs,
|
|
190
|
-
runningSince: args.runningSince
|
|
409
|
+
runningSince: args.runningSince ?? null,
|
|
191
410
|
hasGatewayContext: args.hasGatewayContext,
|
|
192
411
|
activeConnectionCount: args.activeConnectionCount,
|
|
193
412
|
waiters: {
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { defineChannelMessageAdapter } from 'openclaw/plugin-sdk/channel-message';
|
|
2
1
|
import type {
|
|
3
2
|
ChannelMessageAdapterShape,
|
|
4
3
|
ChannelMessageSendMediaContext,
|
|
@@ -6,15 +5,23 @@ import type {
|
|
|
6
5
|
ChannelMessageSendResult,
|
|
7
6
|
ChannelMessageSendTextContext,
|
|
8
7
|
} from 'openclaw/plugin-sdk/channel-message';
|
|
8
|
+
import { defineChannelMessageAdapter } from 'openclaw/plugin-sdk/channel-message';
|
|
9
9
|
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
buildFileTransferOutboxEntry,
|
|
12
|
+
buildTextOutboxEntry,
|
|
13
|
+
} from '../../core/outbox-entry-builders.ts';
|
|
11
14
|
import type { BncrRoute, OutboxEntry } from '../../core/types.ts';
|
|
12
15
|
import { buildBncrDurableQueuedResult } from './durable-queue-adapter.ts';
|
|
13
16
|
|
|
14
17
|
export type BncrDurableMessageQueuedAdapterDeps<TConfig = unknown> = {
|
|
15
18
|
enqueueText: (ctx: ChannelMessageSendTextContext<TConfig>) => Promise<OutboxEntry> | OutboxEntry;
|
|
16
|
-
enqueueMedia?: (
|
|
17
|
-
|
|
19
|
+
enqueueMedia?: (
|
|
20
|
+
ctx: ChannelMessageSendMediaContext<TConfig>,
|
|
21
|
+
) => Promise<OutboxEntry> | OutboxEntry;
|
|
22
|
+
enqueuePayload?: (
|
|
23
|
+
ctx: ChannelMessageSendPayloadContext<TConfig>,
|
|
24
|
+
) => Promise<OutboxEntry> | OutboxEntry;
|
|
18
25
|
now?: () => number;
|
|
19
26
|
};
|
|
20
27
|
|
|
@@ -97,7 +104,10 @@ export function createBncrDurableMessageQueuedAdapterFromBuilders<TConfig = unkn
|
|
|
97
104
|
});
|
|
98
105
|
}
|
|
99
106
|
|
|
100
|
-
function toChannelMessageSendResult(
|
|
107
|
+
function toChannelMessageSendResult(
|
|
108
|
+
entry: OutboxEntry | undefined,
|
|
109
|
+
now?: () => number,
|
|
110
|
+
): ChannelMessageSendResult {
|
|
101
111
|
if (!entry) throw new Error('bncr durable message adapter did not receive an outbox entry');
|
|
102
112
|
const queued = buildBncrDurableQueuedResult({ entry, sentAt: now?.() });
|
|
103
113
|
return {
|
|
@@ -74,7 +74,9 @@ export function buildBncrDurableQueuedResult(args: {
|
|
|
74
74
|
}): BncrDurableQueuedResult {
|
|
75
75
|
const sentAt = Number.isFinite(args.sentAt) ? Number(args.sentAt) : args.entry.createdAt;
|
|
76
76
|
const platformMessageId = args.entry.messageId;
|
|
77
|
-
const replyToId =
|
|
77
|
+
const replyToId =
|
|
78
|
+
normalizeOutboundReplyToId({ replyToId: args.replyToId ?? extractReplyToId(args.entry) }) ||
|
|
79
|
+
undefined;
|
|
78
80
|
const chatId = formatQueuedReceiptChatId(args.entry.route);
|
|
79
81
|
const meta: BncrDurableQueuedReceiptMeta = {
|
|
80
82
|
status: 'accepted',
|
|
@@ -66,7 +66,8 @@ export function buildBncrMediaOutboundFrame(params: {
|
|
|
66
66
|
messageId: params.messageId,
|
|
67
67
|
idempotencyKey: params.messageId,
|
|
68
68
|
sessionKey: params.sessionKey,
|
|
69
|
-
replyToId:
|
|
69
|
+
replyToId:
|
|
70
|
+
normalizeOutboundReplyToId({ kind: params.kind, replyToId: params.replyToId }) || undefined,
|
|
70
71
|
message: {
|
|
71
72
|
platform: params.route.platform,
|
|
72
73
|
groupId: params.route.groupId,
|
|
@@ -34,7 +34,10 @@ export function computeOutboxRetryWait(nextAttemptAt: number, nowMs: number): nu
|
|
|
34
34
|
return Math.max(0, finiteNumberOr(nextAttemptAt, 0) - finiteNumberOr(nowMs, 0));
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
export function updateMinOutboxDelay(
|
|
37
|
+
export function updateMinOutboxDelay(
|
|
38
|
+
currentDelay: number | null,
|
|
39
|
+
candidateDelay: number | null,
|
|
40
|
+
): number | null {
|
|
38
41
|
if (candidateDelay == null) return currentDelay;
|
|
39
42
|
return currentDelay == null ? candidateDelay : Math.min(currentDelay, candidateDelay);
|
|
40
43
|
}
|
|
@@ -51,7 +54,9 @@ export function selectOutboxTargetAccounts(args: {
|
|
|
51
54
|
const filterAcc = args.accountId ? args.normalizeAccountId(args.accountId) : null;
|
|
52
55
|
if (filterAcc) return [filterAcc];
|
|
53
56
|
return Array.from(
|
|
54
|
-
new Set(
|
|
57
|
+
new Set(
|
|
58
|
+
Array.from(args.outboxEntries).map((entry) => args.normalizeAccountId(entry.accountId)),
|
|
59
|
+
),
|
|
55
60
|
);
|
|
56
61
|
}
|
|
57
62
|
|
|
@@ -112,7 +117,11 @@ export function selectOutboxFileTransferRouteCandidates(args: {
|
|
|
112
117
|
);
|
|
113
118
|
const ownerConnId =
|
|
114
119
|
args.ownerConnId && !attemptedConnIds.has(args.ownerConnId) ? args.ownerConnId : undefined;
|
|
115
|
-
let connIds = ownerConnId
|
|
120
|
+
let connIds = ownerConnId
|
|
121
|
+
? [ownerConnId]
|
|
122
|
+
: filteredCandidates.length > 0
|
|
123
|
+
? filteredCandidates
|
|
124
|
+
: routeCandidates;
|
|
116
125
|
let routeReason: OutboxFileTransferRouteSelection['routeReason'] = ownerConnId
|
|
117
126
|
? 'owner'
|
|
118
127
|
: connIds.length > 0
|
|
@@ -128,7 +137,8 @@ export function selectOutboxFileTransferRouteCandidates(args: {
|
|
|
128
137
|
const filteredRecentInboundConnIds = recentInboundConnIds.filter(
|
|
129
138
|
(connId) => !attemptedConnIds.has(connId),
|
|
130
139
|
);
|
|
131
|
-
connIds =
|
|
140
|
+
connIds =
|
|
141
|
+
filteredRecentInboundConnIds.length > 0 ? filteredRecentInboundConnIds : recentInboundConnIds;
|
|
132
142
|
routeReason = connIds.length > 0 ? 'recent-inbound-fallback' : 'none';
|
|
133
143
|
}
|
|
134
144
|
|
|
@@ -154,9 +164,12 @@ export function selectOutboxRouteCandidates(args: {
|
|
|
154
164
|
const revalidatedCandidates = routeCandidates.filter(
|
|
155
165
|
(connId) => attemptedConnIds.has(connId) && args.isRevalidatedAttemptedConn(connId),
|
|
156
166
|
);
|
|
157
|
-
const preferredCandidates =
|
|
167
|
+
const preferredCandidates =
|
|
168
|
+
unattemptedCandidates.length > 0 ? unattemptedCandidates : routeCandidates;
|
|
158
169
|
const ownerConnId =
|
|
159
|
-
args.ownerConnId && preferredCandidates.includes(args.ownerConnId)
|
|
170
|
+
args.ownerConnId && preferredCandidates.includes(args.ownerConnId)
|
|
171
|
+
? args.ownerConnId
|
|
172
|
+
: undefined;
|
|
160
173
|
let connIds = ownerConnId ? [ownerConnId] : preferredCandidates;
|
|
161
174
|
let routeReason: OutboxRouteSelection['routeReason'] = ownerConnId
|
|
162
175
|
? 'owner'
|