@xmoxmo/bncr 0.2.6 → 0.2.8

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 (45) hide show
  1. package/README.md +7 -1
  2. package/index.ts +30 -15
  3. package/package.json +4 -3
  4. package/scripts/check-pack.mjs +77 -0
  5. package/scripts/selfcheck.mjs +10 -0
  6. package/src/channel.ts +398 -642
  7. package/src/core/extended-diagnostics.ts +10 -0
  8. package/src/core/file-ack.ts +9 -0
  9. package/src/core/file-transfer-payloads.ts +72 -0
  10. package/src/core/register-trace.ts +79 -0
  11. package/src/core/targets.ts +10 -1
  12. package/src/messaging/inbound/commands.ts +20 -10
  13. package/src/messaging/inbound/context-facts.ts +200 -0
  14. package/src/messaging/inbound/dispatch.ts +66 -14
  15. package/src/messaging/inbound/gate.ts +66 -26
  16. package/src/messaging/inbound/runtime-compat.ts +41 -0
  17. package/src/messaging/inbound/session-label.ts +7 -7
  18. package/src/messaging/outbound/durable-message-adapter.ts +107 -0
  19. package/src/messaging/outbound/durable-queue-adapter.ts +157 -0
  20. package/src/messaging/outbound/session-route.ts +2 -2
  21. package/src/openclaw/config-runtime.ts +52 -0
  22. package/src/openclaw/inbound-session-runtime.ts +94 -0
  23. package/src/openclaw/ingress-runtime.ts +35 -0
  24. package/src/openclaw/media-runtime.ts +73 -0
  25. package/src/openclaw/reply-runtime.ts +104 -0
  26. package/src/openclaw/routing-runtime.ts +48 -0
  27. package/src/openclaw/sdk-helpers.ts +20 -0
  28. package/src/openclaw/session-route-runtime.ts +15 -0
  29. package/src/plugin/capabilities.ts +8 -0
  30. package/src/plugin/config.ts +35 -0
  31. package/src/plugin/gateway-methods.ts +12 -0
  32. package/src/plugin/gateway-runtime.ts +11 -0
  33. package/src/plugin/message-policy.ts +4 -0
  34. package/src/plugin/message-send.ts +13 -0
  35. package/src/plugin/messaging.ts +142 -0
  36. package/src/plugin/meta.ts +10 -0
  37. package/src/plugin/outbound.ts +51 -0
  38. package/src/plugin/setup.ts +24 -0
  39. package/src/plugin/status.ts +38 -0
  40. package/src/runtime/log-dedupe.ts +56 -0
  41. package/src/runtime/outbound-ack-timeout.ts +96 -0
  42. package/src/runtime/outbound-flags.ts +81 -0
  43. package/src/runtime/outbox-transitions.ts +119 -0
  44. package/src/runtime/status-snapshots.ts +108 -0
  45. package/src/runtime/status-worker.ts +172 -0
@@ -2,6 +2,10 @@ import type { RegisterTraceEntry } from './register-trace.ts';
2
2
 
3
3
  type ExtendedDiagnosticsInput = {
4
4
  diagnostics: Record<string, any>;
5
+ runtimeSurface?: {
6
+ channel: Record<string, boolean>;
7
+ missing: string[];
8
+ };
5
9
  register: {
6
10
  bridgeId: string;
7
11
  gatewayPid: number;
@@ -48,6 +52,12 @@ type ExtendedDiagnosticsInput = {
48
52
  export function buildExtendedDiagnostics(input: ExtendedDiagnosticsInput) {
49
53
  return {
50
54
  ...input.diagnostics,
55
+ runtimeSurface: input.runtimeSurface
56
+ ? {
57
+ channel: { ...input.runtimeSurface.channel },
58
+ missing: input.runtimeSurface.missing.slice(),
59
+ }
60
+ : undefined,
51
61
  register: {
52
62
  ...input.register,
53
63
  traceRecent: input.register.traceRecent.slice(),
@@ -0,0 +1,9 @@
1
+ export function buildFileAckKey(args: {
2
+ transferId: string;
3
+ stage: string;
4
+ chunkIndex?: number;
5
+ }): string {
6
+ const n = Number(args.chunkIndex);
7
+ const idx = Number.isInteger(n) && n >= 0 ? String(n) : '-';
8
+ return `${args.transferId}|${args.stage}|${idx}`;
9
+ }
@@ -0,0 +1,72 @@
1
+ import type { BncrRoute } from './accounts.ts';
2
+
3
+ export function buildFileTransferInitPayload(args: {
4
+ transferId: string;
5
+ sessionKey: string;
6
+ route: BncrRoute;
7
+ fileName: string;
8
+ mimeType?: string;
9
+ fileSize: number;
10
+ chunkSize: number;
11
+ totalChunks: number;
12
+ fileSha256: string;
13
+ ts: number;
14
+ }) {
15
+ return {
16
+ transferId: args.transferId,
17
+ direction: 'oc2bncr' as const,
18
+ sessionKey: args.sessionKey,
19
+ platform: args.route.platform,
20
+ groupId: args.route.groupId,
21
+ userId: args.route.userId,
22
+ fileName: args.fileName,
23
+ mimeType: args.mimeType,
24
+ fileSize: args.fileSize,
25
+ chunkSize: args.chunkSize,
26
+ totalChunks: args.totalChunks,
27
+ fileSha256: args.fileSha256,
28
+ ts: args.ts,
29
+ };
30
+ }
31
+
32
+ export function buildFileTransferChunkPayload(args: {
33
+ transferId: string;
34
+ chunkIndex: number;
35
+ offset: number;
36
+ size: number;
37
+ chunkSha256: string;
38
+ base64: string;
39
+ ts: number;
40
+ }) {
41
+ return {
42
+ transferId: args.transferId,
43
+ chunkIndex: args.chunkIndex,
44
+ offset: args.offset,
45
+ size: args.size,
46
+ chunkSha256: args.chunkSha256,
47
+ base64: args.base64,
48
+ ts: args.ts,
49
+ };
50
+ }
51
+
52
+ export function buildFileTransferAbortPayload(args: {
53
+ transferId: string;
54
+ reason: string;
55
+ ts: number;
56
+ }) {
57
+ return {
58
+ transferId: args.transferId,
59
+ reason: args.reason,
60
+ ts: args.ts,
61
+ };
62
+ }
63
+
64
+ export function buildFileTransferCompletePayload(args: {
65
+ transferId: string;
66
+ ts: number;
67
+ }) {
68
+ return {
69
+ transferId: args.transferId,
70
+ ts: args.ts,
71
+ };
72
+ }
@@ -33,6 +33,19 @@ export type RegisterTraceSummary = {
33
33
  likelyStartupFanoutOnly: boolean;
34
34
  };
35
35
 
36
+ export type RegisterDriftSnapshot = {
37
+ capturedAt: number;
38
+ registerCount: number;
39
+ apiGeneration: number;
40
+ postWarmupRegisterCount: number;
41
+ apiInstanceId: string | null;
42
+ registryFingerprint: string | null;
43
+ dominantBucket: string | null;
44
+ sourceBuckets: Record<string, number>;
45
+ traceWindowSize: number;
46
+ traceRecent: Array<Record<string, unknown>>;
47
+ };
48
+
36
49
  export function classifyRegisterTrace(stack: string) {
37
50
  if (
38
51
  stack.includes('prepareSecretsRuntimeSnapshot') ||
@@ -68,6 +81,72 @@ export function dominantRegisterBucket(sourceBuckets: Record<string, number>) {
68
81
  return winner;
69
82
  }
70
83
 
84
+ export function buildRegisterTraceEntry(args: {
85
+ ts: number;
86
+ bridgeId: string;
87
+ gatewayPid: number;
88
+ registerCount: number;
89
+ apiGeneration: number;
90
+ apiRebound: boolean;
91
+ apiInstanceId: string | null;
92
+ registryFingerprint: string | null;
93
+ source: string | null;
94
+ pluginVersion: string | null;
95
+ stack: string;
96
+ }): RegisterTraceEntry {
97
+ return {
98
+ ts: args.ts,
99
+ bridgeId: args.bridgeId,
100
+ gatewayPid: args.gatewayPid,
101
+ registerCount: args.registerCount,
102
+ apiGeneration: args.apiGeneration,
103
+ apiRebound: args.apiRebound,
104
+ apiInstanceId: args.apiInstanceId,
105
+ registryFingerprint: args.registryFingerprint,
106
+ source: args.source,
107
+ pluginVersion: args.pluginVersion,
108
+ stack: args.stack,
109
+ stackBucket: classifyRegisterTrace(args.stack),
110
+ };
111
+ }
112
+
113
+ export function appendBoundedRegisterTrace(
114
+ traceRecent: RegisterTraceEntry[],
115
+ trace: RegisterTraceEntry,
116
+ maxEntries = 12,
117
+ ) {
118
+ traceRecent.push(trace);
119
+ const cap = Math.max(0, Math.floor(finiteNumberOr(maxEntries, 12)));
120
+ if (cap === 0) {
121
+ traceRecent.splice(0, traceRecent.length);
122
+ return;
123
+ }
124
+ if (traceRecent.length > cap) traceRecent.splice(0, traceRecent.length - cap);
125
+ }
126
+
127
+ export function buildRegisterDriftSnapshot(args: {
128
+ capturedAt: number;
129
+ registerCount: number;
130
+ apiGeneration: number;
131
+ summary: RegisterTraceSummary;
132
+ apiInstanceId: string | null;
133
+ registryFingerprint: string | null;
134
+ traceRecent: RegisterTraceEntry[];
135
+ }): RegisterDriftSnapshot {
136
+ return {
137
+ capturedAt: args.capturedAt,
138
+ registerCount: args.registerCount,
139
+ apiGeneration: args.apiGeneration,
140
+ postWarmupRegisterCount: args.summary.postWarmupRegisterCount,
141
+ apiInstanceId: args.apiInstanceId,
142
+ registryFingerprint: args.registryFingerprint,
143
+ dominantBucket: args.summary.dominantBucket,
144
+ sourceBuckets: { ...args.summary.sourceBuckets },
145
+ traceWindowSize: args.traceRecent.length,
146
+ traceRecent: args.traceRecent.map((trace) => ({ ...trace })),
147
+ };
148
+ }
149
+
71
150
  export function buildRegisterTraceSummary(args: {
72
151
  traceRecent: RegisterTraceEntry[];
73
152
  firstRegisterAt: number | null;
@@ -51,8 +51,17 @@ function parseRouteFromStandardDisplayScope(scope: string): BncrRoute | null {
51
51
  return null;
52
52
  }
53
53
 
54
- export function parseRouteFromDisplayScope(scope: string): BncrRoute | null {
54
+ function normalizeDisplayScopePrefix(scope: string): string {
55
55
  const raw = asString(scope).trim();
56
+ if (!raw) return '';
57
+ if (raw.startsWith('Bncr:')) return raw;
58
+ if (/^bncr[:-]/i.test(raw)) return raw;
59
+ if (!parseRouteFromStandardDisplayScope(raw)) return raw;
60
+ return `Bncr:${raw}`;
61
+ }
62
+
63
+ export function parseRouteFromDisplayScope(scope: string): BncrRoute | null {
64
+ const raw = normalizeDisplayScopePrefix(scope);
56
65
  if (!raw) return null;
57
66
 
58
67
  const payload = raw.match(/^Bncr:(.+)$/)?.[1];
@@ -1,5 +1,3 @@
1
- import { resolvePinnedMainDmOwnerFromAllowlist } from 'openclaw/plugin-sdk/conversation-runtime';
2
- import { resolveInboundLastRouteSessionKey } from 'openclaw/plugin-sdk/routing';
3
1
  import { emitBncrLogLine } from '../../core/logging.ts';
4
2
  import { resolveBncrChannelPolicy } from '../../core/policy.ts';
5
3
  import {
@@ -8,11 +6,22 @@ import {
8
6
  withTaskSessionKey,
9
7
  } from '../../core/targets.ts';
10
8
  import { buildBncrReplyConfig } from './reply-config.ts';
9
+ import { resolveBncrChannelInboundRuntime } from './runtime-compat.ts';
11
10
  import {
12
11
  buildBncrInboundSessionIdentityPatch,
13
12
  recordAndPatchBncrInboundSessionEntry,
14
13
  wrapBncrInboundRecordSessionLabelCorrection,
15
14
  } from './session-label.ts';
15
+ import { dispatchOpenClawReplyWithBufferedBlockDispatcher } from '../../openclaw/reply-runtime.ts';
16
+ import {
17
+ resolveOpenClawAgentRoute,
18
+ resolveOpenClawInboundLastRouteSessionKey,
19
+ } from '../../openclaw/routing-runtime.ts';
20
+ import {
21
+ recordBncrInboundSession,
22
+ resolveBncrInboundSessionStorePath,
23
+ resolveBncrPinnedMainDmOwnerFromAllowlist,
24
+ } from '../../openclaw/inbound-session-runtime.ts';
16
25
 
17
26
  type ParsedInbound = ReturnType<typeof import('./parse.ts')['parseBncrInboundParams']>;
18
27
 
@@ -110,7 +119,7 @@ export async function handleBncrNativeCommand(params: {
110
119
  { debugOnly: true, debugEnabled: nativeCommandDebugEnabled },
111
120
  );
112
121
 
113
- const resolvedRoute = api.runtime.channel.routing.resolveAgentRoute({
122
+ const resolvedRoute = resolveOpenClawAgentRoute(api, {
114
123
  cfg,
115
124
  channel: channelId,
116
125
  accountId,
@@ -137,11 +146,12 @@ export async function handleBncrNativeCommand(params: {
137
146
  }
138
147
  const senderIdForContext = clientId || displayTo;
139
148
  const senderDisplayName = clientId ? 'bncr-client' : displayTo;
140
- const storePath = api.runtime.channel.session.resolveStorePath(cfg?.session?.store, {
149
+ const storePath = resolveBncrInboundSessionStorePath({
150
+ storeConfig: cfg?.session?.store,
141
151
  agentId: resolvedRoute.agentId,
142
152
  });
143
153
 
144
- const ctxPayload = api.runtime.channel.turn.buildContext({
154
+ const ctxPayload = resolveBncrChannelInboundRuntime(api).buildContext({
145
155
  channel: channelId,
146
156
  provider: channelId,
147
157
  surface: channelId,
@@ -259,13 +269,13 @@ export async function handleBncrNativeCommand(params: {
259
269
  const channelPolicy = resolveBncrChannelPolicy(cfg?.channels?.bncr || {});
260
270
  const pinnedMainDmOwner =
261
271
  peer.kind === 'direct'
262
- ? resolvePinnedMainDmOwnerFromAllowlist({
272
+ ? resolveBncrPinnedMainDmOwnerFromAllowlist({
263
273
  dmScope: cfg?.session?.dmScope,
264
274
  allowFrom: channelPolicy.allowFrom,
265
275
  normalizeEntry: (entry: string) => String(entry || '').trim(),
266
276
  })
267
277
  : null;
268
- const inboundLastRouteSessionKey = resolveInboundLastRouteSessionKey({
278
+ const inboundLastRouteSessionKey = resolveOpenClawInboundLastRouteSessionKey({
269
279
  route: resolvedRoute,
270
280
  sessionKey,
271
281
  });
@@ -282,7 +292,7 @@ export async function handleBncrNativeCommand(params: {
282
292
  },
283
293
  { debugOnly: true, debugEnabled: nativeCommandDebugEnabled },
284
294
  );
285
- await api.runtime.channel.turn.run({
295
+ await resolveBncrChannelInboundRuntime(api).run({
286
296
  channel: channelId,
287
297
  accountId,
288
298
  raw: parsed,
@@ -302,7 +312,7 @@ export async function handleBncrNativeCommand(params: {
302
312
  storePath,
303
313
  ctxPayload,
304
314
  recordInboundSession: wrapBncrInboundRecordSessionLabelCorrection({
305
- recordInboundSession: api.runtime.channel.session.recordInboundSession,
315
+ recordInboundSession: recordBncrInboundSession,
306
316
  expectedLabel: displayTo,
307
317
  }),
308
318
  record: {
@@ -330,7 +340,7 @@ export async function handleBncrNativeCommand(params: {
330
340
  },
331
341
  },
332
342
  runDispatch: () =>
333
- api.runtime.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
343
+ dispatchOpenClawReplyWithBufferedBlockDispatcher(api, {
334
344
  ctx: ctxPayload,
335
345
  cfg: effectiveReply.replyCfg,
336
346
  dispatcherOptions: {
@@ -0,0 +1,200 @@
1
+ export type BncrStructuredContextFactsInput = {
2
+ channelId: string;
3
+ accountId: string;
4
+ route: {
5
+ agentId?: string;
6
+ routeSessionKey?: string;
7
+ dispatchSessionKey?: string;
8
+ mainSessionKey?: string;
9
+ };
10
+ conversation: {
11
+ kind: string;
12
+ id: string;
13
+ label: string;
14
+ };
15
+ reply: {
16
+ to: string;
17
+ originatingTo: string;
18
+ };
19
+ sender: {
20
+ id: string;
21
+ displayName?: string;
22
+ };
23
+ message: {
24
+ id?: string | null;
25
+ rawBody: string;
26
+ bodyForAgent?: string;
27
+ commandBody?: string;
28
+ envelopeBody?: string;
29
+ };
30
+ media?: Array<{
31
+ path: string;
32
+ contentType?: string;
33
+ kind?: string;
34
+ messageId?: string;
35
+ }>;
36
+ };
37
+
38
+ export function buildBncrStructuredContextFacts(input: BncrStructuredContextFactsInput) {
39
+ const rawBody = input.message.rawBody;
40
+ return {
41
+ channel: {
42
+ id: input.channelId,
43
+ accountId: input.accountId,
44
+ },
45
+ route: {
46
+ agentId: input.route.agentId,
47
+ routeSessionKey: input.route.routeSessionKey,
48
+ dispatchSessionKey: input.route.dispatchSessionKey,
49
+ mainSessionKey: input.route.mainSessionKey,
50
+ },
51
+ conversation: {
52
+ kind: input.conversation.kind,
53
+ id: input.conversation.id,
54
+ label: input.conversation.label,
55
+ },
56
+ reply: {
57
+ to: input.reply.to,
58
+ originatingTo: input.reply.originatingTo,
59
+ },
60
+ sender: {
61
+ id: input.sender.id,
62
+ displayName: input.sender.displayName || input.sender.id,
63
+ },
64
+ message: {
65
+ id: input.message.id || undefined,
66
+ rawBody,
67
+ bodyForAgent: input.message.bodyForAgent ?? rawBody,
68
+ commandBody: input.message.commandBody ?? rawBody,
69
+ envelopeBody: input.message.envelopeBody,
70
+ },
71
+ media: (input.media || []).map((item) => ({
72
+ path: item.path,
73
+ contentType: item.contentType,
74
+ kind: item.kind,
75
+ messageId: item.messageId,
76
+ })),
77
+ };
78
+ }
79
+
80
+ // Keep this payload intentionally small: OpenClaw already renders standard
81
+ // conversation/sender/message metadata as untrusted context. Only include
82
+ // bncr-specific facts that are not otherwise visible to the model, so normal
83
+ // text turns do not get a duplicate "Bncr inbound context" JSON block.
84
+ export function buildBncrPromptVisibleContextFacts(
85
+ facts: ReturnType<typeof buildBncrStructuredContextFacts>,
86
+ ) {
87
+ const result: {
88
+ reply?: {
89
+ to: string;
90
+ originatingTo: string;
91
+ };
92
+ media?: Array<{
93
+ contentType?: string;
94
+ kind?: string;
95
+ messageId?: string;
96
+ }>;
97
+ } = {};
98
+
99
+ if (facts.reply.originatingTo !== facts.reply.to) {
100
+ result.reply = {
101
+ to: facts.reply.to,
102
+ originatingTo: facts.reply.originatingTo,
103
+ };
104
+ }
105
+
106
+ if (facts.media.length > 0) {
107
+ result.media = facts.media.map((item) => ({
108
+ contentType: item.contentType,
109
+ kind: item.kind,
110
+ messageId: item.messageId,
111
+ }));
112
+ }
113
+
114
+ return result;
115
+ }
116
+
117
+ function inferBncrStructuredMediaKind(contentType: string | undefined) {
118
+ if (contentType?.startsWith('image/')) return 'image';
119
+ if (contentType?.startsWith('video/')) return 'video';
120
+ if (contentType?.startsWith('audio/')) return 'audio';
121
+ return 'document';
122
+ }
123
+
124
+ export type BncrStructuredContextFactsFromInboundPartsInput = {
125
+ channelId: string;
126
+ parsed: {
127
+ accountId: string;
128
+ peer: {
129
+ kind: string;
130
+ id: string;
131
+ };
132
+ clientId?: string;
133
+ msgId?: string;
134
+ mimeType?: string;
135
+ };
136
+ resolution: {
137
+ chatType: string;
138
+ canonicalTo: string;
139
+ originatingTo: string;
140
+ resolvedRoute: {
141
+ agentId?: string;
142
+ sessionKey?: string;
143
+ mainSessionKey?: string;
144
+ };
145
+ dispatchSessionKey?: string;
146
+ };
147
+ prepared: {
148
+ rawBody: string;
149
+ body?: string;
150
+ mediaPath?: string | null;
151
+ };
152
+ senderIdForContext: string;
153
+ senderDisplayName?: string;
154
+ };
155
+
156
+ export function buildBncrStructuredContextFactsFromInboundParts(
157
+ input: BncrStructuredContextFactsFromInboundPartsInput,
158
+ ) {
159
+ const mediaPath = input.prepared.mediaPath || undefined;
160
+ return buildBncrStructuredContextFacts({
161
+ channelId: input.channelId,
162
+ accountId: input.parsed.accountId,
163
+ route: {
164
+ agentId: input.resolution.resolvedRoute.agentId,
165
+ routeSessionKey: input.resolution.resolvedRoute.sessionKey,
166
+ dispatchSessionKey: input.resolution.dispatchSessionKey,
167
+ mainSessionKey: input.resolution.resolvedRoute.mainSessionKey,
168
+ },
169
+ conversation: {
170
+ kind: input.resolution.chatType,
171
+ id: input.parsed.peer.id,
172
+ label: input.resolution.canonicalTo,
173
+ },
174
+ reply: {
175
+ to: input.resolution.canonicalTo,
176
+ originatingTo: input.resolution.originatingTo,
177
+ },
178
+ sender: {
179
+ id: input.senderIdForContext,
180
+ displayName: input.senderDisplayName,
181
+ },
182
+ message: {
183
+ id: input.parsed.msgId,
184
+ rawBody: input.prepared.rawBody,
185
+ bodyForAgent: input.prepared.rawBody,
186
+ commandBody: input.prepared.rawBody,
187
+ envelopeBody: input.prepared.body,
188
+ },
189
+ media: mediaPath
190
+ ? [
191
+ {
192
+ path: mediaPath,
193
+ contentType: input.parsed.mimeType,
194
+ kind: inferBncrStructuredMediaKind(input.parsed.mimeType),
195
+ messageId: input.parsed.msgId,
196
+ },
197
+ ]
198
+ : [],
199
+ });
200
+ }