@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
|
@@ -38,6 +38,8 @@ export const OUTBOUND_SCHEDULE_SOURCE = {
|
|
|
38
38
|
RETRY_REROUTE_WAIT: 'retry-reroute-wait',
|
|
39
39
|
// Direct push failure kept entry in outbox and scheduled backoff.
|
|
40
40
|
PUSH_FAIL_WAIT: 'push-fail-wait',
|
|
41
|
+
// Pre-push guard deferred delivery before an actual send attempt.
|
|
42
|
+
PRE_PUSH_GUARD_WAIT: 'pre-push-guard-wait',
|
|
41
43
|
// Per-account flush processed its single-run item budget and yielded to the next drain.
|
|
42
44
|
ACCOUNT_BUDGET_YIELD: 'account-budget-yield',
|
|
43
45
|
// Per-account flush spent its single-run time budget and yielded to the next drain.
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import type { BncrRoute, OutboxEntry } from '../../core/types.ts';
|
|
2
2
|
import { buildReplyMediaFallbackDebugInfo } from './diagnostics.ts';
|
|
3
|
+
import type { OutboundReplyTargetPolicy } from './reply-target-policy.ts';
|
|
3
4
|
import { normalizeOutboundReplyToId } from './reply-target-policy.ts';
|
|
4
5
|
|
|
6
|
+
export type { OutboundReplyTargetPolicy } from './reply-target-policy.ts';
|
|
7
|
+
|
|
5
8
|
export type ReplyPayloadInput = {
|
|
6
9
|
text?: string;
|
|
7
10
|
mediaUrl?: string;
|
|
@@ -21,6 +24,7 @@ export type NormalizedReplyPayload = {
|
|
|
21
24
|
audioAsVoice: boolean;
|
|
22
25
|
kind?: 'tool' | 'block' | 'final';
|
|
23
26
|
replyToId: string;
|
|
27
|
+
replyTargetPolicy: OutboundReplyTargetPolicy;
|
|
24
28
|
};
|
|
25
29
|
|
|
26
30
|
export type ReplyMediaEntriesParams = {
|
|
@@ -51,6 +55,7 @@ export type ReplyMediaFileTransferParams = {
|
|
|
51
55
|
audioAsVoice: boolean;
|
|
52
56
|
kind?: 'tool' | 'block' | 'final';
|
|
53
57
|
replyToId: string;
|
|
58
|
+
replyTargetPolicy: OutboundReplyTargetPolicy;
|
|
54
59
|
createdAt: number;
|
|
55
60
|
};
|
|
56
61
|
|
|
@@ -70,6 +75,7 @@ export type ReplyMediaFallbackTextEntryParams = {
|
|
|
70
75
|
mediaUrl: string;
|
|
71
76
|
kind?: 'tool' | 'block' | 'final';
|
|
72
77
|
replyToId: string;
|
|
78
|
+
replyTargetPolicy: OutboundReplyTargetPolicy;
|
|
73
79
|
fallback: { text: string; reason: string };
|
|
74
80
|
};
|
|
75
81
|
|
|
@@ -85,6 +91,7 @@ export function buildReplyTextOutboxEntry(
|
|
|
85
91
|
text: string;
|
|
86
92
|
kind?: 'tool' | 'block' | 'final';
|
|
87
93
|
replyToId: string;
|
|
94
|
+
replyTargetPolicy: OutboundReplyTargetPolicy;
|
|
88
95
|
},
|
|
89
96
|
helpers: {
|
|
90
97
|
buildTextOutboxEntry: (args: {
|
|
@@ -94,6 +101,7 @@ export function buildReplyTextOutboxEntry(
|
|
|
94
101
|
text: string;
|
|
95
102
|
kind?: 'tool' | 'block' | 'final';
|
|
96
103
|
replyToId?: string;
|
|
104
|
+
replyTargetPolicy?: OutboundReplyTargetPolicy;
|
|
97
105
|
}) => OutboxEntry;
|
|
98
106
|
},
|
|
99
107
|
): OutboxEntry {
|
|
@@ -104,6 +112,7 @@ export function buildReplyTextOutboxEntry(
|
|
|
104
112
|
text: params.text,
|
|
105
113
|
kind: params.kind,
|
|
106
114
|
replyToId: params.replyToId || undefined,
|
|
115
|
+
replyTargetPolicy: params.replyTargetPolicy,
|
|
107
116
|
});
|
|
108
117
|
}
|
|
109
118
|
|
|
@@ -123,6 +132,7 @@ export function enqueueReplyTextEntry(
|
|
|
123
132
|
text: string;
|
|
124
133
|
kind?: 'tool' | 'block' | 'final';
|
|
125
134
|
replyToId?: string;
|
|
135
|
+
replyTargetPolicy?: OutboundReplyTargetPolicy;
|
|
126
136
|
}) => OutboxEntry;
|
|
127
137
|
},
|
|
128
138
|
): void {
|
|
@@ -137,6 +147,7 @@ export function enqueueReplyTextEntry(
|
|
|
137
147
|
text: params.payload.text,
|
|
138
148
|
kind: params.payload.kind,
|
|
139
149
|
replyToId: params.payload.replyToId,
|
|
150
|
+
replyTargetPolicy: params.payload.replyTargetPolicy,
|
|
140
151
|
},
|
|
141
152
|
{ buildTextOutboxEntry: helpers.buildTextOutboxEntry },
|
|
142
153
|
),
|
|
@@ -146,7 +157,11 @@ export function enqueueReplyTextEntry(
|
|
|
146
157
|
export function enqueueReplyMediaFallbackTextEntry(
|
|
147
158
|
params: ReplyMediaFallbackTextEntryParams,
|
|
148
159
|
helpers: {
|
|
149
|
-
logInfo: (
|
|
160
|
+
logInfo: (
|
|
161
|
+
scope: string | undefined,
|
|
162
|
+
message: string,
|
|
163
|
+
options?: { debugOnly?: boolean },
|
|
164
|
+
) => void;
|
|
150
165
|
enqueueOutbound: (entry: OutboxEntry) => void;
|
|
151
166
|
buildTextOutboxEntry: (args: {
|
|
152
167
|
accountId: string;
|
|
@@ -155,6 +170,7 @@ export function enqueueReplyMediaFallbackTextEntry(
|
|
|
155
170
|
text: string;
|
|
156
171
|
kind?: 'tool' | 'block' | 'final';
|
|
157
172
|
replyToId?: string;
|
|
173
|
+
replyTargetPolicy?: OutboundReplyTargetPolicy;
|
|
158
174
|
}) => OutboxEntry;
|
|
159
175
|
},
|
|
160
176
|
): void {
|
|
@@ -172,6 +188,7 @@ export function enqueueReplyMediaFallbackTextEntry(
|
|
|
172
188
|
text: params.fallback.text,
|
|
173
189
|
kind: params.kind,
|
|
174
190
|
replyToId: params.replyToId,
|
|
191
|
+
replyTargetPolicy: params.replyTargetPolicy,
|
|
175
192
|
},
|
|
176
193
|
{ buildTextOutboxEntry: helpers.buildTextOutboxEntry },
|
|
177
194
|
),
|
|
@@ -193,6 +210,7 @@ export function enqueueReplyMediaFileTransferEntry(
|
|
|
193
210
|
audioAsVoice: boolean;
|
|
194
211
|
kind?: 'tool' | 'block' | 'final';
|
|
195
212
|
replyToId?: string;
|
|
213
|
+
replyTargetPolicy?: OutboundReplyTargetPolicy;
|
|
196
214
|
}) => OutboxEntry;
|
|
197
215
|
rememberRecentMediaSend: (args: {
|
|
198
216
|
sessionKey: string;
|
|
@@ -215,6 +233,7 @@ export function enqueueReplyMediaFileTransferEntry(
|
|
|
215
233
|
audioAsVoice: params.audioAsVoice,
|
|
216
234
|
kind: params.kind,
|
|
217
235
|
replyToId: params.replyToId || undefined,
|
|
236
|
+
replyTargetPolicy: params.replyTargetPolicy,
|
|
218
237
|
}),
|
|
219
238
|
);
|
|
220
239
|
helpers.rememberRecentMediaSend({
|
|
@@ -241,6 +260,7 @@ export function enqueueSingleReplyMediaEntry(
|
|
|
241
260
|
mediaUrl: params.mediaUrl,
|
|
242
261
|
kind: params.params.payload.kind,
|
|
243
262
|
replyToId: params.params.payload.replyToId,
|
|
263
|
+
replyTargetPolicy: params.params.payload.replyTargetPolicy,
|
|
244
264
|
fallback: params.fallback,
|
|
245
265
|
});
|
|
246
266
|
return;
|
|
@@ -258,6 +278,7 @@ export function enqueueSingleReplyMediaEntry(
|
|
|
258
278
|
audioAsVoice: params.params.payload.audioAsVoice,
|
|
259
279
|
kind: params.params.payload.kind,
|
|
260
280
|
replyToId: params.params.payload.replyToId,
|
|
281
|
+
replyTargetPolicy: params.params.payload.replyTargetPolicy,
|
|
261
282
|
createdAt: params.currentTime,
|
|
262
283
|
});
|
|
263
284
|
}
|
|
@@ -310,6 +331,7 @@ export function enqueueNormalizedReplyPayload(
|
|
|
310
331
|
export function normalizeReplyPayload(
|
|
311
332
|
payload: ReplyPayloadInput,
|
|
312
333
|
helpers: { asString: (value: unknown, fallback?: string) => string },
|
|
334
|
+
options?: { replyTargetPolicy?: OutboundReplyTargetPolicy },
|
|
313
335
|
): NormalizedReplyPayload {
|
|
314
336
|
const text = helpers.asString(payload?.text || '').trim();
|
|
315
337
|
const mediaUrl = helpers.asString(payload?.mediaUrl || '').trim();
|
|
@@ -324,6 +346,11 @@ export function normalizeReplyPayload(
|
|
|
324
346
|
asVoice: payload?.asVoice === true,
|
|
325
347
|
audioAsVoice: payload?.audioAsVoice === true,
|
|
326
348
|
kind: payload?.kind,
|
|
327
|
-
|
|
349
|
+
replyTargetPolicy: options?.replyTargetPolicy ?? 'agent-default',
|
|
350
|
+
replyToId: normalizeOutboundReplyToId({
|
|
351
|
+
kind: payload?.kind,
|
|
352
|
+
replyToId: payload?.replyToId,
|
|
353
|
+
replyTargetPolicy: options?.replyTargetPolicy,
|
|
354
|
+
}),
|
|
328
355
|
};
|
|
329
356
|
}
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { normalizeReplyToId } from './media-dedupe.ts';
|
|
2
2
|
|
|
3
3
|
export type OutboundReplyKind = 'tool' | 'block' | 'final';
|
|
4
|
+
export type OutboundReplyTargetPolicy = 'agent-default' | 'preserve';
|
|
4
5
|
|
|
5
6
|
const STRIP_TOOL_REPLY_TO_ID = true;
|
|
6
7
|
|
|
7
8
|
export function normalizeOutboundReplyToId(params: {
|
|
8
9
|
kind?: OutboundReplyKind;
|
|
9
10
|
replyToId?: string | null;
|
|
11
|
+
replyTargetPolicy?: OutboundReplyTargetPolicy;
|
|
10
12
|
}) {
|
|
11
|
-
if (params.kind === 'tool' && STRIP_TOOL_REPLY_TO_ID
|
|
13
|
+
if (params.kind === 'tool' && STRIP_TOOL_REPLY_TO_ID && params.replyTargetPolicy !== 'preserve')
|
|
14
|
+
return '';
|
|
12
15
|
return normalizeReplyToId(params.replyToId);
|
|
13
16
|
}
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import type { BncrRoute } from '../../channel.ts';
|
|
2
|
-
|
|
3
1
|
function finiteNonNegativeIntegerOr(value: unknown, fallback: number): number {
|
|
4
2
|
const n = Number(value);
|
|
5
3
|
if (!Number.isFinite(n) || n < 0) return fallback;
|
|
@@ -48,14 +46,18 @@ export function computeRetryRerouteDecision(
|
|
|
48
46
|
? input.attemptedConnIds.filter((v): v is string => typeof v === 'string' && !!v)
|
|
49
47
|
: [];
|
|
50
48
|
const currentConnId = `${input.currentConnId || ''}`.trim();
|
|
51
|
-
if (currentConnId && !attemptedConnIds.includes(currentConnId))
|
|
49
|
+
if (currentConnId && !attemptedConnIds.includes(currentConnId))
|
|
50
|
+
attemptedConnIds.push(currentConnId);
|
|
52
51
|
|
|
53
52
|
const availableConnIds = Array.isArray(input.availableConnIds)
|
|
54
53
|
? input.availableConnIds.filter((v): v is string => typeof v === 'string' && !!v)
|
|
55
54
|
: [];
|
|
56
55
|
const revalidatedConnIds = attemptedConnIds.filter((connId) => availableConnIds.includes(connId));
|
|
57
|
-
const hasUntriedAlternative = availableConnIds.some(
|
|
58
|
-
|
|
56
|
+
const hasUntriedAlternative = availableConnIds.some(
|
|
57
|
+
(connId) => !attemptedConnIds.includes(connId),
|
|
58
|
+
);
|
|
59
|
+
const shouldFastReroute =
|
|
60
|
+
input.requireAck && input.currentFastReroutePending !== true && hasUntriedAlternative;
|
|
59
61
|
|
|
60
62
|
const currentRetryCount = finiteNonNegativeIntegerOr(input.currentRetryCount, 0);
|
|
61
63
|
const currentRouteAttemptRound = finiteNonNegativeIntegerOr(input.currentRouteAttemptRound, 0);
|
|
@@ -73,10 +75,16 @@ export function computeRetryRerouteDecision(
|
|
|
73
75
|
};
|
|
74
76
|
}
|
|
75
77
|
|
|
76
|
-
const nextAttemptAt = shouldFastReroute
|
|
78
|
+
const nextAttemptAt = shouldFastReroute
|
|
79
|
+
? input.nowMs + 1_000
|
|
80
|
+
: input.nowMs + deps.backoffMs(nextRetryCount);
|
|
77
81
|
const lastError = input.requireAck ? 'push-ack-timeout' : 'push-delivery-unconfirmed';
|
|
78
|
-
const routeAttemptRound = hasUntriedAlternative
|
|
79
|
-
|
|
82
|
+
const routeAttemptRound = hasUntriedAlternative
|
|
83
|
+
? currentRouteAttemptRound
|
|
84
|
+
: currentRouteAttemptRound + 1;
|
|
85
|
+
const fastReroutePending = hasUntriedAlternative
|
|
86
|
+
? shouldFastReroute || input.currentFastReroutePending === true
|
|
87
|
+
: false;
|
|
80
88
|
|
|
81
89
|
return {
|
|
82
90
|
kind: 'retry',
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { normalizeAccountId } from '../../core/accounts.ts';
|
|
2
|
+
import { readOpenClawBooleanParam, readOpenClawStringParam } from '../../openclaw/sdk-helpers.ts';
|
|
3
|
+
|
|
4
|
+
export type NormalizedBncrSendParams = {
|
|
5
|
+
to: string;
|
|
6
|
+
accountId: string;
|
|
7
|
+
message: string;
|
|
8
|
+
caption: string;
|
|
9
|
+
mediaUrl?: string;
|
|
10
|
+
asVoice: boolean;
|
|
11
|
+
audioAsVoice: boolean;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
15
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function normalizeBncrSendParams(input: {
|
|
19
|
+
params: unknown;
|
|
20
|
+
accountId: string;
|
|
21
|
+
}): NormalizedBncrSendParams {
|
|
22
|
+
const paramsObj = isPlainObject(input.params) ? input.params : {};
|
|
23
|
+
const to = readOpenClawStringParam(paramsObj, 'to', { required: true });
|
|
24
|
+
const resolvedAccountId = normalizeAccountId(
|
|
25
|
+
readOpenClawStringParam(paramsObj, 'accountId') ?? input.accountId,
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
const message = readOpenClawStringParam(paramsObj, 'message', { allowEmpty: true }) ?? '';
|
|
29
|
+
const caption = readOpenClawStringParam(paramsObj, 'caption', { allowEmpty: true }) ?? '';
|
|
30
|
+
const mediaUrl =
|
|
31
|
+
readOpenClawStringParam(paramsObj, 'media', { trim: false }) ??
|
|
32
|
+
readOpenClawStringParam(paramsObj, 'path', { trim: false }) ??
|
|
33
|
+
readOpenClawStringParam(paramsObj, 'filePath', { trim: false }) ??
|
|
34
|
+
readOpenClawStringParam(paramsObj, 'mediaUrl', { trim: false });
|
|
35
|
+
const asVoice = readOpenClawBooleanParam(paramsObj, 'asVoice') ?? false;
|
|
36
|
+
const audioAsVoice = readOpenClawBooleanParam(paramsObj, 'audioAsVoice') ?? false;
|
|
37
|
+
|
|
38
|
+
if (asVoice && !mediaUrl) throw new Error('send voice requires media path');
|
|
39
|
+
|
|
40
|
+
const normalizedMessage = mediaUrl ? '' : message || caption || '';
|
|
41
|
+
const normalizedCaption = mediaUrl ? caption || message || '' : '';
|
|
42
|
+
|
|
43
|
+
if (!normalizedMessage.trim() && !normalizedCaption.trim() && !mediaUrl) {
|
|
44
|
+
throw new Error('send requires message or media');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
to,
|
|
49
|
+
accountId: resolvedAccountId,
|
|
50
|
+
message: normalizedMessage,
|
|
51
|
+
caption: normalizedCaption,
|
|
52
|
+
mediaUrl: mediaUrl || undefined,
|
|
53
|
+
asVoice,
|
|
54
|
+
audioAsVoice,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { buildOpenClawChannelOutboundSessionRoute } from '../../openclaw/session-route-runtime.ts';
|
|
2
1
|
import {
|
|
3
2
|
buildCanonicalBncrSessionKey,
|
|
4
3
|
formatDisplayScope,
|
|
@@ -7,6 +6,7 @@ import {
|
|
|
7
6
|
routeScopeToHex,
|
|
8
7
|
} from '../../core/targets.ts';
|
|
9
8
|
import type { BncrRoute } from '../../core/types.ts';
|
|
9
|
+
import { buildOpenClawChannelOutboundSessionRoute } from '../../openclaw/session-route-runtime.ts';
|
|
10
10
|
|
|
11
11
|
type ResolveBncrOutboundSessionRouteParams = {
|
|
12
12
|
cfg: any;
|
|
@@ -44,10 +44,7 @@ function resolveReplyApi(api: RuntimeApiHolder): RuntimeReplyApi {
|
|
|
44
44
|
return reply;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
export function resolveOpenClawEnvelopeFormatOptions(
|
|
48
|
-
api: RuntimeApiHolder,
|
|
49
|
-
cfg: unknown,
|
|
50
|
-
): unknown {
|
|
47
|
+
export function resolveOpenClawEnvelopeFormatOptions(api: RuntimeApiHolder, cfg: unknown): unknown {
|
|
51
48
|
const reply = resolveReplyApi(api);
|
|
52
49
|
if (typeof reply.resolveEnvelopeFormatOptions !== 'function') {
|
|
53
50
|
throw new Error('OpenClaw channel reply resolveEnvelopeFormatOptions API is unavailable');
|
|
@@ -98,7 +95,9 @@ export async function dispatchOpenClawReplyWithBufferedBlockDispatcher(
|
|
|
98
95
|
): Promise<unknown> {
|
|
99
96
|
const reply = resolveReplyApi(api);
|
|
100
97
|
if (typeof reply.dispatchReplyWithBufferedBlockDispatcher !== 'function') {
|
|
101
|
-
throw new Error(
|
|
98
|
+
throw new Error(
|
|
99
|
+
'OpenClaw channel reply dispatchReplyWithBufferedBlockDispatcher API is unavailable',
|
|
100
|
+
);
|
|
102
101
|
}
|
|
103
102
|
return reply.dispatchReplyWithBufferedBlockDispatcher(params);
|
|
104
103
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export type OpenClawChannelRuntimeSurfaceDiagnostics = {
|
|
2
|
+
channel: {
|
|
3
|
+
inbound: boolean;
|
|
4
|
+
media: boolean;
|
|
5
|
+
reply: boolean;
|
|
6
|
+
routing: boolean;
|
|
7
|
+
session: boolean;
|
|
8
|
+
};
|
|
9
|
+
missing: string[];
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function buildOpenClawChannelRuntimeSurfaceDiagnostics(
|
|
13
|
+
api: unknown,
|
|
14
|
+
): OpenClawChannelRuntimeSurfaceDiagnostics {
|
|
15
|
+
const channelRuntime = (api as any)?.runtime?.channel;
|
|
16
|
+
const surfaces = {
|
|
17
|
+
inbound: Boolean(channelRuntime?.inbound),
|
|
18
|
+
media: Boolean(channelRuntime?.media),
|
|
19
|
+
reply: Boolean(channelRuntime?.reply),
|
|
20
|
+
routing: Boolean(channelRuntime?.routing),
|
|
21
|
+
session: Boolean(channelRuntime?.session),
|
|
22
|
+
};
|
|
23
|
+
return {
|
|
24
|
+
channel: surfaces,
|
|
25
|
+
missing: Object.entries(surfaces)
|
|
26
|
+
.filter(([, present]) => !present)
|
|
27
|
+
.map(([name]) => name),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -4,7 +4,10 @@ import {
|
|
|
4
4
|
jsonResult as sdkJsonResult,
|
|
5
5
|
setAccountEnabledInConfigSection as sdkSetAccountEnabledInConfigSection,
|
|
6
6
|
} from 'openclaw/plugin-sdk/core';
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
readJsonFileWithFallback as sdkReadJsonFileWithFallback,
|
|
9
|
+
writeJsonFileAtomically as sdkWriteJsonFileAtomically,
|
|
10
|
+
} from 'openclaw/plugin-sdk/json-store';
|
|
8
11
|
import { readStringParam as sdkReadStringParam } from 'openclaw/plugin-sdk/param-readers';
|
|
9
12
|
import { createDefaultChannelRuntimeState as sdkCreateDefaultChannelRuntimeState } from 'openclaw/plugin-sdk/status-helpers';
|
|
10
13
|
import { extractToolSend as sdkExtractToolSend } from 'openclaw/plugin-sdk/tool-send';
|
package/src/plugin/messaging.ts
CHANGED
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
import { BNCR_DEFAULT_ACCOUNT_ID, normalizeAccountId } from '../core/accounts.ts';
|
|
2
|
-
import {
|
|
3
|
-
formatDisplayScope,
|
|
4
|
-
formatTargetDisplay,
|
|
5
|
-
parseExplicitTarget,
|
|
6
|
-
} from '../core/targets.ts';
|
|
2
|
+
import { formatDisplayScope, formatTargetDisplay, parseExplicitTarget } from '../core/targets.ts';
|
|
7
3
|
import { resolveBncrOutboundSessionRoute } from '../messaging/outbound/session-route.ts';
|
|
8
4
|
import {
|
|
9
5
|
looksLikeBncrExplicitTarget,
|
|
@@ -38,10 +34,7 @@ function resolveMessagingCanonicalAgentId(
|
|
|
38
34
|
cfg: any,
|
|
39
35
|
accountId: string,
|
|
40
36
|
) {
|
|
41
|
-
return (
|
|
42
|
-
runtimeBridge.canonicalAgentId ||
|
|
43
|
-
runtimeBridge.ensureCanonicalAgentId({ cfg, accountId })
|
|
44
|
-
);
|
|
37
|
+
return runtimeBridge.canonicalAgentId || runtimeBridge.ensureCanonicalAgentId({ cfg, accountId });
|
|
45
38
|
}
|
|
46
39
|
|
|
47
40
|
export function createBncrMessagingExplicitTargetParser(
|
package/src/plugin/status.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
BNCR_DEFAULT_ACCOUNT_ID,
|
|
3
|
+
resolveAccount,
|
|
4
|
+
resolveDefaultDisplayName,
|
|
5
|
+
} from '../core/accounts.ts';
|
|
2
6
|
import { buildAccountStatusSnapshot } from '../core/status.ts';
|
|
3
7
|
import { createOpenClawDefaultChannelRuntimeState } from '../openclaw/sdk-helpers.ts';
|
|
4
8
|
|
|
@@ -18,13 +22,19 @@ export function createBncrStatusSurface(getBridge: () => BncrStatusBridge) {
|
|
|
18
22
|
},
|
|
19
23
|
buildAccountSnapshot: async ({ account, runtime }: any) => {
|
|
20
24
|
const runtimeBridge = getBridge();
|
|
21
|
-
const
|
|
25
|
+
const accountId = account?.accountId || BNCR_DEFAULT_ACCOUNT_ID;
|
|
26
|
+
const snapshotAccount = {
|
|
27
|
+
accountId,
|
|
28
|
+
name: account?.name,
|
|
29
|
+
enabled: account?.enabled,
|
|
30
|
+
};
|
|
31
|
+
const rt = runtime || runtimeBridge.getAccountRuntimeSnapshot(accountId);
|
|
22
32
|
return buildAccountStatusSnapshot({
|
|
23
|
-
account,
|
|
33
|
+
account: snapshotAccount,
|
|
24
34
|
runtime: rt,
|
|
25
|
-
healthSummary: runtimeBridge.getStatusHeadline(
|
|
35
|
+
healthSummary: runtimeBridge.getStatusHeadline(accountId),
|
|
26
36
|
// default 名不可隐藏时,统一展示稳定默认值
|
|
27
|
-
displayName: resolveDefaultDisplayName(account?.name,
|
|
37
|
+
displayName: resolveDefaultDisplayName(account?.name, accountId),
|
|
28
38
|
});
|
|
29
39
|
},
|
|
30
40
|
resolveAccountState: ({ enabled, configured, account, cfg, runtime }: any) => {
|
|
@@ -65,6 +65,15 @@ export function computeBncrRecommendedAckTimeoutMs(args: ComputeBncrRecommendedA
|
|
|
65
65
|
return Math.min(args.maxAckTimeoutMs, Math.max(args.minAckTimeoutMs, recommended));
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
export function resolveBncrRuntimeAckTimeoutDecision(args: ComputeBncrRecommendedAckTimeoutArgs) {
|
|
69
|
+
const timeoutMs = computeBncrRecommendedAckTimeoutMs(args);
|
|
70
|
+
const reason = computeBncrRecommendedAckTimeoutReason({
|
|
71
|
+
...args,
|
|
72
|
+
recommendedAckTimeoutMs: timeoutMs,
|
|
73
|
+
});
|
|
74
|
+
return { timeoutMs, reason };
|
|
75
|
+
}
|
|
76
|
+
|
|
68
77
|
function finiteNumberOr(value: unknown, fallback: number) {
|
|
69
78
|
return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
|
|
70
79
|
}
|
|
@@ -94,3 +103,67 @@ export function buildBncrRuntimeAckStrategy(args: {
|
|
|
94
103
|
recovered: ackObservability.adaptiveAckRecovered === true,
|
|
95
104
|
};
|
|
96
105
|
}
|
|
106
|
+
|
|
107
|
+
export function buildBncrRuntimeAckObservability(args: {
|
|
108
|
+
lastAckOkAt: number | null;
|
|
109
|
+
lastAckTimeoutAt: number | null;
|
|
110
|
+
recentAckTimeoutCount: number;
|
|
111
|
+
lateAckOkCount: number;
|
|
112
|
+
lastLateAckOkAt: number | null;
|
|
113
|
+
adaptiveAckRecoveryOkCount: number;
|
|
114
|
+
lastAckQueueLatencyMs: number | null;
|
|
115
|
+
lastAckPushLatencyMs: number | null;
|
|
116
|
+
lastLateAckQueueLatencyMs: number | null;
|
|
117
|
+
lastLateAckPushLatencyMs: number | null;
|
|
118
|
+
adaptiveAckTimeoutEnabled: boolean;
|
|
119
|
+
defaultAckTimeoutMs: number;
|
|
120
|
+
currentAckTimeoutMs: number;
|
|
121
|
+
minAckTimeoutMs: number;
|
|
122
|
+
maxAckTimeoutMs: number;
|
|
123
|
+
lateAckObservationTtlMs: number;
|
|
124
|
+
recoveryOkThreshold: number;
|
|
125
|
+
nowMs: number;
|
|
126
|
+
}) {
|
|
127
|
+
const lastLateAckAgeMs =
|
|
128
|
+
typeof args.lastLateAckOkAt === 'number' && args.lastLateAckOkAt > 0
|
|
129
|
+
? Math.max(0, args.nowMs - args.lastLateAckOkAt)
|
|
130
|
+
: null;
|
|
131
|
+
const lateAckObservationExpired =
|
|
132
|
+
typeof lastLateAckAgeMs === 'number' && lastLateAckAgeMs > args.lateAckObservationTtlMs;
|
|
133
|
+
const adaptiveAckRecovered = args.adaptiveAckRecoveryOkCount >= args.recoveryOkThreshold;
|
|
134
|
+
const ackTimeoutDecision = resolveBncrRuntimeAckTimeoutDecision({
|
|
135
|
+
lateAckOkCount: args.lateAckOkCount,
|
|
136
|
+
recentAckTimeoutCount: args.recentAckTimeoutCount,
|
|
137
|
+
lastLateAckPushLatencyMs: args.lastLateAckPushLatencyMs,
|
|
138
|
+
lastLateAckOkAt: args.lastLateAckOkAt,
|
|
139
|
+
adaptiveAckRecoveryOkCount: args.adaptiveAckRecoveryOkCount,
|
|
140
|
+
nowMs: args.nowMs,
|
|
141
|
+
defaultAckTimeoutMs: args.defaultAckTimeoutMs,
|
|
142
|
+
minAckTimeoutMs: args.minAckTimeoutMs,
|
|
143
|
+
maxAckTimeoutMs: args.maxAckTimeoutMs,
|
|
144
|
+
lateAckObservationTtlMs: args.lateAckObservationTtlMs,
|
|
145
|
+
recoveryOkThreshold: args.recoveryOkThreshold,
|
|
146
|
+
});
|
|
147
|
+
return {
|
|
148
|
+
lastAckOkAt: args.lastAckOkAt,
|
|
149
|
+
lastAckTimeoutAt: args.lastAckTimeoutAt,
|
|
150
|
+
recentAckTimeoutCount: args.recentAckTimeoutCount,
|
|
151
|
+
lateAckOkCount: args.lateAckOkCount,
|
|
152
|
+
lastLateAckOkAt: args.lastLateAckOkAt,
|
|
153
|
+
lastLateAckAgeMs,
|
|
154
|
+
lateAckObservationTtlMs: args.lateAckObservationTtlMs,
|
|
155
|
+
lateAckObservationExpired,
|
|
156
|
+
adaptiveAckRecoveryOkCount: args.adaptiveAckRecoveryOkCount,
|
|
157
|
+
adaptiveAckRecoveryOkThreshold: args.recoveryOkThreshold,
|
|
158
|
+
adaptiveAckRecovered,
|
|
159
|
+
lastAckQueueLatencyMs: args.lastAckQueueLatencyMs,
|
|
160
|
+
lastAckPushLatencyMs: args.lastAckPushLatencyMs,
|
|
161
|
+
lastLateAckQueueLatencyMs: args.lastLateAckQueueLatencyMs,
|
|
162
|
+
lastLateAckPushLatencyMs: args.lastLateAckPushLatencyMs,
|
|
163
|
+
adaptiveAckTimeoutEnabled: args.adaptiveAckTimeoutEnabled,
|
|
164
|
+
defaultAckTimeoutMs: args.defaultAckTimeoutMs,
|
|
165
|
+
currentAckTimeoutMs: args.currentAckTimeoutMs,
|
|
166
|
+
recommendedAckTimeoutMs: ackTimeoutDecision.timeoutMs,
|
|
167
|
+
recommendedAckTimeoutReason: ackTimeoutDecision.reason,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { BNCR_DEFAULT_ACCOUNT_ID, CHANNEL_ID, normalizeAccountId } from '../core/accounts.ts';
|
|
2
2
|
import { getOpenClawRuntimeConfig } from '../openclaw/config-runtime.ts';
|
|
3
3
|
|
|
4
4
|
type RuntimeApiHolder = { api: unknown };
|
|
@@ -103,9 +103,7 @@ export function buildBncrAckOkTelemetryPatch(args: {
|
|
|
103
103
|
const defaultAckTimeoutMs = Math.max(0, finiteNumberOr(args.defaultAckTimeoutMs, 0));
|
|
104
104
|
const ackQueueLatencyMs = Math.max(0, ackAt - finiteNumberOr(args.entry.createdAt, ackAt));
|
|
105
105
|
const ackPushLatencyMs =
|
|
106
|
-
typeof args.entry.lastPushAt === 'number'
|
|
107
|
-
? Math.max(0, ackAt - args.entry.lastPushAt)
|
|
108
|
-
: null;
|
|
106
|
+
typeof args.entry.lastPushAt === 'number' ? Math.max(0, ackAt - args.entry.lastPushAt) : null;
|
|
109
107
|
const lateAccepted = args.entry.awaitingRetryPush === true;
|
|
110
108
|
return {
|
|
111
109
|
ackAt,
|
|
@@ -114,6 +112,8 @@ export function buildBncrAckOkTelemetryPatch(args: {
|
|
|
114
112
|
lateAccepted,
|
|
115
113
|
shouldResetAdaptiveAckRecovery: lateAccepted,
|
|
116
114
|
shouldIncrementAdaptiveAckRecovery:
|
|
117
|
-
!lateAccepted &&
|
|
115
|
+
!lateAccepted &&
|
|
116
|
+
typeof ackPushLatencyMs === 'number' &&
|
|
117
|
+
ackPushLatencyMs <= defaultAckTimeoutMs,
|
|
118
118
|
};
|
|
119
119
|
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import {
|
|
2
|
+
appendBoundedRegisterTrace,
|
|
3
|
+
buildRegisterDriftSnapshot,
|
|
4
|
+
buildRegisterTraceEntry,
|
|
5
|
+
buildRegisterTraceSummary,
|
|
6
|
+
type RegisterDriftSnapshot,
|
|
7
|
+
type RegisterTraceEntry,
|
|
8
|
+
type RegisterTraceSummary,
|
|
9
|
+
} from '../core/register-trace.ts';
|
|
10
|
+
|
|
11
|
+
export type RegisterTraceRuntimeState = {
|
|
12
|
+
registerCount: number;
|
|
13
|
+
apiGeneration: number;
|
|
14
|
+
firstRegisterAt: number | null;
|
|
15
|
+
lastRegisterAt: number | null;
|
|
16
|
+
lastApiRebindAt: number | null;
|
|
17
|
+
pluginSource: string | null;
|
|
18
|
+
pluginVersion: string | null;
|
|
19
|
+
lastApiInstanceId: string | null;
|
|
20
|
+
lastRegistryFingerprint: string | null;
|
|
21
|
+
lastDriftSnapshot: RegisterDriftSnapshot | null;
|
|
22
|
+
registerTraceRecent: RegisterTraceEntry[];
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type RegisterTraceRuntimeMeta = {
|
|
26
|
+
source?: string;
|
|
27
|
+
pluginVersion?: string;
|
|
28
|
+
apiRebound?: boolean;
|
|
29
|
+
apiInstanceId?: string;
|
|
30
|
+
registryFingerprint?: string;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export function buildRegisterTraceRuntimeSummary(args: {
|
|
34
|
+
state: Pick<RegisterTraceRuntimeState, 'registerTraceRecent' | 'firstRegisterAt'>;
|
|
35
|
+
warmupWindowMs: number;
|
|
36
|
+
}): RegisterTraceSummary {
|
|
37
|
+
return buildRegisterTraceSummary({
|
|
38
|
+
traceRecent: args.state.registerTraceRecent,
|
|
39
|
+
firstRegisterAt: args.state.firstRegisterAt,
|
|
40
|
+
warmupWindowMs: args.warmupWindowMs,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function noteRegisterTraceRuntime(args: {
|
|
45
|
+
state: RegisterTraceRuntimeState;
|
|
46
|
+
meta: RegisterTraceRuntimeMeta;
|
|
47
|
+
ts: number;
|
|
48
|
+
stack: string;
|
|
49
|
+
bridgeId: string;
|
|
50
|
+
gatewayPid: number;
|
|
51
|
+
warmupWindowMs: number;
|
|
52
|
+
maxTraceEntries?: number;
|
|
53
|
+
}): { trace: RegisterTraceEntry; summary: RegisterTraceSummary; capturedDriftSnapshot: boolean } {
|
|
54
|
+
const { state, meta } = args;
|
|
55
|
+
state.registerCount += 1;
|
|
56
|
+
if (state.firstRegisterAt == null) state.firstRegisterAt = args.ts;
|
|
57
|
+
state.lastRegisterAt = args.ts;
|
|
58
|
+
if (meta.apiRebound) {
|
|
59
|
+
state.apiGeneration += 1;
|
|
60
|
+
state.lastApiRebindAt = args.ts;
|
|
61
|
+
} else if (state.registerCount === 1 && state.apiGeneration === 0) {
|
|
62
|
+
state.apiGeneration = 1;
|
|
63
|
+
}
|
|
64
|
+
if (meta.source) state.pluginSource = meta.source;
|
|
65
|
+
if (meta.pluginVersion) state.pluginVersion = meta.pluginVersion;
|
|
66
|
+
if (meta.apiInstanceId) state.lastApiInstanceId = meta.apiInstanceId;
|
|
67
|
+
if (meta.registryFingerprint) state.lastRegistryFingerprint = meta.registryFingerprint;
|
|
68
|
+
|
|
69
|
+
const trace = buildRegisterTraceEntry({
|
|
70
|
+
ts: args.ts,
|
|
71
|
+
bridgeId: args.bridgeId,
|
|
72
|
+
gatewayPid: args.gatewayPid,
|
|
73
|
+
registerCount: state.registerCount,
|
|
74
|
+
apiGeneration: state.apiGeneration,
|
|
75
|
+
apiRebound: meta.apiRebound === true,
|
|
76
|
+
apiInstanceId: state.lastApiInstanceId,
|
|
77
|
+
registryFingerprint: state.lastRegistryFingerprint,
|
|
78
|
+
source: state.pluginSource,
|
|
79
|
+
pluginVersion: state.pluginVersion,
|
|
80
|
+
stack: args.stack,
|
|
81
|
+
});
|
|
82
|
+
appendBoundedRegisterTrace(state.registerTraceRecent, trace, args.maxTraceEntries ?? 12);
|
|
83
|
+
|
|
84
|
+
const summary = buildRegisterTraceRuntimeSummary({
|
|
85
|
+
state,
|
|
86
|
+
warmupWindowMs: args.warmupWindowMs,
|
|
87
|
+
});
|
|
88
|
+
const capturedDriftSnapshot = summary.postWarmupRegisterCount > 0;
|
|
89
|
+
if (capturedDriftSnapshot) {
|
|
90
|
+
state.lastDriftSnapshot = buildRegisterDriftSnapshot({
|
|
91
|
+
capturedAt: args.ts,
|
|
92
|
+
registerCount: state.registerCount,
|
|
93
|
+
apiGeneration: state.apiGeneration,
|
|
94
|
+
summary,
|
|
95
|
+
apiInstanceId: state.lastApiInstanceId,
|
|
96
|
+
registryFingerprint: state.lastRegistryFingerprint,
|
|
97
|
+
traceRecent: state.registerTraceRecent,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return { trace, summary, capturedDriftSnapshot };
|
|
102
|
+
}
|