@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.
Files changed (61) hide show
  1. package/dist/index.js +7 -3
  2. package/index.ts +11 -10
  3. package/openclaw.plugin.json +21 -0
  4. package/package.json +4 -4
  5. package/scripts/check-pack.mjs +112 -22
  6. package/scripts/check-register-drift.mjs +91 -65
  7. package/scripts/selfcheck.mjs +79 -3
  8. package/src/channel.ts +549 -810
  9. package/src/core/accounts.ts +1 -1
  10. package/src/core/connection-capability.ts +2 -2
  11. package/src/core/connection-reachability.ts +112 -1
  12. package/src/core/dead-letter-diagnostics.ts +91 -0
  13. package/src/core/diagnostic-counters.ts +61 -0
  14. package/src/core/diagnostics.ts +9 -5
  15. package/src/core/downlink-health.ts +15 -10
  16. package/src/core/extended-diagnostics.ts +4 -0
  17. package/src/core/file-transfer-payloads.ts +1 -4
  18. package/src/core/logging.ts +98 -0
  19. package/src/core/outbox-entry-builders.ts +15 -2
  20. package/src/core/outbox-file-transfer-bookkeeping.ts +1 -1
  21. package/src/core/outbox-file-transfer-failure.ts +2 -5
  22. package/src/core/outbox-file-transfer-success.ts +1 -4
  23. package/src/core/outbox-text-push-failure.ts +2 -4
  24. package/src/core/outbox-text-push-success.ts +1 -1
  25. package/src/core/persisted-outbox-entry.ts +53 -0
  26. package/src/core/probe.ts +33 -13
  27. package/src/core/register-trace.ts +48 -0
  28. package/src/core/status-meta.ts +77 -0
  29. package/src/core/status.ts +50 -57
  30. package/src/messaging/inbound/commands.ts +42 -94
  31. package/src/messaging/inbound/dispatch.ts +25 -54
  32. package/src/messaging/inbound/last-route.ts +46 -0
  33. package/src/messaging/inbound/native-command.ts +49 -0
  34. package/src/messaging/inbound/native-reply-delivery.ts +43 -0
  35. package/src/messaging/inbound/parse.ts +3 -3
  36. package/src/messaging/inbound/runtime-compat.ts +8 -2
  37. package/src/messaging/outbound/build-send-action.ts +1 -2
  38. package/src/messaging/outbound/diagnostics.ts +221 -2
  39. package/src/messaging/outbound/durable-message-adapter.ts +15 -5
  40. package/src/messaging/outbound/durable-queue-adapter.ts +3 -1
  41. package/src/messaging/outbound/media.ts +2 -1
  42. package/src/messaging/outbound/queue-selectors.ts +19 -6
  43. package/src/messaging/outbound/reasons.ts +2 -0
  44. package/src/messaging/outbound/reply-enqueue.ts +29 -2
  45. package/src/messaging/outbound/reply-target-policy.ts +4 -1
  46. package/src/messaging/outbound/retry-policy.ts +16 -8
  47. package/src/messaging/outbound/send-params.ts +56 -0
  48. package/src/messaging/outbound/session-route.ts +1 -1
  49. package/src/openclaw/reply-runtime.ts +4 -5
  50. package/src/openclaw/routing-runtime.ts +0 -1
  51. package/src/openclaw/runtime-surface.ts +29 -0
  52. package/src/openclaw/sdk-helpers.ts +4 -1
  53. package/src/plugin/gateway-methods.ts +2 -0
  54. package/src/plugin/messaging.ts +2 -9
  55. package/src/plugin/status.ts +15 -5
  56. package/src/runtime/outbound-ack-timeout.ts +73 -0
  57. package/src/runtime/outbound-flags.ts +1 -1
  58. package/src/runtime/outbox-transitions.ts +4 -4
  59. package/src/runtime/register-trace-runtime.ts +102 -0
  60. package/src/runtime/status-snapshots.ts +10 -4
  61. 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: (scope: string | undefined, message: string, options?: { debugOnly?: boolean }) => void;
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
- replyToId: normalizeOutboundReplyToId({ kind: payload?.kind, replyToId: payload?.replyToId }),
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) return '';
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)) attemptedConnIds.push(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((connId) => !attemptedConnIds.includes(connId));
58
- const shouldFastReroute = input.requireAck && input.currentFastReroutePending !== true && hasUntriedAlternative;
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 ? input.nowMs + 1_000 : input.nowMs + deps.backoffMs(nextRetryCount);
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 ? currentRouteAttemptRound : currentRouteAttemptRound + 1;
79
- const fastReroutePending = hasUntriedAlternative ? shouldFastReroute || input.currentFastReroutePending === true : false;
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('OpenClaw channel reply dispatchReplyWithBufferedBlockDispatcher API is unavailable');
98
+ throw new Error(
99
+ 'OpenClaw channel reply dispatchReplyWithBufferedBlockDispatcher API is unavailable',
100
+ );
102
101
  }
103
102
  return reply.dispatchReplyWithBufferedBlockDispatcher(params);
104
103
  }
@@ -45,4 +45,3 @@ export function resolveOpenClawInboundLastRouteSessionKey(params: {
45
45
  }): string {
46
46
  return resolveInboundLastRouteSessionKey(params as any);
47
47
  }
48
-
@@ -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 { readJsonFileWithFallback as sdkReadJsonFileWithFallback, writeJsonFileAtomically as sdkWriteJsonFileAtomically } from 'openclaw/plugin-sdk/json-store';
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';
@@ -4,6 +4,8 @@ export const BNCR_GATEWAY_METHODS = [
4
4
  'bncr.activity',
5
5
  'bncr.ack',
6
6
  'bncr.diagnostics',
7
+ 'bncr.deadLetter.inspect',
8
+ 'bncr.deadLetter.prune',
7
9
  'bncr.file.init',
8
10
  'bncr.file.chunk',
9
11
  'bncr.file.complete',
@@ -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(
@@ -1,4 +1,8 @@
1
- import { BNCR_DEFAULT_ACCOUNT_ID, resolveAccount, resolveDefaultDisplayName } from '../core/accounts.ts';
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 rt = runtime || runtimeBridge.getAccountRuntimeSnapshot(account?.accountId);
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(account?.accountId),
35
+ healthSummary: runtimeBridge.getStatusHeadline(accountId),
26
36
  // default 名不可隐藏时,统一展示稳定默认值
27
- displayName: resolveDefaultDisplayName(account?.name, account?.accountId),
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 { CHANNEL_ID, BNCR_DEFAULT_ACCOUNT_ID, normalizeAccountId } from '../core/accounts.ts';
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 && typeof ackPushLatencyMs === 'number' && ackPushLatencyMs <= defaultAckTimeoutMs,
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
+ }