@xmoxmo/bncr 0.5.9 → 0.6.1

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.
@@ -1,7 +1,13 @@
1
1
  import type { ChannelMessageSendResult } from 'openclaw/plugin-sdk/channel-message';
2
- import type { BncrRoute } from '../core/types.ts';
2
+ import type { GatewayRequestHandlerOptions } from 'openclaw/plugin-sdk/core';
3
+ import type { BncrRecentOutboundEntry, BncrRoute } from '../core/types.ts';
3
4
  import type { ReplyPayloadInput } from '../messaging/outbound/reply-enqueue.ts';
4
5
  import type { OutboundReplyTargetPolicy } from '../messaging/outbound/reply-target-policy.ts';
6
+ import {
7
+ type BncrBridgeCallDiagnosticsBridge,
8
+ createBncrBridgeCallBridge,
9
+ invokeBncrBridgeGatewayHandler,
10
+ } from './bridge-call.ts';
5
11
  import type {
6
12
  BncrChannelConfigRoot,
7
13
  BncrChannelSendContext,
@@ -34,6 +40,8 @@ export type BncrChannelPluginBridge = {
34
40
  getStatusHeadline: (accountId: string) => string;
35
41
  resolveVerifiedTarget: (to: string, accountId: string) => BncrVerifiedTarget;
36
42
  rememberSessionRoute: (sessionKey: string, accountId: string, route: BncrRoute) => void;
43
+ listRecentOutbound: (sessionKey: string) => BncrRecentOutboundEntry[];
44
+ listRecentOutboundByAccount: (accountId: string) => BncrRecentOutboundEntry[];
37
45
  enqueueFromReply: (args: {
38
46
  accountId: string;
39
47
  sessionKey: string;
@@ -44,6 +52,14 @@ export type BncrChannelPluginBridge = {
44
52
  }) => Promise<void>;
45
53
  channelStartAccount: (ctx: unknown) => void | Promise<void>;
46
54
  channelStopAccount: (ctx: unknown) => void | Promise<void>;
55
+ handleDiagnostics: (ctx: GatewayRequestHandlerOptions) => Promise<void>;
56
+ handleDeadLetterInspect: (ctx: GatewayRequestHandlerOptions) => Promise<void>;
57
+ handleDeadLetterPrune: (ctx: GatewayRequestHandlerOptions) => Promise<void>;
58
+ callClientRpc: (
59
+ method: string,
60
+ args: Record<string, unknown>,
61
+ accountId: string,
62
+ ) => Promise<Record<string, unknown>>;
47
63
  };
48
64
 
49
65
  export function createBncrChannelPluginBridgeGroup(runtime: {
@@ -118,6 +134,31 @@ export function createBncrChannelPluginBridgeGroup(runtime: {
118
134
  };
119
135
  };
120
136
 
137
+ const getDiagnosticsBridge = (): BncrBridgeCallDiagnosticsBridge => {
138
+ const bridge = runtime.getBridge();
139
+ return {
140
+ diagnostics: async (args) => invokeBncrBridgeGatewayHandler(bridge.handleDiagnostics, args),
141
+ deadLetterInspect: async (args) =>
142
+ invokeBncrBridgeGatewayHandler(bridge.handleDeadLetterInspect, args),
143
+ deadLetterPrune: async (args) =>
144
+ invokeBncrBridgeGatewayHandler(bridge.handleDeadLetterPrune, args),
145
+ };
146
+ };
147
+
148
+ const getBridgeCallBridge = () => {
149
+ const bridge = runtime.getBridge();
150
+ return createBncrBridgeCallBridge({
151
+ defaultAccountId: runtime.defaultAccountId,
152
+ getStatusBridge,
153
+ getDiagnosticsBridge,
154
+ getRecentOutboundBridge: () => ({
155
+ listRecentOutbound: (sessionKey) => bridge.listRecentOutbound(sessionKey),
156
+ listRecentOutboundByAccount: (accountId) => bridge.listRecentOutboundByAccount(accountId),
157
+ }),
158
+ callClientRpc: (method, args, accountId) => bridge.callClientRpc(method, args, accountId),
159
+ });
160
+ };
161
+
121
162
  return {
122
163
  getMessageSendBridge,
123
164
  getOutboundBridge,
@@ -125,5 +166,6 @@ export function createBncrChannelPluginBridgeGroup(runtime: {
125
166
  getStatusBridge,
126
167
  getToolActionBridge,
127
168
  getGatewayBridge,
169
+ getBridgeCallBridge,
128
170
  };
129
171
  }
@@ -1,4 +1,5 @@
1
1
  import type { ChannelMessageSendResult } from 'openclaw/plugin-sdk/channel-message';
2
+ import { Type } from 'typebox';
2
3
  import { listAccountIds, normalizeAccountId, resolveAccount } from '../core/accounts.ts';
3
4
  import { BncrConfigSchema } from '../core/config-schema.ts';
4
5
  import { resolveBncrChannelPolicy } from '../core/policy.ts';
@@ -6,9 +7,11 @@ import type { ReplyPayloadInput } from '../messaging/outbound/reply-enqueue.ts';
6
7
  import type { OutboundReplyTargetPolicy } from '../messaging/outbound/reply-target-policy.ts';
7
8
  import type { OpenClawChannelToolSend, openClawJsonResult } from '../openclaw/sdk-helpers.ts';
8
9
  import { readOpenClawStringParam } from '../openclaw/sdk-helpers.ts';
10
+ import type { BncrBridgeCallBridge } from './bridge-call.ts';
9
11
  import type {
10
12
  BncrChannelConfigRoot,
11
13
  BncrChannelSendContext,
14
+ BncrMessageToolSchemaContribution,
12
15
  BncrSceneRecord,
13
16
  BncrStatusRuntimeSnapshot,
14
17
  BncrVerifiedTarget,
@@ -35,6 +38,33 @@ type BncrChannelMeta = {
35
38
 
36
39
  type PluginSurfaceResult = ReturnType<typeof openClawJsonResult>;
37
40
 
41
+ const BNCR_MESSAGE_TOOL_SCHEMA: BncrMessageToolSchemaContribution = {
42
+ visibility: 'current-channel',
43
+ properties: {
44
+ bridgeMethod: Type.Optional(
45
+ Type.String({
46
+ description:
47
+ 'Bncr-only bridge method forwarded to the OpenClawClient RPC bridge; use bncr.methods to list calls, or bncr.client.adapters.call to invoke a Bncr adapter method generically.',
48
+ }),
49
+ ),
50
+ extra: Type.Optional(
51
+ Type.Record(Type.String(), Type.Unknown(), {
52
+ description: 'Bncr-specific structured params merged into the unified outbound normalizer.',
53
+ }),
54
+ ),
55
+ type: Type.Optional(
56
+ Type.String({
57
+ description: 'Explicit outbound type such as text, audio, video, voice, file, or appmsg.',
58
+ }),
59
+ ),
60
+ downloadMedia: Type.Optional(
61
+ Type.Boolean({
62
+ description: 'Force URL media to be downloaded before sending.',
63
+ }),
64
+ ),
65
+ },
66
+ };
67
+
38
68
  export function createBncrChannelPluginSurfaceGroup(runtime: {
39
69
  channelId: string;
40
70
  sceneRegistry: Map<string, BncrSceneRecord>;
@@ -80,6 +110,7 @@ export function createBncrChannelPluginSurfaceGroup(runtime: {
80
110
  channelStartAccount: BncrGatewayAccountBridge['channelStartAccount'];
81
111
  channelStopAccount: BncrGatewayAccountBridge['channelStopAccount'];
82
112
  };
113
+ getBridgeCallBridge: () => BncrBridgeCallBridge;
83
114
  channelMeta: BncrChannelMeta;
84
115
  channelCapabilities: {
85
116
  chatTypes: Array<'direct' | 'group' | 'thread'>;
@@ -121,11 +152,12 @@ export function createBncrChannelPluginSurfaceGroup(runtime: {
121
152
  }
122
153
 
123
154
  return {
124
- actions: ['send'] as const,
155
+ actions: ['send', 'delete', 'unsend'] as const,
125
156
  capabilities: [] as const,
157
+ schema: BNCR_MESSAGE_TOOL_SCHEMA,
126
158
  };
127
159
  },
128
- supportsAction: ({ action }) => action === 'send',
160
+ supportsAction: ({ action }) => action === 'send' || action === 'delete' || action === 'unsend',
129
161
  extractToolSend: ({ args }) =>
130
162
  runtime.extractToolSend(
131
163
  (args && typeof args === 'object' ? (args as Record<string, unknown>) : {}) as Record<
@@ -135,19 +167,62 @@ export function createBncrChannelPluginSurfaceGroup(runtime: {
135
167
  'sendMessage',
136
168
  ) || null,
137
169
  handleAction: async ({ action, params, accountId, mediaLocalRoots }) => {
138
- if (action !== 'send') {
139
- throw new Error(`Action ${action} is not supported for provider ${runtime.channelId}.`);
140
- }
141
-
142
170
  /* -- extract raw params (no normalization) ----------------------- */
143
171
  const paramsObj = (params && typeof params === 'object' ? params : {}) as Record<
144
172
  string,
145
173
  unknown
146
174
  >;
147
- const to = readOpenClawStringParam(paramsObj, 'to', { required: true });
148
175
  const resolvedAccountId = normalizeAccountId(
149
176
  (readOpenClawStringParam(paramsObj, 'accountId') ?? accountId) || '',
150
177
  );
178
+ if (action === 'delete' || action === 'unsend') {
179
+ const to = readOpenClawStringParam(paramsObj, 'to', { required: true });
180
+ const messageId =
181
+ (typeof paramsObj.messageId === 'number'
182
+ ? String(paramsObj.messageId)
183
+ : readOpenClawStringParam(paramsObj, 'messageId')) ||
184
+ readOpenClawStringParam(paramsObj, 'message_id');
185
+ if (!messageId) {
186
+ throw new Error(`${action} requires messageId`);
187
+ }
188
+
189
+ const verified = runtime.getToolActionBridge().resolveVerifiedTarget(to, resolvedAccountId);
190
+ const bridgeResult = await runtime.getBridgeCallBridge().call(
191
+ 'bncr.client.adapters.call',
192
+ {
193
+ msgInfo: {
194
+ groupId: verified.route.groupId || '0',
195
+ userId: verified.route.userId || '0',
196
+ },
197
+ from: verified.route.platform,
198
+ method: 'delMsg',
199
+ messageId,
200
+ },
201
+ resolvedAccountId,
202
+ );
203
+ return runtime.openClawJsonResult({ ok: true, ...bridgeResult });
204
+ }
205
+
206
+ if (action !== 'send') {
207
+ throw new Error(`Action ${action} is not supported for provider ${runtime.channelId}.`);
208
+ }
209
+
210
+ const bridgeMethod =
211
+ typeof paramsObj.bridgeMethod === 'string' ? paramsObj.bridgeMethod.trim() : '';
212
+ if (bridgeMethod) {
213
+ const rawExtra = paramsObj.extra;
214
+ const bridgeArgs =
215
+ rawExtra && typeof rawExtra === 'object' && !Array.isArray(rawExtra)
216
+ ? { ...(rawExtra as Record<string, unknown>) }
217
+ : {};
218
+ if (!('accountId' in bridgeArgs)) {
219
+ bridgeArgs.accountId = resolvedAccountId || undefined;
220
+ }
221
+ const bridgeResult = await runtime.getBridgeCallBridge().call(bridgeMethod, bridgeArgs);
222
+ return runtime.openClawJsonResult(bridgeResult);
223
+ }
224
+
225
+ const to = readOpenClawStringParam(paramsObj, 'to', { required: true });
151
226
  const toolMediaUrl =
152
227
  (typeof paramsObj.mediaUrl === 'string' ? paramsObj.mediaUrl : '') ||
153
228
  (typeof paramsObj.media === 'string' ? paramsObj.media : '') ||
@@ -1,6 +1,7 @@
1
1
  import type { GatewayRequestHandlerOptions } from 'openclaw/plugin-sdk/core';
2
2
  import type { BncrRoute, OutboxEntry } from '../core/types.ts';
3
3
  import type { BncrGroupHistoryMap } from '../messaging/inbound/group-history.ts';
4
+ import type { BncrOutboundReplayCache } from '../messaging/inbound/outbound-replay-cache.ts';
4
5
  import type { parseBncrInboundParams } from '../messaging/inbound/parse.ts';
5
6
  import type { ReplyPayloadInput } from '../messaging/outbound/reply-enqueue.ts';
6
7
  import type { OutboundReplyTargetPolicy } from '../messaging/outbound/reply-target-policy.ts';
@@ -107,6 +108,7 @@ export function buildBncrInboundSurfaceRuntime(deps: {
107
108
  defaultPublicAgentId: () => string;
108
109
  sceneRegistry: Map<string, BncrSceneRecord>;
109
110
  groupHistories: BncrGroupHistoryMap;
111
+ outboundReplayCache: BncrOutboundReplayCache;
110
112
  prepareInboundAcceptance: (args: {
111
113
  parsed: ReturnType<typeof parseBncrInboundParams>;
112
114
  canonicalAgentId: string;
@@ -1,5 +1,11 @@
1
+ import type { TSchema } from 'typebox';
1
2
  import type { RegisterDriftSnapshot } from '../core/register-trace.ts';
2
- import type { BncrDiagnosticsSummary, BncrRoute, OutboxEntry } from '../core/types.ts';
3
+ import type {
4
+ BncrDiagnosticsSummary,
5
+ BncrOutboundReplayEntry,
6
+ BncrRoute,
7
+ OutboxEntry,
8
+ } from '../core/types.ts';
3
9
  import type { OpenClawChannelToolSend, openClawJsonResult } from '../openclaw/sdk-helpers.ts';
4
10
 
5
11
  type OpenClawJsonResultPayload = ReturnType<typeof openClawJsonResult>;
@@ -88,6 +94,13 @@ export type BncrPersistedGroupHistoryBucket = {
88
94
  entries: BncrPersistedGroupHistoryEntry[];
89
95
  };
90
96
 
97
+ export type BncrPersistedOutboundReplayEntry = BncrOutboundReplayEntry;
98
+ export type BncrPersistedOutboundReplayMediaEntry = BncrPersistedGroupHistoryMediaEntry;
99
+ export type BncrPersistedOutboundReplayBucket = {
100
+ key: string;
101
+ entries: BncrPersistedOutboundReplayEntry[];
102
+ };
103
+
91
104
  export type BncrStatusRuntimeSnapshot = {
92
105
  connected?: boolean;
93
106
  running?: boolean;
@@ -143,10 +156,20 @@ export type BncrChannelSendContext = {
143
156
  extra?: Record<string, unknown>;
144
157
  };
145
158
 
159
+ /**
160
+ * TypeBox schema contribution for the shared `message` tool.
161
+ * The host wraps each property with Type.Optional before validation.
162
+ */
163
+ export type BncrMessageToolSchemaContribution = {
164
+ visibility?: 'current-channel' | 'all-configured';
165
+ properties: Record<string, TSchema>;
166
+ };
167
+
146
168
  export type ChannelMessageActionAdapter = {
147
169
  describeMessageTool: (ctx: { cfg: BncrChannelConfigRoot }) => {
148
- actions: readonly ['send'];
170
+ actions: readonly ('send' | 'delete' | 'unsend')[];
149
171
  capabilities: readonly [];
172
+ schema?: BncrMessageToolSchemaContribution;
150
173
  } | null;
151
174
  supportsAction: (ctx: { action: string }) => boolean;
152
175
  extractToolSend: (ctx: { args: unknown }) => OpenClawChannelToolSend | null;
@@ -164,6 +187,7 @@ export type PersistedState = {
164
187
  sessionRoutes: BncrPersistedSessionRoute[];
165
188
  sceneRegistry?: BncrSceneRecord[];
166
189
  groupHistories?: BncrPersistedGroupHistoryBucket[];
190
+ outboundReplayCache?: BncrPersistedOutboundReplayBucket[];
167
191
  lastSessionByAccount?: BncrPersistedLastSession[];
168
192
  lastActivityByAccount?: BncrPersistedAccountTimestamp[];
169
193
  lastInboundByAccount?: BncrPersistedAccountTimestamp[];
@@ -1,4 +1,3 @@
1
- import { randomUUID } from 'node:crypto';
2
1
  import { normalizeAccountId } from '../core/accounts.ts';
3
2
  import { buildBncrDebugJsonMessage } from '../core/logging.ts';
4
3
  import type { BncrRoute, OutboxEntry } from '../core/types.ts';
@@ -211,28 +210,22 @@ export function createBncrChannelSendRuntime(runtime: BncrChannelSendRuntime) {
211
210
  const verified = runtime.resolveVerifiedTarget(to, accountId);
212
211
  runtime.rememberSessionRoute(verified.sessionKey, accountId, verified.route);
213
212
 
214
- await runtime.enqueueFromReply({
215
- accountId,
216
- sessionKey: verified.sessionKey,
217
- route: verified.route,
218
- payload: {
219
- text: text,
220
- mediaUrl: mediaUrl || undefined,
221
- mediaUrls: Array.isArray(mediaUrls) && mediaUrls.length ? mediaUrls : undefined,
222
- asVoice: asVoice || undefined,
223
- audioAsVoice: audioAsVoice || undefined,
224
- downloadMedia,
225
- type,
226
- extra,
227
- kind: kind as 'tool' | 'block' | 'final' | undefined,
228
- replyToId,
229
- },
230
- mediaLocalRoots: ctx.mediaLocalRoots,
213
+ const entry = await enqueueChannelMessageHandoff(runtime, ctx, {
214
+ text: text,
215
+ mediaUrl: mediaUrl || undefined,
216
+ mediaUrls: Array.isArray(mediaUrls) && mediaUrls.length ? mediaUrls : undefined,
217
+ asVoice: asVoice || undefined,
218
+ audioAsVoice: audioAsVoice || undefined,
219
+ downloadMedia,
220
+ type,
221
+ extra,
222
+ kind: kind as 'tool' | 'block' | 'final' | undefined,
223
+ replyToId,
231
224
  });
232
225
 
233
226
  return {
234
227
  channel: runtime.channelId,
235
- messageId: randomUUID(),
228
+ messageId: entry.messageId,
236
229
  chatId: verified.sessionKey,
237
230
  };
238
231
  }
@@ -0,0 +1,129 @@
1
+ import type { GatewayRequestHandlerOptions } from 'openclaw/plugin-sdk/core';
2
+
3
+ export const BNCR_CLIENT_RPC_REQUEST_EVENT = 'plugin.bncr.rpc.request';
4
+ export const BNCR_CLIENT_RPC_RESPONSE_METHOD = 'bncr.rpc.response';
5
+ export const BNCR_CLIENT_RPC_TIMEOUT_MS = 15000;
6
+
7
+ type BncrClientRpcPending = {
8
+ method: string;
9
+ resolve: (result: Record<string, unknown>) => void;
10
+ reject: (error: Error) => void;
11
+ timer: NodeJS.Timeout;
12
+ };
13
+
14
+ export type BncrClientRpcRuntime = {
15
+ call: (
16
+ method: string,
17
+ args: Record<string, unknown>,
18
+ accountId: string,
19
+ ) => Promise<Record<string, unknown>>;
20
+ handleResponse: (ctx: GatewayRequestHandlerOptions) => void;
21
+ pendingCount: () => number;
22
+ shutdown: () => void;
23
+ };
24
+
25
+ export function createBncrClientRpcRuntime(runtime: {
26
+ resolvePushConnIds: (accountId: string) => ReadonlySet<string>;
27
+ broadcastToConnIds: (event: string, payload: unknown, connIds: ReadonlySet<string>) => void;
28
+ now: () => number;
29
+ logInfo: (scope: string, message: string, options?: { debugOnly?: boolean }) => void;
30
+ timeoutMs?: number;
31
+ }): BncrClientRpcRuntime {
32
+ const pending = new Map<string, BncrClientRpcPending>();
33
+ let requestSeq = 0;
34
+ const timeoutMs = Math.max(Number(runtime.timeoutMs ?? BNCR_CLIENT_RPC_TIMEOUT_MS), 1);
35
+
36
+ const cleanup = (requestId: string) => {
37
+ const entry = pending.get(requestId);
38
+ if (!entry) return;
39
+ clearTimeout(entry.timer);
40
+ pending.delete(requestId);
41
+ };
42
+
43
+ const call = (method: string, args: Record<string, unknown>, accountId: string) => {
44
+ const normalizedMethod = String(method || '').trim();
45
+ if (!normalizedMethod) {
46
+ return Promise.reject(new Error('bncr client RPC requires a method name'));
47
+ }
48
+
49
+ const connIds = runtime.resolvePushConnIds(accountId);
50
+ if (!connIds.size) {
51
+ return Promise.reject(new Error(`no active bncr client connection for account ${accountId}`));
52
+ }
53
+ const targetConnIds = new Set([Array.from(connIds)[0]]);
54
+
55
+ const requestId = `rpc_${runtime.now().toString(36)}_${String(process.pid)}_${++requestSeq}`;
56
+ return new Promise<Record<string, unknown>>((resolve, reject) => {
57
+ const timer = setTimeout(() => {
58
+ cleanup(requestId);
59
+ reject(
60
+ new Error(
61
+ `bncr client RPC timeout method=${normalizedMethod}|accountId=${accountId}|timeoutMs=${timeoutMs}`,
62
+ ),
63
+ );
64
+ }, timeoutMs);
65
+ pending.set(requestId, {
66
+ method: normalizedMethod,
67
+ resolve,
68
+ reject,
69
+ timer,
70
+ });
71
+ runtime.broadcastToConnIds(
72
+ BNCR_CLIENT_RPC_REQUEST_EVENT,
73
+ {
74
+ requestId,
75
+ method: normalizedMethod,
76
+ args: args ?? {},
77
+ },
78
+ targetConnIds,
79
+ );
80
+ });
81
+ };
82
+
83
+ const handleResponse = (ctx: GatewayRequestHandlerOptions) => {
84
+ const params =
85
+ ctx.params && typeof ctx.params === 'object' ? (ctx.params as Record<string, unknown>) : {};
86
+ const requestId = typeof params.requestId === 'string' ? params.requestId.trim() : '';
87
+ const entry = requestId ? pending.get(requestId) : undefined;
88
+ if (!entry) {
89
+ runtime.logInfo('client-rpc', `rpc response ignored requestId=${requestId || '(missing)'}`, {
90
+ debugOnly: true,
91
+ });
92
+ ctx.respond(true, { ok: true, consumed: false, requestId });
93
+ return;
94
+ }
95
+
96
+ cleanup(requestId);
97
+ if (params.ok === true) {
98
+ const result =
99
+ params.result && typeof params.result === 'object'
100
+ ? (params.result as Record<string, unknown>)
101
+ : { value: params.result };
102
+ entry.resolve({
103
+ ok: true,
104
+ method: entry.method,
105
+ result,
106
+ });
107
+ } else {
108
+ const message =
109
+ typeof params.error === 'string'
110
+ ? params.error
111
+ : `bncr client RPC failed method=${entry.method}`;
112
+ entry.reject(new Error(message));
113
+ }
114
+ ctx.respond(true, { ok: true, consumed: true, requestId });
115
+ };
116
+
117
+ return {
118
+ call,
119
+ handleResponse,
120
+ pendingCount: () => pending.size,
121
+ shutdown: () => {
122
+ for (const [requestId, entry] of pending.entries()) {
123
+ clearTimeout(entry.timer);
124
+ entry.reject(new Error(`bncr client RPC cancelled method=${entry.method}`));
125
+ pending.delete(requestId);
126
+ }
127
+ },
128
+ };
129
+ }
@@ -6,6 +6,7 @@ export const BNCR_GATEWAY_METHODS = [
6
6
  'bncr.diagnostics',
7
7
  'bncr.deadLetter.inspect',
8
8
  'bncr.deadLetter.prune',
9
+ 'bncr.rpc.response',
9
10
  'bncr.file.init',
10
11
  'bncr.file.chunk',
11
12
  'bncr.file.complete',
@@ -2,6 +2,7 @@ import type { GatewayRequestHandlerOptions } from 'openclaw/plugin-sdk/core';
2
2
  import { buildCanonicalBncrSessionKey } from '../core/targets.ts';
3
3
  import type { BncrConnection, BncrRoute } from '../core/types.ts';
4
4
  import type { BncrGroupHistoryMap } from '../messaging/inbound/group-history.ts';
5
+ import type { BncrOutboundReplayCache } from '../messaging/inbound/outbound-replay-cache.ts';
5
6
  import type { parseBncrInboundParams } from '../messaging/inbound/parse.ts';
6
7
  import type { ReplyPayloadInput } from '../messaging/outbound/reply-enqueue.ts';
7
8
  import type {
@@ -134,6 +135,7 @@ export type BncrInboundHandlersRuntime = {
134
135
  defaultPublicAgentId: () => string;
135
136
  sceneRegistry: Map<string, BncrSceneRecord>;
136
137
  groupHistories: BncrGroupHistoryMap;
138
+ outboundReplayCache: BncrOutboundReplayCache;
137
139
  prepareInboundAcceptance: (args: {
138
140
  parsed: ParsedInboundParams;
139
141
  canonicalAgentId: string;
@@ -164,6 +166,7 @@ export type BncrInboundHandlersRuntime = {
164
166
  shouldAccumulate: boolean;
165
167
  sceneRegistry: Map<string, BncrSceneRecord>;
166
168
  groupHistories: BncrGroupHistoryMap;
169
+ outboundReplayCache: BncrOutboundReplayCache;
167
170
  defaultAdminAgentId: string;
168
171
  defaultPublicAgentId: string;
169
172
  now: () => number;
@@ -333,6 +336,7 @@ export function createBncrInboundHandlers(runtime: Omit<BncrInboundHandlersRunti
333
336
  shouldAccumulate,
334
337
  sceneRegistry: runtime.sceneRegistry,
335
338
  groupHistories: runtime.groupHistories,
339
+ outboundReplayCache: runtime.outboundReplayCache,
336
340
  defaultAdminAgentId,
337
341
  defaultPublicAgentId: runtime.defaultPublicAgentId(),
338
342
  now: runtime.now,
@@ -1,5 +1,6 @@
1
1
  import type { GatewayRequestHandlerOptions } from 'openclaw/plugin-sdk/core';
2
2
  import type { BncrConnection, BncrRoute } from '../core/types.ts';
3
+ import type { BncrOutboundReplayCache } from '../messaging/inbound/outbound-replay-cache.ts';
3
4
  import {
4
5
  createBncrFileInboundHandlersComponent,
5
6
  createBncrInboundHandlersComponent,
@@ -86,6 +87,7 @@ export function createBncrInboundSurfaceHandlersGroup(runtime: {
86
87
  >[0]['defaultPublicAgentId'];
87
88
  sceneRegistry: Parameters<typeof createBncrInboundHandlersComponent>[0]['sceneRegistry'];
88
89
  groupHistories: Parameters<typeof createBncrInboundHandlersComponent>[0]['groupHistories'];
90
+ outboundReplayCache: BncrOutboundReplayCache;
89
91
  prepareInboundAcceptance: Parameters<
90
92
  typeof createBncrInboundHandlersComponent
91
93
  >[0]['prepareInboundAcceptance'];
@@ -152,6 +154,7 @@ export function createBncrInboundSurfaceHandlersGroup(runtime: {
152
154
  defaultPublicAgentId: runtime.defaultPublicAgentId,
153
155
  sceneRegistry: runtime.sceneRegistry,
154
156
  groupHistories: runtime.groupHistories,
157
+ outboundReplayCache: runtime.outboundReplayCache,
155
158
  prepareInboundAcceptance: runtime.prepareInboundAcceptance,
156
159
  logInboundSummary: runtime.logInboundSummary,
157
160
  flushPushQueueBestEffort: runtime.flushPushQueueBestEffort,
@@ -26,6 +26,7 @@ export type BncrOutboxAckOutcomeRuntime = {
26
26
  scheduleSave: () => void;
27
27
  resolveMessageAck: (messageId: string, result: 'acked' | 'timeout') => void;
28
28
  moveToDeadLetter: (entry: OutboxEntry, reason: string) => void;
29
+ markRecentOutboundAcked?: (entry: OutboxEntry) => void;
29
30
  logOutboxAckSummary: (
30
31
  scope: 'outbox ack ok' | 'outbox ack ok late' | 'outbox ack retry' | 'outbox ack fatal',
31
32
  args: {
@@ -69,6 +70,7 @@ export function createBncrOutboxAckOutcome(runtime: BncrOutboxAckOutcomeRuntime)
69
70
  runtime.deleteOutboxEntry(args.messageId);
70
71
  runtime.scheduleSave();
71
72
  runtime.resolveMessageAck(args.messageId, 'acked');
73
+ runtime.markRecentOutboundAcked?.(args.entry);
72
74
  runtime.logOutboxAckSummary(
73
75
  telemetryPatch.lateAccepted ? 'outbox ack ok late' : 'outbox ack ok',
74
76
  {