@xmoxmo/bncr 0.3.6 → 0.3.7

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 (164) hide show
  1. package/README.md +5 -0
  2. package/dist/index.js +28 -5
  3. package/index.ts +55 -721
  4. package/package.json +8 -4
  5. package/scripts/check-pack.mjs +93 -18
  6. package/scripts/check-register-drift.mjs +35 -13
  7. package/scripts/selfcheck.mjs +80 -11
  8. package/src/bootstrap/channel-plugin-runtime.ts +81 -0
  9. package/src/bootstrap/cli.ts +97 -0
  10. package/src/bootstrap/register-runtime-gateway.ts +129 -0
  11. package/src/bootstrap/register-runtime-helpers.ts +140 -0
  12. package/src/bootstrap/register-runtime-singleton.ts +137 -0
  13. package/src/bootstrap/register-runtime.ts +201 -0
  14. package/src/bootstrap/runtime-discovery.ts +187 -0
  15. package/src/bootstrap/runtime-loader.ts +54 -0
  16. package/src/channel.ts +1590 -4967
  17. package/src/core/accounts.ts +23 -4
  18. package/src/core/dead-letter-diagnostics.ts +37 -5
  19. package/src/core/diagnostics.ts +31 -15
  20. package/src/core/downlink-health.ts +3 -11
  21. package/src/core/extended-diagnostics.ts +78 -36
  22. package/src/core/file-transfer-payloads.ts +1 -1
  23. package/src/core/logging.ts +1 -0
  24. package/src/core/outbox-enqueue.ts +13 -2
  25. package/src/core/outbox-entry-builders.ts +2 -0
  26. package/src/core/outbox-summary.ts +75 -3
  27. package/src/core/permissions.ts +15 -2
  28. package/src/core/persisted-outbox-entry.ts +21 -6
  29. package/src/core/policy.ts +45 -4
  30. package/src/core/probe.ts +3 -15
  31. package/src/core/register-trace.ts +3 -3
  32. package/src/core/status.ts +43 -4
  33. package/src/core/targets.ts +216 -205
  34. package/src/core/types.ts +221 -0
  35. package/src/core/value-sanitize.ts +29 -0
  36. package/src/messaging/inbound/commands.ts +147 -172
  37. package/src/messaging/inbound/context-facts.ts +4 -2
  38. package/src/messaging/inbound/contracts.ts +70 -0
  39. package/src/messaging/inbound/dispatch-prep.ts +303 -0
  40. package/src/messaging/inbound/dispatch.ts +49 -462
  41. package/src/messaging/inbound/gate.ts +18 -5
  42. package/src/messaging/inbound/last-route.ts +10 -4
  43. package/src/messaging/inbound/media-url-download.ts +109 -0
  44. package/src/messaging/inbound/native-command-runtime.ts +225 -0
  45. package/src/messaging/inbound/parse.ts +2 -1
  46. package/src/messaging/inbound/remote-media.ts +49 -0
  47. package/src/messaging/inbound/reply-config.ts +16 -4
  48. package/src/messaging/inbound/reply-dispatch.ts +162 -0
  49. package/src/messaging/inbound/runtime-compat.ts +31 -10
  50. package/src/messaging/inbound/session-label.ts +15 -7
  51. package/src/messaging/inbound/turn-context.ts +131 -0
  52. package/src/messaging/outbound/actions.ts +24 -10
  53. package/src/messaging/outbound/diagnostics-debug-builders.ts +365 -0
  54. package/src/messaging/outbound/diagnostics.ts +31 -355
  55. package/src/messaging/outbound/durable-message-adapter.ts +20 -16
  56. package/src/messaging/outbound/durable-queue-adapter.ts +20 -7
  57. package/src/messaging/outbound/media.ts +24 -13
  58. package/src/messaging/outbound/reply-enqueue-media.ts +181 -0
  59. package/src/messaging/outbound/reply-enqueue.ts +46 -155
  60. package/src/messaging/outbound/send-params.ts +3 -0
  61. package/src/messaging/outbound/send.ts +19 -10
  62. package/src/messaging/outbound/session-route.ts +18 -3
  63. package/src/openclaw/channel-runtime-contracts.ts +76 -0
  64. package/src/openclaw/config-runtime.ts +13 -7
  65. package/src/openclaw/inbound-session-runtime.ts +7 -3
  66. package/src/openclaw/ingress-runtime.ts +17 -27
  67. package/src/openclaw/reply-runtime.ts +54 -59
  68. package/src/openclaw/routing-runtime.ts +35 -18
  69. package/src/openclaw/runtime-surface.ts +156 -12
  70. package/src/openclaw/sdk-helpers.ts +8 -1
  71. package/src/openclaw/session-route-runtime.ts +12 -12
  72. package/src/plugin/ack-outbox-runtime-group.ts +264 -0
  73. package/src/plugin/bridge-ack-facade.ts +137 -0
  74. package/src/plugin/bridge-connection-facade.ts +111 -0
  75. package/src/plugin/bridge-diagnostics-facade.ts +23 -0
  76. package/src/plugin/bridge-drain-facade.ts +98 -0
  77. package/src/plugin/bridge-extended-diagnostics-facade.ts +149 -0
  78. package/src/plugin/bridge-file-transfer-push-facade.ts +140 -0
  79. package/src/plugin/bridge-lifecycle.ts +156 -0
  80. package/src/plugin/bridge-media-facade.ts +241 -0
  81. package/src/plugin/bridge-outbox-facade.ts +182 -0
  82. package/src/plugin/bridge-runtime-helpers.ts +266 -0
  83. package/src/plugin/bridge-runtime-snapshots.ts +104 -0
  84. package/src/plugin/bridge-runtime-surface-facade.ts +8 -0
  85. package/src/plugin/bridge-status-facade.ts +76 -0
  86. package/src/plugin/bridge-status-worker-facade.ts +72 -0
  87. package/src/plugin/bridge-support-runtime.ts +137 -0
  88. package/src/plugin/bridge-surface-handlers-group.ts +242 -0
  89. package/src/plugin/bridge-surface-helpers.ts +28 -0
  90. package/src/plugin/capabilities.ts +1 -3
  91. package/src/plugin/channel-components.ts +289 -0
  92. package/src/plugin/channel-inbound-helpers.ts +149 -0
  93. package/src/plugin/channel-plugin-bridge-group.ts +129 -0
  94. package/src/plugin/channel-plugin-surface-group.ts +202 -0
  95. package/src/plugin/channel-runtime-builders-delivery.ts +513 -0
  96. package/src/plugin/channel-runtime-builders-status.ts +331 -0
  97. package/src/plugin/channel-runtime-builders.ts +25 -0
  98. package/src/plugin/channel-runtime-constants.ts +40 -0
  99. package/src/plugin/channel-runtime-types.ts +146 -0
  100. package/src/plugin/channel-send-runtime-group.ts +37 -0
  101. package/src/plugin/channel-send.ts +226 -0
  102. package/src/plugin/channel-utils.ts +102 -0
  103. package/src/plugin/config.ts +24 -3
  104. package/src/plugin/connection-handlers-helpers.ts +254 -0
  105. package/src/plugin/connection-handlers.ts +440 -0
  106. package/src/plugin/connection-state-helpers.ts +159 -0
  107. package/src/plugin/connection-state-runtime-group.ts +51 -0
  108. package/src/plugin/connection-state.ts +527 -0
  109. package/src/plugin/diagnostics-handlers.ts +211 -0
  110. package/src/plugin/error-message.ts +15 -0
  111. package/src/plugin/file-ack-runtime.ts +284 -0
  112. package/src/plugin/file-inbound-abort.ts +112 -0
  113. package/src/plugin/file-inbound-chunk.ts +146 -0
  114. package/src/plugin/file-inbound-complete.ts +153 -0
  115. package/src/plugin/file-inbound-handlers.ts +19 -0
  116. package/src/plugin/file-inbound-init.ts +122 -0
  117. package/src/plugin/file-inbound-runtime.ts +51 -0
  118. package/src/plugin/file-inbound-state.ts +62 -0
  119. package/src/plugin/file-transfer-logs.ts +227 -0
  120. package/src/plugin/file-transfer-orchestrator-chunk.ts +135 -0
  121. package/src/plugin/file-transfer-orchestrator.ts +304 -0
  122. package/src/plugin/file-transfer-runtime-group.ts +102 -0
  123. package/src/plugin/file-transfer-send.ts +89 -0
  124. package/src/plugin/file-transfer-setup.ts +206 -0
  125. package/src/plugin/gateway-event-context.ts +41 -0
  126. package/src/plugin/gateway-runtime.ts +14 -4
  127. package/src/plugin/inbound-acceptance.ts +107 -0
  128. package/src/plugin/inbound-handlers.ts +248 -0
  129. package/src/plugin/inbound-surface-handlers-group.ts +152 -0
  130. package/src/plugin/media-dedupe-runtime.ts +90 -0
  131. package/src/plugin/media-orchestrators-runtime-group.ts +316 -0
  132. package/src/plugin/message-ack-runtime.ts +284 -0
  133. package/src/plugin/message-send.ts +16 -6
  134. package/src/plugin/messaging.ts +98 -36
  135. package/src/plugin/outbound.ts +50 -8
  136. package/src/plugin/outbox-ack-logs.ts +136 -0
  137. package/src/plugin/outbox-ack-outcome.ts +128 -0
  138. package/src/plugin/outbox-drain-ack.ts +145 -0
  139. package/src/plugin/outbox-drain-failure.ts +84 -0
  140. package/src/plugin/outbox-drain-loop.ts +554 -0
  141. package/src/plugin/outbox-drain-post-push.ts +159 -0
  142. package/src/plugin/outbox-drain-runtime.ts +141 -0
  143. package/src/plugin/outbox-drain-schedule.ts +116 -0
  144. package/src/plugin/outbox-file-push-flow.ts +69 -0
  145. package/src/plugin/outbox-push-route-runtime-group.ts +81 -0
  146. package/src/plugin/outbox-push.ts +267 -0
  147. package/src/plugin/outbox-route.ts +181 -0
  148. package/src/plugin/outbox-text-push-flow.ts +90 -0
  149. package/src/plugin/runtime-diagnostics-assembler.ts +183 -0
  150. package/src/plugin/runtime-diagnostics-helpers.ts +302 -0
  151. package/src/plugin/runtime-diagnostics-payload-builders.ts +171 -0
  152. package/src/plugin/runtime-diagnostics-snapshot.ts +31 -0
  153. package/src/plugin/setup.ts +33 -6
  154. package/src/plugin/state-store.ts +249 -0
  155. package/src/plugin/state-transient-runtime-group.ts +105 -0
  156. package/src/plugin/status-runtime.ts +251 -0
  157. package/src/plugin/status.ts +33 -7
  158. package/src/plugin/target-runtime.ts +141 -0
  159. package/src/plugin/target-status-runtime-group.ts +130 -0
  160. package/src/plugin/transient-state-runtime.ts +82 -0
  161. package/src/runtime/outbound-ack-timeout.ts +5 -3
  162. package/src/runtime/outbound-flags.ts +24 -8
  163. package/src/runtime/status-snapshots.ts +36 -7
  164. package/src/runtime/status-worker.ts +34 -4
@@ -0,0 +1,316 @@
1
+ import type { BncrRoute, FileSendTransferState, OutboxEntry } from '../core/types.ts';
2
+ import { normalizeMessageText } from '../messaging/outbound/media-dedupe.ts';
3
+ import type {
4
+ NormalizedReplyPayload,
5
+ OutboundReplyTargetPolicy,
6
+ ReplyMediaEntriesParams,
7
+ ReplyPayloadInput,
8
+ } from '../messaging/outbound/reply-enqueue.ts';
9
+ import {
10
+ enqueueNormalizedReplyPayload,
11
+ enqueueReplyTextEntry,
12
+ normalizeReplyPayload,
13
+ } from '../messaging/outbound/reply-enqueue.ts';
14
+ import {
15
+ enqueueReplyMediaFallbackTextEntry,
16
+ enqueueReplyMediaFileTransferEntry,
17
+ enqueueSingleReplyMediaEntry,
18
+ } from '../messaging/outbound/reply-enqueue-media.ts';
19
+ import type { createBncrFileAckRuntime } from './file-ack-runtime.ts';
20
+ import { createBncrFileTransferOrchestrator } from './file-transfer-orchestrator.ts';
21
+ import type { createBncrFileTransferSetup } from './file-transfer-setup.ts';
22
+
23
+ type PreparedOutboundTransfer = Awaited<
24
+ ReturnType<ReturnType<typeof createBncrFileTransferSetup>['prepareOutboundTransfer']>
25
+ >;
26
+ type FileAckPayload = Awaited<
27
+ ReturnType<ReturnType<typeof createBncrFileAckRuntime>['waitForFileAck']>
28
+ >;
29
+
30
+ function buildReplyMediaEntryHelpers(runtime: {
31
+ logInfo: (scope: string | undefined, message: string, options?: { debugOnly?: boolean }) => void;
32
+ enqueueOutbound: (entry: OutboxEntry) => void;
33
+ buildTextOutboxEntry: (args: {
34
+ accountId: string;
35
+ sessionKey: string;
36
+ route: BncrRoute;
37
+ text: string;
38
+ kind?: 'tool' | 'block' | 'final';
39
+ replyToId?: string;
40
+ replyTargetPolicy?: OutboundReplyTargetPolicy;
41
+ }) => OutboxEntry;
42
+ buildFileTransferOutboxEntry: (args: {
43
+ accountId: string;
44
+ sessionKey: string;
45
+ route: BncrRoute;
46
+ mediaUrl: string;
47
+ mediaLocalRoots?: readonly string[];
48
+ text: string;
49
+ asVoice: boolean;
50
+ audioAsVoice: boolean;
51
+ type?: string;
52
+ kind?: 'tool' | 'block' | 'final';
53
+ replyToId?: string;
54
+ replyTargetPolicy?: OutboundReplyTargetPolicy;
55
+ }) => OutboxEntry;
56
+ rememberRecentMediaSend: (args: {
57
+ sessionKey: string;
58
+ mediaUrl: string;
59
+ text: string;
60
+ replyToId: string;
61
+ createdAt: number;
62
+ }) => void;
63
+ }) {
64
+ return {
65
+ enqueueReplyMediaFallbackTextEntry: (
66
+ params: Parameters<typeof enqueueReplyMediaFallbackTextEntry>[0],
67
+ ) =>
68
+ enqueueReplyMediaFallbackTextEntry(params, {
69
+ logInfo: runtime.logInfo,
70
+ enqueueOutbound: runtime.enqueueOutbound,
71
+ buildTextOutboxEntry: runtime.buildTextOutboxEntry,
72
+ }),
73
+ enqueueReplyMediaFileTransferEntry: (
74
+ params: Parameters<typeof enqueueReplyMediaFileTransferEntry>[0],
75
+ ) =>
76
+ enqueueReplyMediaFileTransferEntry(params, {
77
+ enqueueOutbound: runtime.enqueueOutbound,
78
+ buildFileTransferOutboxEntry: runtime.buildFileTransferOutboxEntry,
79
+ rememberRecentMediaSend: runtime.rememberRecentMediaSend,
80
+ }),
81
+ };
82
+ }
83
+
84
+ function buildReplyTextEntryHelper(runtime: {
85
+ enqueueOutbound: (entry: OutboxEntry) => void;
86
+ buildTextOutboxEntry: (args: {
87
+ accountId: string;
88
+ sessionKey: string;
89
+ route: BncrRoute;
90
+ text: string;
91
+ kind?: 'tool' | 'block' | 'final';
92
+ replyToId?: string;
93
+ replyTargetPolicy?: OutboundReplyTargetPolicy;
94
+ }) => OutboxEntry;
95
+ }) {
96
+ return (params: {
97
+ accountId: string;
98
+ sessionKey: string;
99
+ route: BncrRoute;
100
+ payload: NormalizedReplyPayload;
101
+ }) =>
102
+ enqueueReplyTextEntry(params, {
103
+ enqueueOutbound: runtime.enqueueOutbound,
104
+ buildTextOutboxEntry: runtime.buildTextOutboxEntry,
105
+ });
106
+ }
107
+
108
+ function normalizeReplyOrchestratorPayload(args: {
109
+ payload: ReplyPayloadInput;
110
+ asString: (value: unknown, fallback?: string) => string;
111
+ replyTargetPolicy?: OutboundReplyTargetPolicy;
112
+ }) {
113
+ return normalizeReplyPayload(
114
+ args.payload,
115
+ { asString: args.asString },
116
+ {
117
+ replyTargetPolicy: args.replyTargetPolicy,
118
+ },
119
+ );
120
+ }
121
+
122
+ export function createBncrMediaOrchestratorsRuntimeGroup(runtime: {
123
+ now: () => number;
124
+ asString: (value: unknown, fallback?: string) => string;
125
+ fileSendTransfers: Map<string, FileSendTransferState>;
126
+ getGatewayContext: () => {
127
+ broadcastToConnIds: (event: string, payload: unknown, connIds: ReadonlySet<string>) => void;
128
+ } | null;
129
+ fileInitEvent: string;
130
+ fileAbortEvent: string;
131
+ prepareOutboundTransfer: (args: {
132
+ accountId: string;
133
+ sessionKey: string;
134
+ route: BncrRoute;
135
+ mediaUrl: string;
136
+ mediaLocalRoots?: readonly string[];
137
+ hasGatewayContext: boolean;
138
+ }) => Promise<PreparedOutboundTransfer>;
139
+ sendChunk: (args: {
140
+ transferId: string;
141
+ accountId: string;
142
+ chunkIndex: number;
143
+ attempt: number;
144
+ offset: number;
145
+ size: number;
146
+ chunkSha256: string;
147
+ base64: string;
148
+ connIds: ReadonlySet<string>;
149
+ }) => void;
150
+ sendComplete: (args: {
151
+ transferId: string;
152
+ accountId: string;
153
+ connIds: ReadonlySet<string>;
154
+ }) => void;
155
+ waitForFileAck: (params: {
156
+ transferId: string;
157
+ stage: string;
158
+ chunkIndex?: number;
159
+ timeoutMs: number;
160
+ }) => Promise<FileAckPayload>;
161
+ logFileTransferChunkAck: (args: {
162
+ transferId: string;
163
+ accountId: string;
164
+ chunkIndex: number;
165
+ attempt: number;
166
+ }) => void;
167
+ logFileTransferChunkAckFail: (args: {
168
+ transferId: string;
169
+ accountId: string;
170
+ chunkIndex: number;
171
+ attempt: number;
172
+ error: unknown;
173
+ }) => void;
174
+ logFileTransferCompleteAck: (args: {
175
+ transferId: string;
176
+ accountId: string;
177
+ payload: { path: string };
178
+ }) => void;
179
+ logInfo: (scope: string | undefined, message: string, options?: { debugOnly?: boolean }) => void;
180
+ logEnqueueFromReply: (args: {
181
+ accountId: string;
182
+ sessionKey: string;
183
+ route: BncrRoute;
184
+ payload: NormalizedReplyPayload;
185
+ }) => void;
186
+ enqueueOutbound: (entry: OutboxEntry) => void;
187
+ buildTextOutboxEntry: (args: {
188
+ accountId: string;
189
+ sessionKey: string;
190
+ route: BncrRoute;
191
+ text: string;
192
+ kind?: 'tool' | 'block' | 'final';
193
+ replyToId?: string;
194
+ replyTargetPolicy?: OutboundReplyTargetPolicy;
195
+ }) => OutboxEntry;
196
+ buildFileTransferOutboxEntry: (args: {
197
+ accountId: string;
198
+ sessionKey: string;
199
+ route: BncrRoute;
200
+ mediaUrl: string;
201
+ mediaLocalRoots?: readonly string[];
202
+ text: string;
203
+ asVoice: boolean;
204
+ audioAsVoice: boolean;
205
+ type?: string;
206
+ kind?: 'tool' | 'block' | 'final';
207
+ replyToId?: string;
208
+ replyTargetPolicy?: OutboundReplyTargetPolicy;
209
+ }) => OutboxEntry;
210
+ rememberRecentMediaSend: (args: {
211
+ sessionKey: string;
212
+ mediaUrl: string;
213
+ text: string;
214
+ replyToId: string;
215
+ createdAt: number;
216
+ }) => void;
217
+ tryBuildMediaDedupeFallback: (args: {
218
+ sessionKey: string;
219
+ mediaUrl: string;
220
+ text: string;
221
+ replyToId: string;
222
+ currentTime: number;
223
+ }) => {
224
+ text: string;
225
+ reason: 'same-text-sent-checkmark' | 'text-changed-downgrade';
226
+ } | null;
227
+ }) {
228
+ const fileTransferOrchestrator = createBncrFileTransferOrchestrator({
229
+ now: runtime.now,
230
+ fileSendTransfers: runtime.fileSendTransfers,
231
+ getGatewayContext: runtime.getGatewayContext,
232
+ fileInitEvent: runtime.fileInitEvent,
233
+ fileAbortEvent: runtime.fileAbortEvent,
234
+ prepareOutboundTransfer: runtime.prepareOutboundTransfer,
235
+ sendChunk: runtime.sendChunk,
236
+ sendComplete: runtime.sendComplete,
237
+ waitForFileAck: runtime.waitForFileAck,
238
+ logFileTransferChunkAck: runtime.logFileTransferChunkAck,
239
+ logFileTransferChunkAckFail: runtime.logFileTransferChunkAckFail,
240
+ logFileTransferCompleteAck: runtime.logFileTransferCompleteAck,
241
+ });
242
+
243
+ const replyMediaEntryHelpers = buildReplyMediaEntryHelpers(runtime);
244
+ const replyTextEntryHelper = buildReplyTextEntryHelper(runtime);
245
+
246
+ const enqueueReplyMediaEntries = (params: ReplyMediaEntriesParams) => {
247
+ let first = true;
248
+ const currentTime = runtime.now();
249
+
250
+ for (const mediaUrl of params.payload.mediaList) {
251
+ const normalizedText = normalizeMessageText(first ? params.payload.text : '');
252
+ const fallback = runtime.tryBuildMediaDedupeFallback({
253
+ sessionKey: params.sessionKey,
254
+ mediaUrl,
255
+ text: normalizedText,
256
+ replyToId: params.payload.replyToId,
257
+ currentTime,
258
+ });
259
+
260
+ enqueueSingleReplyMediaEntry(
261
+ {
262
+ params,
263
+ mediaUrl,
264
+ normalizedText,
265
+ text: first ? params.payload.text : '',
266
+ fallback,
267
+ currentTime,
268
+ },
269
+ replyMediaEntryHelpers,
270
+ );
271
+
272
+ first = false;
273
+ }
274
+ };
275
+
276
+ const enqueueFromReply = (params: {
277
+ accountId: string;
278
+ sessionKey: string;
279
+ route: BncrRoute;
280
+ payload: ReplyPayloadInput;
281
+ mediaLocalRoots?: readonly string[];
282
+ replyTargetPolicy?: OutboundReplyTargetPolicy;
283
+ }) => {
284
+ const { accountId, sessionKey, route, payload, mediaLocalRoots, replyTargetPolicy } = params;
285
+ const normalized = normalizeReplyOrchestratorPayload({
286
+ payload,
287
+ asString: runtime.asString,
288
+ replyTargetPolicy,
289
+ });
290
+
291
+ enqueueNormalizedReplyPayload(
292
+ {
293
+ accountId,
294
+ sessionKey,
295
+ route,
296
+ payload: normalized,
297
+ mediaLocalRoots,
298
+ },
299
+ {
300
+ logEnqueueFromReply: runtime.logEnqueueFromReply,
301
+ enqueueReplyMediaEntries,
302
+ enqueueReplyTextEntry: replyTextEntryHelper,
303
+ },
304
+ );
305
+ };
306
+
307
+ const replyMediaOrchestrator = {
308
+ enqueueFromReply,
309
+ enqueueReplyMediaEntries,
310
+ };
311
+
312
+ return {
313
+ fileTransferOrchestrator,
314
+ replyMediaOrchestrator,
315
+ };
316
+ }
@@ -0,0 +1,284 @@
1
+ import type { GatewayRequestHandlerOptions } from 'openclaw/plugin-sdk/core';
2
+ import type { OutboxEntry } from '../core/types.ts';
3
+ import { buildBncrGatewayEventContext } from './gateway-event-context.ts';
4
+
5
+ export type MessageAckWaitResult = 'acked' | 'timeout';
6
+
7
+ export type MessageAckWaiter = {
8
+ promise: Promise<MessageAckWaitResult>;
9
+ resolve: (result: MessageAckWaitResult) => void;
10
+ timer: NodeJS.Timeout;
11
+ };
12
+
13
+ export function createBncrMessageAckRuntime(runtime: {
14
+ asString: (value: unknown, fallback?: string) => string;
15
+ now: () => number;
16
+ clampFiniteNumber: (value: unknown, fallback: number, min: number, max: number) => number;
17
+ normalizeAccountId: (accountId: string) => string;
18
+ recommendedAckTimeoutMaxMs: number;
19
+ messageAckWaiters: Map<string, MessageAckWaiter>;
20
+ stopped: () => boolean;
21
+ outbox: Map<string, OutboxEntry>;
22
+ observeLease: (
23
+ kind:
24
+ | 'connect'
25
+ | 'inbound'
26
+ | 'activity'
27
+ | 'ack'
28
+ | 'file.init'
29
+ | 'file.chunk'
30
+ | 'file.complete'
31
+ | 'file.abort',
32
+ payload: Record<string, unknown>,
33
+ ) => { stale: boolean };
34
+ rememberGatewayContext: (context: GatewayRequestHandlerOptions['context']) => void;
35
+ markSeen: (accountId: string, connId: string, clientId?: string) => void;
36
+ logInfo: (scope: string | undefined, message: string, options?: { debugOnly?: boolean }) => void;
37
+ logWarn: (scope: string | undefined, message: string, options?: { debugOnly?: boolean }) => void;
38
+ handleAckOk: (args: {
39
+ accountId: string;
40
+ messageId: string;
41
+ connId: string;
42
+ clientId?: string;
43
+ stale: boolean;
44
+ entry: OutboxEntry;
45
+ }) => void;
46
+ handleAckFatal: (args: {
47
+ entry: OutboxEntry;
48
+ messageId: string;
49
+ connId: string;
50
+ clientId?: string;
51
+ error: string;
52
+ }) => void;
53
+ handleAckRetry: (args: {
54
+ entry: OutboxEntry;
55
+ messageId: string;
56
+ connId: string;
57
+ clientId?: string;
58
+ error: string;
59
+ }) => void;
60
+ flushPushQueueBestEffort: (args?: {
61
+ accountId?: string;
62
+ trigger?: string;
63
+ reason?: string;
64
+ }) => void;
65
+ outboundFlushTriggerAckOk: string;
66
+ outboundFlushReasonMessageAcked: string;
67
+ }) {
68
+ function clearAllMessageAckWaiters(result: MessageAckWaitResult = 'timeout') {
69
+ for (const waiter of runtime.messageAckWaiters.values()) {
70
+ clearTimeout(waiter.timer);
71
+ waiter.resolve(result);
72
+ }
73
+ runtime.messageAckWaiters.clear();
74
+ }
75
+
76
+ function resolveMessageAck(messageId: string, result: MessageAckWaitResult = 'acked') {
77
+ const key = runtime.asString(messageId).trim();
78
+ if (!key) return false;
79
+ const waiter = runtime.messageAckWaiters.get(key);
80
+ if (!waiter) return false;
81
+ runtime.messageAckWaiters.delete(key);
82
+ clearTimeout(waiter.timer);
83
+ waiter.resolve(result);
84
+ return true;
85
+ }
86
+
87
+ async function waitForMessageAck(
88
+ messageId: string,
89
+ waitMs: number,
90
+ ): Promise<MessageAckWaitResult> {
91
+ const key = runtime.asString(messageId).trim();
92
+ const timeoutMs = runtime.clampFiniteNumber(waitMs, 0, 0, runtime.recommendedAckTimeoutMaxMs);
93
+ if (!key || !timeoutMs) return 'timeout';
94
+
95
+ const existing = runtime.messageAckWaiters.get(key);
96
+ if (existing) {
97
+ runtime.logWarn('outbox', `message-ack-waiter-reuse ${JSON.stringify({ messageId: key })}`, {
98
+ debugOnly: true,
99
+ });
100
+ return await existing.promise;
101
+ }
102
+
103
+ let timer: NodeJS.Timeout;
104
+ let resolveWaiter!: (result: MessageAckWaitResult) => void;
105
+ const promise = new Promise<MessageAckWaitResult>((resolve) => {
106
+ resolveWaiter = resolve;
107
+ timer = setTimeout(() => {
108
+ runtime.messageAckWaiters.delete(key);
109
+ resolve('timeout');
110
+ }, timeoutMs);
111
+ });
112
+
113
+ runtime.messageAckWaiters.set(key, { promise, resolve: resolveWaiter, timer: timer! });
114
+ return await promise;
115
+ }
116
+
117
+ function prepareAckHandling(args: {
118
+ params: GatewayRequestHandlerOptions['params'];
119
+ respond: GatewayRequestHandlerOptions['respond'];
120
+ client: GatewayRequestHandlerOptions['client'];
121
+ context: GatewayRequestHandlerOptions['context'];
122
+ }): {
123
+ accountId: string;
124
+ connId: string;
125
+ clientId?: string;
126
+ messageId: string;
127
+ entry: OutboxEntry;
128
+ staleObserved: { stale: boolean };
129
+ } | null {
130
+ const { params, respond, client, context } = args;
131
+ const gatewayContext = buildBncrGatewayEventContext({
132
+ params,
133
+ client,
134
+ context,
135
+ asString: runtime.asString,
136
+ normalizeAccountId: runtime.normalizeAccountId,
137
+ now: runtime.now,
138
+ });
139
+ const { accountId, connId, clientId } = gatewayContext;
140
+ const messageId = runtime.asString(params?.messageId || '').trim();
141
+ const staleObserved = runtime.observeLease('ack', params ?? {});
142
+
143
+ runtime.logInfo(
144
+ 'outbox',
145
+ `ack ${JSON.stringify({
146
+ accountId,
147
+ messageId,
148
+ ok: params?.ok !== false,
149
+ fatal: params?.fatal === true,
150
+ error: runtime.asString(params?.error || ''),
151
+ stale: staleObserved.stale,
152
+ })}`,
153
+ { debugOnly: true },
154
+ );
155
+ if (!messageId) {
156
+ respond(false, { error: 'messageId required' });
157
+ return null;
158
+ }
159
+
160
+ if (runtime.stopped()) {
161
+ respond(true, { ok: true, ignored: true, reason: 'service-stopped' });
162
+ return null;
163
+ }
164
+
165
+ const entry = runtime.outbox.get(messageId);
166
+ if (!entry) {
167
+ respond(true, { ok: true, message: 'already-acked-or-missing', stale: staleObserved.stale });
168
+ return null;
169
+ }
170
+
171
+ if (entry.accountId !== accountId) {
172
+ respond(false, { error: 'account mismatch' });
173
+ return null;
174
+ }
175
+
176
+ if (staleObserved.stale) {
177
+ const sameConn = !!entry.lastPushConnId && entry.lastPushConnId === connId;
178
+ const sameClient =
179
+ !entry.lastPushConnId &&
180
+ !!entry.lastPushClientId &&
181
+ !!clientId &&
182
+ entry.lastPushClientId === clientId;
183
+ if (!(sameConn || sameClient)) {
184
+ runtime.logWarn(
185
+ 'stale',
186
+ `ignore kind=ack accountId=${accountId} connId=${connId} clientId=${clientId || '-'} messageId=${messageId} reason=owner-mismatch lastPushConnId=${entry.lastPushConnId || '-'} lastPushClientId=${entry.lastPushClientId || '-'}`,
187
+ { debugOnly: true },
188
+ );
189
+ respond(true, { ok: true, stale: true, ignored: true });
190
+ return null;
191
+ }
192
+ } else {
193
+ runtime.rememberGatewayContext(gatewayContext.context);
194
+ runtime.markSeen(accountId, connId, clientId);
195
+ }
196
+
197
+ return {
198
+ accountId,
199
+ connId,
200
+ clientId,
201
+ messageId,
202
+ entry,
203
+ staleObserved,
204
+ };
205
+ }
206
+
207
+ function respondAckResult(
208
+ respond: GatewayRequestHandlerOptions['respond'],
209
+ stale: boolean,
210
+ result: { ok: true; movedToDeadLetter?: true; willRetry?: true },
211
+ ) {
212
+ respond(true, stale ? { ...result, stale: true, staleAccepted: true } : result);
213
+ }
214
+
215
+ function handleAckOutcome(args: {
216
+ params: GatewayRequestHandlerOptions['params'];
217
+ respond: GatewayRequestHandlerOptions['respond'];
218
+ accountId: string;
219
+ connId: string;
220
+ clientId?: string;
221
+ messageId: string;
222
+ entry: OutboxEntry;
223
+ staleObserved: { stale: boolean };
224
+ }) {
225
+ const { params, respond, accountId, connId, clientId, messageId, entry, staleObserved } = args;
226
+ const ok = params?.ok !== false;
227
+ const fatal = params?.fatal === true;
228
+
229
+ if (ok) {
230
+ runtime.handleAckOk({
231
+ accountId,
232
+ messageId,
233
+ connId,
234
+ clientId,
235
+ stale: staleObserved.stale,
236
+ entry,
237
+ });
238
+ respondAckResult(respond, staleObserved.stale, { ok: true });
239
+ runtime.flushPushQueueBestEffort({
240
+ accountId,
241
+ trigger: runtime.outboundFlushTriggerAckOk,
242
+ reason: runtime.outboundFlushReasonMessageAcked,
243
+ });
244
+ return;
245
+ }
246
+
247
+ if (fatal) {
248
+ const error = runtime.asString(params?.error || 'fatal-ack');
249
+ runtime.handleAckFatal({
250
+ entry,
251
+ messageId,
252
+ connId,
253
+ clientId,
254
+ error,
255
+ });
256
+ respondAckResult(respond, staleObserved.stale, {
257
+ ok: true,
258
+ movedToDeadLetter: true,
259
+ });
260
+ return;
261
+ }
262
+
263
+ runtime.handleAckRetry({
264
+ entry,
265
+ messageId,
266
+ connId,
267
+ clientId,
268
+ error: runtime.asString(params?.error || 'retryable-ack'),
269
+ });
270
+
271
+ respondAckResult(respond, staleObserved.stale, {
272
+ ok: true,
273
+ willRetry: true,
274
+ });
275
+ }
276
+
277
+ return {
278
+ clearAllMessageAckWaiters,
279
+ resolveMessageAck,
280
+ waitForMessageAck,
281
+ prepareAckHandling,
282
+ handleAckOutcome,
283
+ };
284
+ }
@@ -1,13 +1,23 @@
1
+ import type { ChannelMessageSendResult } from 'openclaw/plugin-sdk/channel-message';
2
+
3
+ type MessageSendContext = Record<string, unknown>;
4
+
1
5
  export type BncrMessageSendBridge = {
2
- channelMessageSendText: (ctx: any) => unknown | Promise<unknown>;
3
- channelMessageSendMedia: (ctx: any) => unknown | Promise<unknown>;
4
- channelMessageSendPayload: (ctx: any) => unknown | Promise<unknown>;
6
+ channelMessageSendText: (
7
+ ctx: MessageSendContext,
8
+ ) => ChannelMessageSendResult | Promise<ChannelMessageSendResult>;
9
+ channelMessageSendMedia: (
10
+ ctx: MessageSendContext,
11
+ ) => ChannelMessageSendResult | Promise<ChannelMessageSendResult>;
12
+ channelMessageSendPayload: (
13
+ ctx: MessageSendContext,
14
+ ) => ChannelMessageSendResult | Promise<ChannelMessageSendResult>;
5
15
  };
6
16
 
7
17
  export function createBncrMessageSend(getBridge: () => BncrMessageSendBridge) {
8
18
  return {
9
- text: async (ctx: any) => getBridge().channelMessageSendText(ctx),
10
- media: async (ctx: any) => getBridge().channelMessageSendMedia(ctx),
11
- payload: async (ctx: any) => getBridge().channelMessageSendPayload(ctx),
19
+ text: async (ctx: MessageSendContext) => getBridge().channelMessageSendText(ctx),
20
+ media: async (ctx: MessageSendContext) => getBridge().channelMessageSendMedia(ctx),
21
+ payload: async (ctx: MessageSendContext) => getBridge().channelMessageSendPayload(ctx),
12
22
  };
13
23
  }