@xmoxmo/bncr 0.4.2 → 0.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/scripts/selfcheck.mjs +105 -2
- package/src/channel.ts +7 -0
- package/src/core/outbox-entry-builders.ts +2 -0
- package/src/messaging/outbound/media.ts +2 -0
- package/src/messaging/outbound/reply-enqueue-media.ts +3 -0
- package/src/messaging/outbound/reply-enqueue.ts +4 -0
- package/src/messaging/outbound/send-params.ts +4 -0
- package/src/messaging/outbound/send.ts +3 -0
- package/src/plugin/ack-outbox-runtime-group.ts +2 -0
- package/src/plugin/channel-plugin-surface-group.ts +1 -0
- package/src/plugin/channel-runtime-builders-delivery.ts +1 -0
- package/src/plugin/media-orchestrators-runtime-group.ts +2 -0
- package/src/plugin/outbox-drain-failure.ts +36 -0
- package/src/plugin/outbox-drain-runtime.ts +14 -1
- package/src/plugin/target-runtime.ts +8 -0
package/package.json
CHANGED
package/scripts/selfcheck.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
1
2
|
import fs from 'node:fs';
|
|
2
3
|
import { createRequire } from 'node:module';
|
|
3
4
|
import path from 'node:path';
|
|
@@ -182,6 +183,19 @@ const readPackageVersion = () => {
|
|
|
182
183
|
return typeof pkg?.version === 'string' ? pkg.version.trim() : '';
|
|
183
184
|
};
|
|
184
185
|
|
|
186
|
+
const readNpmLatestVersion = (packageName) => {
|
|
187
|
+
try {
|
|
188
|
+
const raw = execFileSync('npm', ['view', packageName, 'version'], {
|
|
189
|
+
encoding: 'utf8',
|
|
190
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
191
|
+
timeout: 10000,
|
|
192
|
+
}).trim();
|
|
193
|
+
return raw || null;
|
|
194
|
+
} catch {
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
|
|
185
199
|
const requiredOpenClawSdkSubpaths = [
|
|
186
200
|
'openclaw/plugin-sdk',
|
|
187
201
|
'openclaw/plugin-sdk/boolean-param',
|
|
@@ -213,7 +227,7 @@ const resolveOpenClawSdkSubpaths = () => {
|
|
|
213
227
|
});
|
|
214
228
|
};
|
|
215
229
|
|
|
216
|
-
const validateVersionPolicy = (version) => {
|
|
230
|
+
const validateVersionPolicy = (version, latestVersion) => {
|
|
217
231
|
const match = version.match(/^(\d+)\.(\d+)\.(\d+)$/);
|
|
218
232
|
if (!match) {
|
|
219
233
|
return {
|
|
@@ -232,12 +246,101 @@ const validateVersionPolicy = (version) => {
|
|
|
232
246
|
};
|
|
233
247
|
}
|
|
234
248
|
|
|
249
|
+
// Prevent jumping minor when current minor still has unused patch slots
|
|
250
|
+
if (latestVersion) {
|
|
251
|
+
const lm = latestVersion.match(/^(\d+)\.(\d+)\.(\d+)$/);
|
|
252
|
+
if (lm) {
|
|
253
|
+
const vMajor = Number.parseInt(match[1], 10);
|
|
254
|
+
const vMinor = Number.parseInt(match[2], 10);
|
|
255
|
+
const lMajor = Number.parseInt(lm[1], 10);
|
|
256
|
+
const lMinor = Number.parseInt(lm[2], 10);
|
|
257
|
+
const lPatch = Number.parseInt(lm[3], 10);
|
|
258
|
+
|
|
259
|
+
// Version unchanged
|
|
260
|
+
if (vMajor === lMajor && vMinor === lMinor && patch === lPatch) {
|
|
261
|
+
return { ok: false, reason: `version unchanged: ${version}`, version };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Downgrade: any component decreased
|
|
265
|
+
if (
|
|
266
|
+
vMajor < lMajor ||
|
|
267
|
+
(vMajor === lMajor && vMinor < lMinor) ||
|
|
268
|
+
(vMajor === lMajor && vMinor === lMinor && patch < lPatch)
|
|
269
|
+
) {
|
|
270
|
+
return { ok: false, reason: `downgrade from ${latestVersion} to ${version}`, version };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Patch bump: same major and minor, patch must increment by exactly +1
|
|
274
|
+
if (vMajor === lMajor && vMinor === lMinor) {
|
|
275
|
+
if (patch === lPatch + 1) return { ok: true, version };
|
|
276
|
+
return {
|
|
277
|
+
ok: false,
|
|
278
|
+
reason: `patch jump from ${latestVersion} to ${version} (delta=${patch - lPatch}); expected ${lMajor}.${lMinor}.${lPatch + 1}`,
|
|
279
|
+
version,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Minor bump: same major, minor + 1
|
|
284
|
+
if (vMajor === lMajor && vMinor === lMinor + 1) {
|
|
285
|
+
if (lPatch === 9 && patch === 0) return { ok: true, version };
|
|
286
|
+
if (lPatch !== 9)
|
|
287
|
+
return {
|
|
288
|
+
ok: false,
|
|
289
|
+
reason: `minor bumped from ${latestVersion} to ${version} but ${9 - lPatch} patch slots remain in ${lMajor}.${lMinor}; prefer ${lMajor}.${lMinor}.${lPatch + 1}`,
|
|
290
|
+
version,
|
|
291
|
+
};
|
|
292
|
+
return {
|
|
293
|
+
ok: false,
|
|
294
|
+
reason: `minor bump must reset patch to 0; got ${vMajor}.${vMinor}.${patch}`,
|
|
295
|
+
version,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Minor jumped by more than 1
|
|
300
|
+
if (vMajor === lMajor && vMinor > lMinor + 1) {
|
|
301
|
+
return {
|
|
302
|
+
ok: false,
|
|
303
|
+
reason: `minor jumped from ${latestVersion} to ${version}; expected ${lMajor}.${lMinor + 1}.0`,
|
|
304
|
+
version,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Major bump: major + 1, requires previous minor fully exhausted
|
|
309
|
+
if (vMajor === lMajor + 1) {
|
|
310
|
+
if (lMinor === 9 && lPatch === 9 && vMinor === 0 && patch === 0)
|
|
311
|
+
return { ok: true, version };
|
|
312
|
+
if (lMinor !== 9 || lPatch !== 9)
|
|
313
|
+
return {
|
|
314
|
+
ok: false,
|
|
315
|
+
reason: `major bumped from ${latestVersion} to ${version} but previous major series not exhausted; expected ${lMajor + 1}.0.0 after ${lMajor}.9.9`,
|
|
316
|
+
version,
|
|
317
|
+
};
|
|
318
|
+
return {
|
|
319
|
+
ok: false,
|
|
320
|
+
reason: `major bump must reset to .0.0; got ${vMajor}.${vMinor}.${patch}`,
|
|
321
|
+
version,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Major jumped by more than 1
|
|
326
|
+
if (vMajor > lMajor + 1) {
|
|
327
|
+
return {
|
|
328
|
+
ok: false,
|
|
329
|
+
reason: `major jumped from ${latestVersion} to ${version}; expected ${lMajor + 1}.0.0`,
|
|
330
|
+
version,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
235
336
|
return { ok: true, version };
|
|
236
337
|
};
|
|
237
338
|
|
|
238
339
|
const missing = requiredFiles.filter((rel) => !fs.existsSync(path.join(root, rel)));
|
|
239
340
|
const version = readPackageVersion();
|
|
240
|
-
const
|
|
341
|
+
const packageName = '@xmoxmo/bncr';
|
|
342
|
+
const latestVersion = readNpmLatestVersion(packageName);
|
|
343
|
+
const versionPolicy = validateVersionPolicy(version, latestVersion);
|
|
241
344
|
const sdkSubpaths = resolveOpenClawSdkSubpaths();
|
|
242
345
|
const missingSdkSubpaths = sdkSubpaths.filter((entry) => !entry.ok);
|
|
243
346
|
const result = {
|
package/src/channel.ts
CHANGED
|
@@ -1164,6 +1164,7 @@ class BncrBridgeRuntime {
|
|
|
1164
1164
|
asVoice?: boolean;
|
|
1165
1165
|
audioAsVoice?: boolean;
|
|
1166
1166
|
type?: string;
|
|
1167
|
+
extra?: Record<string, unknown>;
|
|
1167
1168
|
kind?: 'tool' | 'block' | 'final';
|
|
1168
1169
|
replyToId?: string;
|
|
1169
1170
|
replyTargetPolicy?: OutboundReplyTargetPolicy;
|
|
@@ -1182,6 +1183,7 @@ class BncrBridgeRuntime {
|
|
|
1182
1183
|
asVoice: params.asVoice,
|
|
1183
1184
|
audioAsVoice: params.audioAsVoice,
|
|
1184
1185
|
type: params.type,
|
|
1186
|
+
extra: params.extra,
|
|
1185
1187
|
kind: params.kind,
|
|
1186
1188
|
replyToId: asString(params.replyToId || '').trim() || undefined,
|
|
1187
1189
|
replyTargetPolicy: params.replyTargetPolicy,
|
|
@@ -1245,6 +1247,7 @@ class BncrBridgeRuntime {
|
|
|
1245
1247
|
mimeType: params.media.mimeType,
|
|
1246
1248
|
}),
|
|
1247
1249
|
hintedType: wantsVoice ? 'voice' : asString(params.meta.type || '') || undefined,
|
|
1250
|
+
extra: params.meta.extra as Record<string, unknown> | undefined,
|
|
1248
1251
|
kind: messageKind,
|
|
1249
1252
|
replyToId: normalizeReplyToId(params.meta.replyToId) || undefined,
|
|
1250
1253
|
now: now(),
|
|
@@ -1724,6 +1727,10 @@ class BncrBridgeRuntime {
|
|
|
1724
1727
|
handleFileTransferPushFailure: (args) => this.handleFileTransferPushFailure(args),
|
|
1725
1728
|
handleTextPushFailure: (args) => this.handleTextPushFailure(args),
|
|
1726
1729
|
isPrePushGuardDeferral: (entry) => this.isPrePushGuardDeferral(entry),
|
|
1730
|
+
resolveAccountIdForSession: (sessionKey) => {
|
|
1731
|
+
const hit = this.sessionRoutes.get(sessionKey);
|
|
1732
|
+
return hit ? hit.accountId : BNCR_DEFAULT_ACCOUNT_ID;
|
|
1733
|
+
},
|
|
1727
1734
|
scheduleSave: () => this.scheduleSave(),
|
|
1728
1735
|
logInfo: (scope, message, options) => this.logInfo(scope, message, options),
|
|
1729
1736
|
logWarn: (scope, message, options) => this.logWarn(scope, message, options),
|
|
@@ -16,6 +16,7 @@ export function buildFileTransferOutboxEntry(args: {
|
|
|
16
16
|
asVoice?: boolean;
|
|
17
17
|
audioAsVoice?: boolean;
|
|
18
18
|
type?: string;
|
|
19
|
+
extra?: Record<string, unknown>;
|
|
19
20
|
kind?: 'tool' | 'block' | 'final';
|
|
20
21
|
replyToId?: string;
|
|
21
22
|
replyTargetPolicy?: OutboundReplyTargetPolicy;
|
|
@@ -38,6 +39,7 @@ export function buildFileTransferOutboxEntry(args: {
|
|
|
38
39
|
asVoice: args.asVoice === true,
|
|
39
40
|
audioAsVoice: args.audioAsVoice === true,
|
|
40
41
|
type: args.type,
|
|
42
|
+
...(args.extra ? { extra: { ...args.extra } } : {}),
|
|
41
43
|
finalEvent: args.pushEvent,
|
|
42
44
|
replyToId:
|
|
43
45
|
normalizeOutboundReplyToId({
|
|
@@ -68,6 +68,7 @@ export function buildBncrMediaOutboundFrame(params: {
|
|
|
68
68
|
mediaMsg: string;
|
|
69
69
|
fileName: string;
|
|
70
70
|
hintedType?: string;
|
|
71
|
+
extra?: Record<string, unknown>;
|
|
71
72
|
kind?: 'tool' | 'block' | 'final';
|
|
72
73
|
replyToId?: string;
|
|
73
74
|
now: number;
|
|
@@ -96,6 +97,7 @@ export function buildBncrMediaOutboundFrame(params: {
|
|
|
96
97
|
base64: params.media.mediaBase64 || '',
|
|
97
98
|
fileName: params.fileName,
|
|
98
99
|
transferMode: params.media.mode,
|
|
100
|
+
...(params.extra ? { extra: { ...params.extra } } : {}),
|
|
99
101
|
},
|
|
100
102
|
ts: params.now,
|
|
101
103
|
};
|
|
@@ -62,6 +62,7 @@ export function enqueueReplyMediaFileTransferEntry(
|
|
|
62
62
|
asVoice: boolean;
|
|
63
63
|
audioAsVoice: boolean;
|
|
64
64
|
type?: string;
|
|
65
|
+
extra?: Record<string, unknown>;
|
|
65
66
|
kind?: 'tool' | 'block' | 'final';
|
|
66
67
|
replyToId?: string;
|
|
67
68
|
replyTargetPolicy?: OutboundReplyTargetPolicy;
|
|
@@ -86,6 +87,7 @@ export function enqueueReplyMediaFileTransferEntry(
|
|
|
86
87
|
asVoice: params.asVoice,
|
|
87
88
|
audioAsVoice: params.audioAsVoice,
|
|
88
89
|
type: params.type,
|
|
90
|
+
extra: params.extra,
|
|
89
91
|
kind: params.kind,
|
|
90
92
|
replyToId: params.replyToId || undefined,
|
|
91
93
|
replyTargetPolicy: params.replyTargetPolicy,
|
|
@@ -132,6 +134,7 @@ export function enqueueSingleReplyMediaEntry(
|
|
|
132
134
|
asVoice: params.params.payload.asVoice,
|
|
133
135
|
audioAsVoice: params.params.payload.audioAsVoice,
|
|
134
136
|
type: params.params.payload.type,
|
|
137
|
+
extra: params.params.payload.extra,
|
|
135
138
|
kind: params.params.payload.kind,
|
|
136
139
|
replyToId: params.params.payload.replyToId,
|
|
137
140
|
replyTargetPolicy: params.params.payload.replyTargetPolicy,
|
|
@@ -19,6 +19,7 @@ export type ReplyPayloadInput = {
|
|
|
19
19
|
asVoice?: boolean;
|
|
20
20
|
audioAsVoice?: boolean;
|
|
21
21
|
type?: string;
|
|
22
|
+
extra?: Record<string, unknown>;
|
|
22
23
|
kind?: 'tool' | 'block' | 'final';
|
|
23
24
|
replyToId?: string;
|
|
24
25
|
};
|
|
@@ -31,6 +32,7 @@ export type NormalizedReplyPayload = {
|
|
|
31
32
|
asVoice: boolean;
|
|
32
33
|
audioAsVoice: boolean;
|
|
33
34
|
type?: string;
|
|
35
|
+
extra?: Record<string, unknown>;
|
|
34
36
|
kind?: 'tool' | 'block' | 'final';
|
|
35
37
|
replyToId: string;
|
|
36
38
|
replyTargetPolicy: OutboundReplyTargetPolicy;
|
|
@@ -64,6 +66,7 @@ export type ReplyMediaFileTransferParams = {
|
|
|
64
66
|
asVoice: boolean;
|
|
65
67
|
audioAsVoice: boolean;
|
|
66
68
|
type?: string;
|
|
69
|
+
extra?: Record<string, unknown>;
|
|
67
70
|
kind?: 'tool' | 'block' | 'final';
|
|
68
71
|
replyToId: string;
|
|
69
72
|
replyTargetPolicy: OutboundReplyTargetPolicy;
|
|
@@ -268,6 +271,7 @@ export function normalizeReplyPayload(
|
|
|
268
271
|
asVoice: payload?.asVoice === true,
|
|
269
272
|
audioAsVoice: payload?.audioAsVoice === true,
|
|
270
273
|
...(type ? { type } : {}),
|
|
274
|
+
...(payload?.extra ? { extra: { ...payload.extra } } : {}),
|
|
271
275
|
kind: payload?.kind,
|
|
272
276
|
replyTargetPolicy: options?.replyTargetPolicy ?? 'agent-default',
|
|
273
277
|
replyToId: normalizeOutboundReplyToId({
|
|
@@ -11,6 +11,7 @@ export type NormalizedBncrSendParams = {
|
|
|
11
11
|
asVoice: boolean;
|
|
12
12
|
audioAsVoice: boolean;
|
|
13
13
|
type?: string;
|
|
14
|
+
extra?: Record<string, unknown>;
|
|
14
15
|
};
|
|
15
16
|
|
|
16
17
|
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
@@ -51,6 +52,8 @@ export function normalizeBncrSendParams(input: {
|
|
|
51
52
|
const asVoice = readOpenClawBooleanParam(paramsObj, 'asVoice') ?? false;
|
|
52
53
|
const audioAsVoice = readOpenClawBooleanParam(paramsObj, 'audioAsVoice') ?? false;
|
|
53
54
|
const type = readOpenClawStringParam(paramsObj, 'type') || undefined;
|
|
55
|
+
const rawExtra = paramsObj.extra;
|
|
56
|
+
const extra = isPlainObject(rawExtra) ? { ...rawExtra } : undefined;
|
|
54
57
|
|
|
55
58
|
const hasMedia = Boolean(mediaUrl || dedupedMediaUrls?.length);
|
|
56
59
|
|
|
@@ -73,5 +76,6 @@ export function normalizeBncrSendParams(input: {
|
|
|
73
76
|
asVoice,
|
|
74
77
|
audioAsVoice,
|
|
75
78
|
...(type ? { type } : {}),
|
|
79
|
+
...(extra ? { extra } : {}),
|
|
76
80
|
};
|
|
77
81
|
}
|
|
@@ -64,6 +64,7 @@ export async function sendBncrMedia(params: {
|
|
|
64
64
|
asVoice?: boolean;
|
|
65
65
|
audioAsVoice?: boolean;
|
|
66
66
|
type?: string;
|
|
67
|
+
extra?: Record<string, unknown>;
|
|
67
68
|
kind?: string;
|
|
68
69
|
replyToId?: string;
|
|
69
70
|
mediaLocalRoots?: readonly string[];
|
|
@@ -83,6 +84,7 @@ export async function sendBncrMedia(params: {
|
|
|
83
84
|
asVoice?: boolean;
|
|
84
85
|
audioAsVoice?: boolean;
|
|
85
86
|
type?: string;
|
|
87
|
+
extra?: Record<string, unknown>;
|
|
86
88
|
kind?: 'tool' | 'block' | 'final';
|
|
87
89
|
replyToId?: string;
|
|
88
90
|
};
|
|
@@ -104,6 +106,7 @@ export async function sendBncrMedia(params: {
|
|
|
104
106
|
asVoice: params.asVoice === true ? true : undefined,
|
|
105
107
|
audioAsVoice: params.audioAsVoice === true ? true : undefined,
|
|
106
108
|
type: params.type,
|
|
109
|
+
extra: params.extra,
|
|
107
110
|
kind: normalizeReplyKind(params.kind),
|
|
108
111
|
replyToId: params.replyToId,
|
|
109
112
|
},
|
|
@@ -19,6 +19,7 @@ export function createBncrAckOutboxRuntimeGroup(runtime: {
|
|
|
19
19
|
isPlainObject: (value: unknown) => value is Record<string, unknown>;
|
|
20
20
|
clampFiniteNumber: (value: unknown, fallback: number, min?: number, max?: number) => number;
|
|
21
21
|
normalizeAccountId: (accountId: string) => string;
|
|
22
|
+
resolveAccountIdForSession: (sessionKey: string) => string | null;
|
|
22
23
|
formatDisplayScope: (route: OutboxEntry['route']) => string;
|
|
23
24
|
isFileTransferEntry: (entry: OutboxEntry) => boolean;
|
|
24
25
|
recommendedAckTimeoutMaxMs: number;
|
|
@@ -235,6 +236,7 @@ export function createBncrAckOutboxRuntimeGroup(runtime: {
|
|
|
235
236
|
handleFileTransferPushFailure: runtime.handleFileTransferPushFailure,
|
|
236
237
|
handleTextPushFailure: runtime.handleTextPushFailure,
|
|
237
238
|
isPrePushGuardDeferral: runtime.isPrePushGuardDeferral,
|
|
239
|
+
resolveAccountIdForSession: runtime.resolveAccountIdForSession,
|
|
238
240
|
moveToDeadLetter: runtime.moveToDeadLetter,
|
|
239
241
|
scheduleSave: runtime.scheduleSave,
|
|
240
242
|
logInfo: runtime.logInfo,
|
|
@@ -152,6 +152,7 @@ export function createBncrChannelPluginSurfaceGroup(runtime: {
|
|
|
152
152
|
asVoice: normalized.asVoice,
|
|
153
153
|
audioAsVoice: normalized.audioAsVoice,
|
|
154
154
|
type: normalized.type,
|
|
155
|
+
extra: normalized.extra,
|
|
155
156
|
mediaLocalRoots,
|
|
156
157
|
resolveVerifiedTarget: (to, accountId) =>
|
|
157
158
|
toolActionBridge.resolveVerifiedTarget(to, accountId),
|
|
@@ -337,6 +337,7 @@ export function buildBncrMediaOrchestratorsRuntime(deps: {
|
|
|
337
337
|
asVoice: boolean;
|
|
338
338
|
audioAsVoice: boolean;
|
|
339
339
|
type?: string;
|
|
340
|
+
extra?: Record<string, unknown>;
|
|
340
341
|
kind?: 'tool' | 'block' | 'final';
|
|
341
342
|
replyToId?: string;
|
|
342
343
|
replyTargetPolicy?: OutboundReplyTargetPolicy;
|
|
@@ -49,6 +49,7 @@ function buildReplyMediaEntryHelpers(runtime: {
|
|
|
49
49
|
asVoice: boolean;
|
|
50
50
|
audioAsVoice: boolean;
|
|
51
51
|
type?: string;
|
|
52
|
+
extra?: Record<string, unknown>;
|
|
52
53
|
kind?: 'tool' | 'block' | 'final';
|
|
53
54
|
replyToId?: string;
|
|
54
55
|
replyTargetPolicy?: OutboundReplyTargetPolicy;
|
|
@@ -203,6 +204,7 @@ export function createBncrMediaOrchestratorsRuntimeGroup(runtime: {
|
|
|
203
204
|
asVoice: boolean;
|
|
204
205
|
audioAsVoice: boolean;
|
|
205
206
|
type?: string;
|
|
207
|
+
extra?: Record<string, unknown>;
|
|
206
208
|
kind?: 'tool' | 'block' | 'final';
|
|
207
209
|
replyToId?: string;
|
|
208
210
|
replyTargetPolicy?: OutboundReplyTargetPolicy;
|
|
@@ -20,6 +20,9 @@ type BncrOutboxDrainScheduleRuntime = {
|
|
|
20
20
|
type BncrOutboxDrainFailureRuntime = {
|
|
21
21
|
backoffMs: (retryCount: number) => number;
|
|
22
22
|
outbox: Map<string, OutboxEntry>;
|
|
23
|
+
resolveAccountIdForSession: (sessionKey: string) => string | null;
|
|
24
|
+
logInfo: (scope: string, message: string, options?: { debugOnly?: boolean }) => void;
|
|
25
|
+
logWarn: (scope: string, message: string, options?: { debugOnly?: boolean }) => void;
|
|
23
26
|
isPrePushGuardDeferral: (entry: OutboxEntry) => boolean;
|
|
24
27
|
moveToDeadLetter: (entry: OutboxEntry, reason: string) => void;
|
|
25
28
|
scheduleSave: () => void;
|
|
@@ -39,6 +42,39 @@ export function createBncrOutboxDrainFailure(runtime: BncrOutboxDrainFailureRunt
|
|
|
39
42
|
const { accountId, entry, attemptedAt, updateMinOutboxDelay } = args;
|
|
40
43
|
let { localNextDelay } = args;
|
|
41
44
|
|
|
45
|
+
// If the entry keeps hitting pre-push guard (no active connection), the
|
|
46
|
+
// accountId on the entry might be wrong (e.g. constructed from route data).
|
|
47
|
+
// Try to correct it from recent inbound session context before giving up.
|
|
48
|
+
if (runtime.isPrePushGuardDeferral(entry) && entry.retryCount > 0) {
|
|
49
|
+
const corrected = runtime.resolveAccountIdForSession(entry.sessionKey);
|
|
50
|
+
if (corrected && corrected !== entry.accountId) {
|
|
51
|
+
const oldAccountId = entry.accountId;
|
|
52
|
+
runtime.logWarn(
|
|
53
|
+
'outbound',
|
|
54
|
+
`account corrected sessionKey=${entry.sessionKey} ${oldAccountId}→${corrected}`,
|
|
55
|
+
);
|
|
56
|
+
runtime.logInfo(
|
|
57
|
+
'outbound',
|
|
58
|
+
JSON.stringify({
|
|
59
|
+
event: 'account-corrected',
|
|
60
|
+
messageId: entry.messageId,
|
|
61
|
+
sessionKey: entry.sessionKey,
|
|
62
|
+
oldAccountId,
|
|
63
|
+
corrected,
|
|
64
|
+
retryCount: entry.retryCount,
|
|
65
|
+
lastError: entry.lastError,
|
|
66
|
+
}),
|
|
67
|
+
{ debugOnly: true },
|
|
68
|
+
);
|
|
69
|
+
entry.accountId = corrected;
|
|
70
|
+
entry.retryCount = 0;
|
|
71
|
+
entry.lastError = undefined;
|
|
72
|
+
runtime.outbox.set(entry.messageId, entry);
|
|
73
|
+
runtime.scheduleSave();
|
|
74
|
+
return { action: 'continue', localNextDelay };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
42
78
|
if (runtime.isPrePushGuardDeferral(entry)) {
|
|
43
79
|
const wait = runtime.prePushGuardRetryDelayMs;
|
|
44
80
|
localNextDelay = runtime.outboxDrainSchedule.scheduleAccountWait({
|
|
@@ -58,6 +58,7 @@ type BncrOutboxDrainRuntime = {
|
|
|
58
58
|
backoffMs: (retryCount: number) => number;
|
|
59
59
|
isPlainObject: (value: unknown) => value is Record<string, unknown>;
|
|
60
60
|
normalizeAccountId: (accountId: string) => string;
|
|
61
|
+
resolveAccountIdForSession: (sessionKey: string) => string | null;
|
|
61
62
|
stopped: () => boolean;
|
|
62
63
|
outbox: Map<string, OutboxEntry>;
|
|
63
64
|
deadLetter: () => OutboxEntry[];
|
|
@@ -132,7 +133,19 @@ type BncrOutboxDrainRuntime = {
|
|
|
132
133
|
|
|
133
134
|
export function createBncrOutboxDrainRuntime(runtime: BncrOutboxDrainRuntime) {
|
|
134
135
|
const handlePushedDrainEntry = createBncrOutboxDrainPostPush(runtime);
|
|
135
|
-
const handleFailedDrainEntry = createBncrOutboxDrainFailure(
|
|
136
|
+
const handleFailedDrainEntry = createBncrOutboxDrainFailure({
|
|
137
|
+
backoffMs: runtime.backoffMs,
|
|
138
|
+
outbox: runtime.outbox,
|
|
139
|
+
resolveAccountIdForSession: runtime.resolveAccountIdForSession,
|
|
140
|
+
logInfo: runtime.logInfo,
|
|
141
|
+
logWarn: runtime.logWarn,
|
|
142
|
+
isPrePushGuardDeferral: runtime.isPrePushGuardDeferral,
|
|
143
|
+
moveToDeadLetter: runtime.moveToDeadLetter,
|
|
144
|
+
scheduleSave: runtime.scheduleSave,
|
|
145
|
+
outboxDrainSchedule: runtime.outboxDrainSchedule,
|
|
146
|
+
maxRetry: runtime.maxRetry,
|
|
147
|
+
prePushGuardRetryDelayMs: runtime.prePushGuardRetryDelayMs,
|
|
148
|
+
});
|
|
136
149
|
|
|
137
150
|
return createBncrOutboxDrainLoop(runtime, {
|
|
138
151
|
handlePushedDrainEntry,
|
|
@@ -133,9 +133,17 @@ export function createBncrTargetRuntime(runtime: {
|
|
|
133
133
|
return verified;
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
+
function resolveSessionAccountId(sessionKey: string): string | null {
|
|
137
|
+
const key = asString(sessionKey).trim();
|
|
138
|
+
if (!key) return null;
|
|
139
|
+
const hit = runtime.sessionRoutes.get(key);
|
|
140
|
+
return hit ? hit.accountId : null;
|
|
141
|
+
}
|
|
142
|
+
|
|
136
143
|
return {
|
|
137
144
|
rememberSessionRoute,
|
|
138
145
|
resolveRouteBySession,
|
|
139
146
|
resolveVerifiedTarget,
|
|
147
|
+
resolveSessionAccountId,
|
|
140
148
|
};
|
|
141
149
|
}
|