@xmoxmo/bncr 0.2.4 → 0.2.6
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/README.md +68 -5
- package/package.json +1 -1
- package/src/channel.ts +2985 -1706
- package/src/core/connection-capability.ts +70 -0
- package/src/core/connection-reachability.ts +168 -0
- package/src/core/diagnostics.ts +54 -0
- package/src/core/downlink-health.ts +61 -0
- package/src/core/extended-diagnostics.ts +65 -0
- package/src/core/lease-state.ts +94 -0
- package/src/core/outbox-enqueue.ts +22 -0
- package/src/core/outbox-entry-builders.ts +92 -0
- package/src/core/outbox-file-transfer-bookkeeping.ts +31 -0
- package/src/core/outbox-file-transfer-failure.ts +25 -0
- package/src/core/outbox-file-transfer-guards.ts +66 -0
- package/src/core/outbox-file-transfer-prep.ts +31 -0
- package/src/core/outbox-file-transfer-success.ts +34 -0
- package/src/core/outbox-push-args.ts +67 -0
- package/src/core/outbox-queue.ts +69 -0
- package/src/core/outbox-summary.ts +14 -0
- package/src/core/outbox-text-push-failure.ts +10 -0
- package/src/core/outbox-text-push-guards.ts +51 -0
- package/src/core/outbox-text-push-prep.ts +36 -0
- package/src/core/outbox-text-push-success.ts +62 -0
- package/src/core/policy.ts +9 -0
- package/src/core/register-trace.ts +115 -0
- package/src/core/status.ts +57 -0
- package/src/core/types.ts +1 -0
- package/src/messaging/inbound/commands.ts +318 -75
- package/src/messaging/inbound/dispatch.ts +435 -139
- package/src/messaging/inbound/parse.ts +8 -0
- package/src/messaging/inbound/session-label.ts +115 -0
- package/src/messaging/outbound/diagnostics.ts +262 -0
- package/src/messaging/outbound/media-dedupe.ts +51 -0
- package/src/messaging/outbound/media.ts +3 -1
- package/src/messaging/outbound/queue-selectors.ts +191 -0
- package/src/messaging/outbound/reasons.ts +52 -0
- package/src/messaging/outbound/reply-enqueue.ts +329 -0
- package/src/messaging/outbound/reply-target-policy.ts +13 -0
- package/src/messaging/outbound/retry-policy.ts +142 -0
- package/src/messaging/outbound/send.ts +6 -0
- package/src/messaging/outbound/session-route.ts +34 -5
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import type { BncrRoute, OutboxEntry } from '../../core/types.ts';
|
|
2
|
+
import { buildReplyMediaFallbackDebugInfo } from './diagnostics.ts';
|
|
3
|
+
import { normalizeOutboundReplyToId } from './reply-target-policy.ts';
|
|
4
|
+
|
|
5
|
+
export type ReplyPayloadInput = {
|
|
6
|
+
text?: string;
|
|
7
|
+
mediaUrl?: string;
|
|
8
|
+
mediaUrls?: string[];
|
|
9
|
+
asVoice?: boolean;
|
|
10
|
+
audioAsVoice?: boolean;
|
|
11
|
+
kind?: 'tool' | 'block' | 'final';
|
|
12
|
+
replyToId?: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type NormalizedReplyPayload = {
|
|
16
|
+
text: string;
|
|
17
|
+
mediaUrl: string;
|
|
18
|
+
mediaUrls?: string[];
|
|
19
|
+
mediaList: string[];
|
|
20
|
+
asVoice: boolean;
|
|
21
|
+
audioAsVoice: boolean;
|
|
22
|
+
kind?: 'tool' | 'block' | 'final';
|
|
23
|
+
replyToId: string;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type ReplyMediaEntriesParams = {
|
|
27
|
+
accountId: string;
|
|
28
|
+
sessionKey: string;
|
|
29
|
+
route: BncrRoute;
|
|
30
|
+
payload: NormalizedReplyPayload;
|
|
31
|
+
mediaLocalRoots?: readonly string[];
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type EnqueueNormalizedReplyPayloadParams = {
|
|
35
|
+
accountId: string;
|
|
36
|
+
sessionKey: string;
|
|
37
|
+
route: BncrRoute;
|
|
38
|
+
payload: NormalizedReplyPayload;
|
|
39
|
+
mediaLocalRoots?: readonly string[];
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export type ReplyMediaFileTransferParams = {
|
|
43
|
+
accountId: string;
|
|
44
|
+
sessionKey: string;
|
|
45
|
+
route: BncrRoute;
|
|
46
|
+
mediaUrl: string;
|
|
47
|
+
mediaLocalRoots?: readonly string[];
|
|
48
|
+
text: string;
|
|
49
|
+
normalizedText: string;
|
|
50
|
+
asVoice: boolean;
|
|
51
|
+
audioAsVoice: boolean;
|
|
52
|
+
kind?: 'tool' | 'block' | 'final';
|
|
53
|
+
replyToId: string;
|
|
54
|
+
createdAt: number;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export type EnqueueSingleReplyMediaEntryParams = {
|
|
58
|
+
params: ReplyMediaEntriesParams;
|
|
59
|
+
mediaUrl: string;
|
|
60
|
+
normalizedText: string;
|
|
61
|
+
text: string;
|
|
62
|
+
fallback: { text: string; reason: string } | null;
|
|
63
|
+
currentTime: number;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export type ReplyMediaFallbackTextEntryParams = {
|
|
67
|
+
accountId: string;
|
|
68
|
+
sessionKey: string;
|
|
69
|
+
route: BncrRoute;
|
|
70
|
+
mediaUrl: string;
|
|
71
|
+
kind?: 'tool' | 'block' | 'final';
|
|
72
|
+
replyToId: string;
|
|
73
|
+
fallback: { text: string; reason: string };
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export function hasReplyMediaEntries(payload: NormalizedReplyPayload) {
|
|
77
|
+
return payload.mediaList.length > 0;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function buildReplyTextOutboxEntry(
|
|
81
|
+
params: {
|
|
82
|
+
accountId: string;
|
|
83
|
+
sessionKey: string;
|
|
84
|
+
route: BncrRoute;
|
|
85
|
+
text: string;
|
|
86
|
+
kind?: 'tool' | 'block' | 'final';
|
|
87
|
+
replyToId: string;
|
|
88
|
+
},
|
|
89
|
+
helpers: {
|
|
90
|
+
buildTextOutboxEntry: (args: {
|
|
91
|
+
accountId: string;
|
|
92
|
+
sessionKey: string;
|
|
93
|
+
route: BncrRoute;
|
|
94
|
+
text: string;
|
|
95
|
+
kind?: 'tool' | 'block' | 'final';
|
|
96
|
+
replyToId?: string;
|
|
97
|
+
}) => OutboxEntry;
|
|
98
|
+
},
|
|
99
|
+
): OutboxEntry {
|
|
100
|
+
return helpers.buildTextOutboxEntry({
|
|
101
|
+
accountId: params.accountId,
|
|
102
|
+
sessionKey: params.sessionKey,
|
|
103
|
+
route: params.route,
|
|
104
|
+
text: params.text,
|
|
105
|
+
kind: params.kind,
|
|
106
|
+
replyToId: params.replyToId || undefined,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function enqueueReplyTextEntry(
|
|
111
|
+
params: {
|
|
112
|
+
accountId: string;
|
|
113
|
+
sessionKey: string;
|
|
114
|
+
route: BncrRoute;
|
|
115
|
+
payload: NormalizedReplyPayload;
|
|
116
|
+
},
|
|
117
|
+
helpers: {
|
|
118
|
+
enqueueOutbound: (entry: OutboxEntry) => void;
|
|
119
|
+
buildTextOutboxEntry: (args: {
|
|
120
|
+
accountId: string;
|
|
121
|
+
sessionKey: string;
|
|
122
|
+
route: BncrRoute;
|
|
123
|
+
text: string;
|
|
124
|
+
kind?: 'tool' | 'block' | 'final';
|
|
125
|
+
replyToId?: string;
|
|
126
|
+
}) => OutboxEntry;
|
|
127
|
+
},
|
|
128
|
+
): void {
|
|
129
|
+
if (!params.payload.text) return;
|
|
130
|
+
|
|
131
|
+
helpers.enqueueOutbound(
|
|
132
|
+
buildReplyTextOutboxEntry(
|
|
133
|
+
{
|
|
134
|
+
accountId: params.accountId,
|
|
135
|
+
sessionKey: params.sessionKey,
|
|
136
|
+
route: params.route,
|
|
137
|
+
text: params.payload.text,
|
|
138
|
+
kind: params.payload.kind,
|
|
139
|
+
replyToId: params.payload.replyToId,
|
|
140
|
+
},
|
|
141
|
+
{ buildTextOutboxEntry: helpers.buildTextOutboxEntry },
|
|
142
|
+
),
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function enqueueReplyMediaFallbackTextEntry(
|
|
147
|
+
params: ReplyMediaFallbackTextEntryParams,
|
|
148
|
+
helpers: {
|
|
149
|
+
logInfo: (scope: string | undefined, message: string, options?: { debugOnly?: boolean }) => void;
|
|
150
|
+
enqueueOutbound: (entry: OutboxEntry) => void;
|
|
151
|
+
buildTextOutboxEntry: (args: {
|
|
152
|
+
accountId: string;
|
|
153
|
+
sessionKey: string;
|
|
154
|
+
route: BncrRoute;
|
|
155
|
+
text: string;
|
|
156
|
+
kind?: 'tool' | 'block' | 'final';
|
|
157
|
+
replyToId?: string;
|
|
158
|
+
}) => OutboxEntry;
|
|
159
|
+
},
|
|
160
|
+
): void {
|
|
161
|
+
helpers.logInfo(
|
|
162
|
+
'outbound',
|
|
163
|
+
`media-dedupe-hit ${JSON.stringify(buildReplyMediaFallbackDebugInfo(params))}`,
|
|
164
|
+
{ debugOnly: true },
|
|
165
|
+
);
|
|
166
|
+
helpers.enqueueOutbound(
|
|
167
|
+
buildReplyTextOutboxEntry(
|
|
168
|
+
{
|
|
169
|
+
accountId: params.accountId,
|
|
170
|
+
sessionKey: params.sessionKey,
|
|
171
|
+
route: params.route,
|
|
172
|
+
text: params.fallback.text,
|
|
173
|
+
kind: params.kind,
|
|
174
|
+
replyToId: params.replyToId,
|
|
175
|
+
},
|
|
176
|
+
{ buildTextOutboxEntry: helpers.buildTextOutboxEntry },
|
|
177
|
+
),
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function enqueueReplyMediaFileTransferEntry(
|
|
182
|
+
params: ReplyMediaFileTransferParams,
|
|
183
|
+
helpers: {
|
|
184
|
+
enqueueOutbound: (entry: OutboxEntry) => void;
|
|
185
|
+
buildFileTransferOutboxEntry: (args: {
|
|
186
|
+
accountId: string;
|
|
187
|
+
sessionKey: string;
|
|
188
|
+
route: BncrRoute;
|
|
189
|
+
mediaUrl: string;
|
|
190
|
+
mediaLocalRoots?: readonly string[];
|
|
191
|
+
text: string;
|
|
192
|
+
asVoice: boolean;
|
|
193
|
+
audioAsVoice: boolean;
|
|
194
|
+
kind?: 'tool' | 'block' | 'final';
|
|
195
|
+
replyToId?: string;
|
|
196
|
+
}) => OutboxEntry;
|
|
197
|
+
rememberRecentMediaSend: (args: {
|
|
198
|
+
sessionKey: string;
|
|
199
|
+
mediaUrl: string;
|
|
200
|
+
text: string;
|
|
201
|
+
replyToId: string;
|
|
202
|
+
createdAt: number;
|
|
203
|
+
}) => void;
|
|
204
|
+
},
|
|
205
|
+
): void {
|
|
206
|
+
helpers.enqueueOutbound(
|
|
207
|
+
helpers.buildFileTransferOutboxEntry({
|
|
208
|
+
accountId: params.accountId,
|
|
209
|
+
sessionKey: params.sessionKey,
|
|
210
|
+
route: params.route,
|
|
211
|
+
mediaUrl: params.mediaUrl,
|
|
212
|
+
mediaLocalRoots: params.mediaLocalRoots,
|
|
213
|
+
text: params.text,
|
|
214
|
+
asVoice: params.asVoice,
|
|
215
|
+
audioAsVoice: params.audioAsVoice,
|
|
216
|
+
kind: params.kind,
|
|
217
|
+
replyToId: params.replyToId || undefined,
|
|
218
|
+
}),
|
|
219
|
+
);
|
|
220
|
+
helpers.rememberRecentMediaSend({
|
|
221
|
+
sessionKey: params.sessionKey,
|
|
222
|
+
mediaUrl: params.mediaUrl,
|
|
223
|
+
text: params.normalizedText,
|
|
224
|
+
replyToId: params.replyToId,
|
|
225
|
+
createdAt: params.createdAt,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function enqueueSingleReplyMediaEntry(
|
|
230
|
+
params: EnqueueSingleReplyMediaEntryParams,
|
|
231
|
+
helpers: {
|
|
232
|
+
enqueueReplyMediaFallbackTextEntry: (params: ReplyMediaFallbackTextEntryParams) => void;
|
|
233
|
+
enqueueReplyMediaFileTransferEntry: (params: ReplyMediaFileTransferParams) => void;
|
|
234
|
+
},
|
|
235
|
+
): void {
|
|
236
|
+
if (params.fallback !== null) {
|
|
237
|
+
helpers.enqueueReplyMediaFallbackTextEntry({
|
|
238
|
+
accountId: params.params.accountId,
|
|
239
|
+
sessionKey: params.params.sessionKey,
|
|
240
|
+
route: params.params.route,
|
|
241
|
+
mediaUrl: params.mediaUrl,
|
|
242
|
+
kind: params.params.payload.kind,
|
|
243
|
+
replyToId: params.params.payload.replyToId,
|
|
244
|
+
fallback: params.fallback,
|
|
245
|
+
});
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
helpers.enqueueReplyMediaFileTransferEntry({
|
|
250
|
+
accountId: params.params.accountId,
|
|
251
|
+
sessionKey: params.params.sessionKey,
|
|
252
|
+
route: params.params.route,
|
|
253
|
+
mediaUrl: params.mediaUrl,
|
|
254
|
+
mediaLocalRoots: params.params.mediaLocalRoots,
|
|
255
|
+
text: params.text,
|
|
256
|
+
normalizedText: params.normalizedText,
|
|
257
|
+
asVoice: params.params.payload.asVoice,
|
|
258
|
+
audioAsVoice: params.params.payload.audioAsVoice,
|
|
259
|
+
kind: params.params.payload.kind,
|
|
260
|
+
replyToId: params.params.payload.replyToId,
|
|
261
|
+
createdAt: params.currentTime,
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export function enqueueNormalizedReplyPayload(
|
|
266
|
+
params: EnqueueNormalizedReplyPayloadParams,
|
|
267
|
+
helpers: {
|
|
268
|
+
logEnqueueFromReply: (args: {
|
|
269
|
+
accountId: string;
|
|
270
|
+
sessionKey: string;
|
|
271
|
+
route: BncrRoute;
|
|
272
|
+
payload: NormalizedReplyPayload;
|
|
273
|
+
}) => void;
|
|
274
|
+
hasReplyMediaEntries: (payload: NormalizedReplyPayload) => boolean;
|
|
275
|
+
enqueueReplyMediaEntries: (params: ReplyMediaEntriesParams) => void;
|
|
276
|
+
enqueueReplyTextEntry: (params: {
|
|
277
|
+
accountId: string;
|
|
278
|
+
sessionKey: string;
|
|
279
|
+
route: BncrRoute;
|
|
280
|
+
payload: NormalizedReplyPayload;
|
|
281
|
+
}) => void;
|
|
282
|
+
},
|
|
283
|
+
): void {
|
|
284
|
+
helpers.logEnqueueFromReply({
|
|
285
|
+
accountId: params.accountId,
|
|
286
|
+
sessionKey: params.sessionKey,
|
|
287
|
+
route: params.route,
|
|
288
|
+
payload: params.payload,
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
if (helpers.hasReplyMediaEntries(params.payload)) {
|
|
292
|
+
helpers.enqueueReplyMediaEntries({
|
|
293
|
+
accountId: params.accountId,
|
|
294
|
+
sessionKey: params.sessionKey,
|
|
295
|
+
route: params.route,
|
|
296
|
+
payload: params.payload,
|
|
297
|
+
mediaLocalRoots: params.mediaLocalRoots,
|
|
298
|
+
});
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
helpers.enqueueReplyTextEntry({
|
|
303
|
+
accountId: params.accountId,
|
|
304
|
+
sessionKey: params.sessionKey,
|
|
305
|
+
route: params.route,
|
|
306
|
+
payload: params.payload,
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export function normalizeReplyPayload(
|
|
311
|
+
payload: ReplyPayloadInput,
|
|
312
|
+
helpers: { asString: (value: unknown, fallback?: string) => string },
|
|
313
|
+
): NormalizedReplyPayload {
|
|
314
|
+
const text = helpers.asString(payload?.text || '').trim();
|
|
315
|
+
const mediaUrl = helpers.asString(payload?.mediaUrl || '').trim();
|
|
316
|
+
const mediaUrls = Array.isArray(payload?.mediaUrls)
|
|
317
|
+
? payload.mediaUrls.map((v) => helpers.asString(v || '').trim()).filter(Boolean)
|
|
318
|
+
: undefined;
|
|
319
|
+
return {
|
|
320
|
+
text,
|
|
321
|
+
mediaUrl,
|
|
322
|
+
mediaUrls,
|
|
323
|
+
mediaList: mediaUrls?.length ? mediaUrls : mediaUrl ? [mediaUrl] : [],
|
|
324
|
+
asVoice: payload?.asVoice === true,
|
|
325
|
+
audioAsVoice: payload?.audioAsVoice === true,
|
|
326
|
+
kind: payload?.kind,
|
|
327
|
+
replyToId: normalizeOutboundReplyToId({ kind: payload?.kind, replyToId: payload?.replyToId }),
|
|
328
|
+
};
|
|
329
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { normalizeReplyToId } from './media-dedupe.ts';
|
|
2
|
+
|
|
3
|
+
export type OutboundReplyKind = 'tool' | 'block' | 'final';
|
|
4
|
+
|
|
5
|
+
const STRIP_TOOL_REPLY_TO_ID = true;
|
|
6
|
+
|
|
7
|
+
export function normalizeOutboundReplyToId(params: {
|
|
8
|
+
kind?: OutboundReplyKind;
|
|
9
|
+
replyToId?: string | null;
|
|
10
|
+
}) {
|
|
11
|
+
if (params.kind === 'tool' && STRIP_TOOL_REPLY_TO_ID) return '';
|
|
12
|
+
return normalizeReplyToId(params.replyToId);
|
|
13
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import type { BncrRoute } from '../../channel.ts';
|
|
2
|
+
|
|
3
|
+
function finiteNonNegativeIntegerOr(value: unknown, fallback: number): number {
|
|
4
|
+
const n = Number(value);
|
|
5
|
+
if (!Number.isFinite(n) || n < 0) return fallback;
|
|
6
|
+
return Math.floor(n);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type RetryRerouteDecisionInput = {
|
|
10
|
+
nowMs: number;
|
|
11
|
+
maxRetry: number;
|
|
12
|
+
requireAck: boolean;
|
|
13
|
+
currentRetryCount: number;
|
|
14
|
+
currentRouteAttemptRound: number;
|
|
15
|
+
currentFastReroutePending: boolean;
|
|
16
|
+
lastError?: string;
|
|
17
|
+
currentConnId?: string;
|
|
18
|
+
attemptedConnIds: string[];
|
|
19
|
+
availableConnIds: string[];
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type RetryRerouteDecision =
|
|
23
|
+
| {
|
|
24
|
+
kind: 'dead-letter';
|
|
25
|
+
terminalReason: string;
|
|
26
|
+
nextRetryCount: number;
|
|
27
|
+
lastAttemptAt: number;
|
|
28
|
+
}
|
|
29
|
+
| {
|
|
30
|
+
kind: 'retry';
|
|
31
|
+
nextRetryCount: number;
|
|
32
|
+
lastAttemptAt: number;
|
|
33
|
+
nextAttemptAt: number;
|
|
34
|
+
lastError: string;
|
|
35
|
+
attemptedConnIds: string[];
|
|
36
|
+
fastReroutePending: boolean;
|
|
37
|
+
routeAttemptRound: number;
|
|
38
|
+
hasUntriedAlternative: boolean;
|
|
39
|
+
shouldFastReroute: boolean;
|
|
40
|
+
revalidatedConnIds: string[];
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export function computeRetryRerouteDecision(
|
|
44
|
+
input: RetryRerouteDecisionInput,
|
|
45
|
+
deps: { backoffMs: (retryCount: number) => number },
|
|
46
|
+
): RetryRerouteDecision {
|
|
47
|
+
const attemptedConnIds = Array.isArray(input.attemptedConnIds)
|
|
48
|
+
? input.attemptedConnIds.filter((v): v is string => typeof v === 'string' && !!v)
|
|
49
|
+
: [];
|
|
50
|
+
const currentConnId = `${input.currentConnId || ''}`.trim();
|
|
51
|
+
if (currentConnId && !attemptedConnIds.includes(currentConnId)) attemptedConnIds.push(currentConnId);
|
|
52
|
+
|
|
53
|
+
const availableConnIds = Array.isArray(input.availableConnIds)
|
|
54
|
+
? input.availableConnIds.filter((v): v is string => typeof v === 'string' && !!v)
|
|
55
|
+
: [];
|
|
56
|
+
const revalidatedConnIds = attemptedConnIds.filter((connId) => availableConnIds.includes(connId));
|
|
57
|
+
const hasUntriedAlternative = availableConnIds.some((connId) => !attemptedConnIds.includes(connId));
|
|
58
|
+
const shouldFastReroute = input.requireAck && input.currentFastReroutePending !== true && hasUntriedAlternative;
|
|
59
|
+
|
|
60
|
+
const currentRetryCount = finiteNonNegativeIntegerOr(input.currentRetryCount, 0);
|
|
61
|
+
const currentRouteAttemptRound = finiteNonNegativeIntegerOr(input.currentRouteAttemptRound, 0);
|
|
62
|
+
const nextRetryCount = currentRetryCount + 1;
|
|
63
|
+
const lastAttemptAt = input.nowMs;
|
|
64
|
+
const terminalReason =
|
|
65
|
+
input.lastError || (input.requireAck ? 'push-ack-timeout' : 'push-delivery-unconfirmed');
|
|
66
|
+
|
|
67
|
+
if (nextRetryCount > input.maxRetry) {
|
|
68
|
+
return {
|
|
69
|
+
kind: 'dead-letter',
|
|
70
|
+
terminalReason,
|
|
71
|
+
nextRetryCount,
|
|
72
|
+
lastAttemptAt,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const nextAttemptAt = shouldFastReroute ? input.nowMs + 1_000 : input.nowMs + deps.backoffMs(nextRetryCount);
|
|
77
|
+
const lastError = input.requireAck ? 'push-ack-timeout' : 'push-delivery-unconfirmed';
|
|
78
|
+
const routeAttemptRound = hasUntriedAlternative ? currentRouteAttemptRound : currentRouteAttemptRound + 1;
|
|
79
|
+
const fastReroutePending = hasUntriedAlternative ? shouldFastReroute || input.currentFastReroutePending === true : false;
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
kind: 'retry',
|
|
83
|
+
nextRetryCount,
|
|
84
|
+
lastAttemptAt,
|
|
85
|
+
nextAttemptAt,
|
|
86
|
+
lastError,
|
|
87
|
+
attemptedConnIds: hasUntriedAlternative ? attemptedConnIds : [],
|
|
88
|
+
fastReroutePending,
|
|
89
|
+
routeAttemptRound,
|
|
90
|
+
hasUntriedAlternative,
|
|
91
|
+
shouldFastReroute,
|
|
92
|
+
revalidatedConnIds,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export type PushFailureDecisionInput = {
|
|
97
|
+
nowMs: number;
|
|
98
|
+
maxRetry: number;
|
|
99
|
+
currentRetryCount: number;
|
|
100
|
+
lastError?: string;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
export type PushFailureDecision =
|
|
104
|
+
| {
|
|
105
|
+
kind: 'dead-letter';
|
|
106
|
+
terminalReason: string;
|
|
107
|
+
nextRetryCount: number;
|
|
108
|
+
lastAttemptAt: number;
|
|
109
|
+
}
|
|
110
|
+
| {
|
|
111
|
+
kind: 'retry';
|
|
112
|
+
nextRetryCount: number;
|
|
113
|
+
lastAttemptAt: number;
|
|
114
|
+
nextAttemptAt: number;
|
|
115
|
+
lastError: string;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
export function computePushFailureDecision(
|
|
119
|
+
input: PushFailureDecisionInput,
|
|
120
|
+
deps: { backoffMs: (retryCount: number) => number },
|
|
121
|
+
): PushFailureDecision {
|
|
122
|
+
const currentRetryCount = finiteNonNegativeIntegerOr(input.currentRetryCount, 0);
|
|
123
|
+
const nextRetryCount = currentRetryCount + 1;
|
|
124
|
+
const lastAttemptAt = input.nowMs;
|
|
125
|
+
|
|
126
|
+
if (nextRetryCount > input.maxRetry) {
|
|
127
|
+
return {
|
|
128
|
+
kind: 'dead-letter',
|
|
129
|
+
terminalReason: input.lastError || 'push-retry-limit',
|
|
130
|
+
nextRetryCount,
|
|
131
|
+
lastAttemptAt,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
kind: 'retry',
|
|
137
|
+
nextRetryCount,
|
|
138
|
+
lastAttemptAt,
|
|
139
|
+
nextAttemptAt: input.nowMs + deps.backoffMs(nextRetryCount),
|
|
140
|
+
lastError: input.lastError || 'push-retry',
|
|
141
|
+
};
|
|
142
|
+
}
|
|
@@ -3,6 +3,7 @@ export async function sendBncrText(params: {
|
|
|
3
3
|
accountId: string;
|
|
4
4
|
to: string;
|
|
5
5
|
text: string;
|
|
6
|
+
kind?: 'tool' | 'block' | 'final';
|
|
6
7
|
replyToId?: string;
|
|
7
8
|
mediaLocalRoots?: readonly string[];
|
|
8
9
|
resolveVerifiedTarget: (
|
|
@@ -18,6 +19,7 @@ export async function sendBncrText(params: {
|
|
|
18
19
|
text?: string;
|
|
19
20
|
mediaUrl?: string;
|
|
20
21
|
mediaUrls?: string[];
|
|
22
|
+
kind?: 'tool' | 'block' | 'final';
|
|
21
23
|
replyToId?: string;
|
|
22
24
|
};
|
|
23
25
|
mediaLocalRoots?: readonly string[];
|
|
@@ -33,6 +35,7 @@ export async function sendBncrText(params: {
|
|
|
33
35
|
route: verified.route,
|
|
34
36
|
payload: {
|
|
35
37
|
text: params.text,
|
|
38
|
+
kind: params.kind,
|
|
36
39
|
replyToId: params.replyToId,
|
|
37
40
|
},
|
|
38
41
|
mediaLocalRoots: params.mediaLocalRoots,
|
|
@@ -54,6 +57,7 @@ export async function sendBncrMedia(params: {
|
|
|
54
57
|
mediaUrls?: string[];
|
|
55
58
|
asVoice?: boolean;
|
|
56
59
|
audioAsVoice?: boolean;
|
|
60
|
+
kind?: 'tool' | 'block' | 'final';
|
|
57
61
|
replyToId?: string;
|
|
58
62
|
mediaLocalRoots?: readonly string[];
|
|
59
63
|
resolveVerifiedTarget: (
|
|
@@ -71,6 +75,7 @@ export async function sendBncrMedia(params: {
|
|
|
71
75
|
mediaUrls?: string[];
|
|
72
76
|
asVoice?: boolean;
|
|
73
77
|
audioAsVoice?: boolean;
|
|
78
|
+
kind?: 'tool' | 'block' | 'final';
|
|
74
79
|
replyToId?: string;
|
|
75
80
|
};
|
|
76
81
|
mediaLocalRoots?: readonly string[];
|
|
@@ -90,6 +95,7 @@ export async function sendBncrMedia(params: {
|
|
|
90
95
|
mediaUrls: params.mediaUrls?.length ? params.mediaUrls : undefined,
|
|
91
96
|
asVoice: params.asVoice === true ? true : undefined,
|
|
92
97
|
audioAsVoice: params.audioAsVoice === true ? true : undefined,
|
|
98
|
+
kind: params.kind,
|
|
93
99
|
replyToId: params.replyToId,
|
|
94
100
|
},
|
|
95
101
|
mediaLocalRoots: params.mediaLocalRoots,
|
|
@@ -26,6 +26,28 @@ function asString(v: unknown, fallback = ''): string {
|
|
|
26
26
|
return String(v);
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
function attachBncrChannelRouteRefFields(args: {
|
|
30
|
+
built: Record<string, unknown>;
|
|
31
|
+
channel: string;
|
|
32
|
+
accountId?: string;
|
|
33
|
+
to: string;
|
|
34
|
+
chatType: 'direct' | 'group';
|
|
35
|
+
threadId?: string;
|
|
36
|
+
}) {
|
|
37
|
+
const { built, channel, accountId, to, chatType, threadId } = args;
|
|
38
|
+
return {
|
|
39
|
+
...built,
|
|
40
|
+
channel,
|
|
41
|
+
...(accountId !== undefined ? { accountId } : {}),
|
|
42
|
+
target: {
|
|
43
|
+
to,
|
|
44
|
+
rawTo: to,
|
|
45
|
+
chatType,
|
|
46
|
+
},
|
|
47
|
+
...(threadId !== undefined ? { thread: { id: threadId } } : {}),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
29
51
|
export function resolveBncrOutboundSessionRoute(params: ResolveBncrOutboundSessionRouteParams) {
|
|
30
52
|
const raw = asString(params.resolvedTarget?.to || params.target).trim();
|
|
31
53
|
if (!raw) return null;
|
|
@@ -65,9 +87,16 @@ export function resolveBncrOutboundSessionRoute(params: ResolveBncrOutboundSessi
|
|
|
65
87
|
...(params.threadId !== undefined ? { threadId: params.threadId } : {}),
|
|
66
88
|
});
|
|
67
89
|
|
|
68
|
-
return {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
90
|
+
return attachBncrChannelRouteRefFields({
|
|
91
|
+
built: {
|
|
92
|
+
...built,
|
|
93
|
+
sessionKey,
|
|
94
|
+
baseSessionKey: sessionKey,
|
|
95
|
+
},
|
|
96
|
+
channel: params.channel,
|
|
97
|
+
accountId: params.accountId,
|
|
98
|
+
to: displayTo,
|
|
99
|
+
chatType: 'direct',
|
|
100
|
+
threadId: params.threadId,
|
|
101
|
+
});
|
|
73
102
|
}
|