@xmoxmo/bncr 0.6.0 → 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.
- package/README.md +8 -0
- package/dist/index.js +2 -0
- package/index.ts +1 -0
- package/package.json +4 -1
- package/scripts/check-pack.mjs +41 -1
- package/scripts/selfcheck.mjs +3 -0
- package/src/bootstrap/register-runtime-gateway.ts +1 -0
- package/src/bootstrap/register-runtime.ts +1 -0
- package/src/channel.ts +50 -1
- package/src/core/types.ts +41 -0
- package/src/messaging/inbound/dispatch.ts +26 -8
- package/src/messaging/inbound/outbound-replay-cache.ts +236 -0
- package/src/messaging/inbound/turn-context.ts +16 -8
- package/src/plugin/ack-outbox-runtime-group.ts +2 -0
- package/src/plugin/bridge-call.ts +168 -0
- package/src/plugin/bridge-outbox-facade.ts +79 -1
- package/src/plugin/channel-components.ts +5 -0
- package/src/plugin/channel-plugin-bridge-group.ts +43 -1
- package/src/plugin/channel-plugin-surface-group.ts +82 -7
- package/src/plugin/channel-runtime-builders-delivery.ts +2 -0
- package/src/plugin/channel-runtime-types.ts +26 -2
- package/src/plugin/channel-send.ts +12 -19
- package/src/plugin/client-rpc-runtime.ts +129 -0
- package/src/plugin/gateway-methods.ts +1 -0
- package/src/plugin/inbound-handlers.ts +4 -0
- package/src/plugin/inbound-surface-handlers-group.ts +3 -0
- package/src/plugin/outbox-ack-outcome.ts +2 -0
- package/src/plugin/state-store.ts +125 -5
- package/src/plugin/state-transient-runtime-group.ts +8 -1
|
@@ -1,5 +1,11 @@
|
|
|
1
|
+
import type { TSchema } from 'typebox';
|
|
1
2
|
import type { RegisterDriftSnapshot } from '../core/register-trace.ts';
|
|
2
|
-
import type {
|
|
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
|
|
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
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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:
|
|
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
|
+
}
|
|
@@ -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
|
{
|
|
@@ -3,6 +3,10 @@ import {
|
|
|
3
3
|
normalizeRegisterDriftSnapshot,
|
|
4
4
|
} from '../core/register-trace.ts';
|
|
5
5
|
import type { BncrRoute, OutboxEntry } from '../core/types.ts';
|
|
6
|
+
import type {
|
|
7
|
+
BncrOutboundReplayEntry,
|
|
8
|
+
BncrOutboundReplayStatus,
|
|
9
|
+
} from '../messaging/inbound/outbound-replay-cache.ts';
|
|
6
10
|
import {
|
|
7
11
|
readOpenClawJsonFileWithFallback,
|
|
8
12
|
writeOpenClawJsonFileAtomically,
|
|
@@ -14,6 +18,8 @@ import type {
|
|
|
14
18
|
BncrPersistedGroupHistoryEntry,
|
|
15
19
|
BncrPersistedGroupHistoryMediaEntry,
|
|
16
20
|
BncrPersistedLastSession,
|
|
21
|
+
BncrPersistedOutboundReplayBucket,
|
|
22
|
+
BncrPersistedOutboundReplayEntry,
|
|
17
23
|
BncrPersistedSessionRoute,
|
|
18
24
|
PersistedState as BncrPersistedState,
|
|
19
25
|
BncrSceneRecord,
|
|
@@ -28,6 +34,9 @@ type BncrPersistedStateStoreInput = {
|
|
|
28
34
|
sessionRoutes?: unknown;
|
|
29
35
|
sceneRegistry?: unknown;
|
|
30
36
|
groupHistories?: unknown;
|
|
37
|
+
outboundReplayCache?: unknown;
|
|
38
|
+
// Legacy persisted key from before the outboundReplayCache rename.
|
|
39
|
+
messageCache?: unknown;
|
|
31
40
|
lastSessionByAccount?: unknown;
|
|
32
41
|
lastActivityByAccount?: unknown;
|
|
33
42
|
lastInboundByAccount?: unknown;
|
|
@@ -41,6 +50,8 @@ type PersistedSessionRouteInput = Partial<BncrPersistedSessionRoute>;
|
|
|
41
50
|
type PersistedSceneRecordInput = Partial<BncrSceneRecord>;
|
|
42
51
|
type PersistedGroupHistoryBucketInput = Partial<BncrPersistedGroupHistoryBucket>;
|
|
43
52
|
type PersistedGroupHistoryEntryInput = Partial<BncrPersistedGroupHistoryEntry>;
|
|
53
|
+
type PersistedOutboundReplayBucketInput = Partial<BncrPersistedOutboundReplayBucket>;
|
|
54
|
+
type PersistedOutboundReplayEntryInput = Partial<BncrOutboundReplayEntry>;
|
|
44
55
|
|
|
45
56
|
export function createBncrStateStore(runtime: {
|
|
46
57
|
getStatePath: () => string | null;
|
|
@@ -65,6 +76,7 @@ export function createBncrStateStore(runtime: {
|
|
|
65
76
|
maxAccountActivityEntries: number;
|
|
66
77
|
sceneRegistry: Map<string, BncrSceneRecord>;
|
|
67
78
|
groupHistories: Map<string, BncrPersistedGroupHistoryEntry[]>;
|
|
79
|
+
outboundReplayCache?: Map<string, BncrPersistedOutboundReplayEntry[]>;
|
|
68
80
|
outbox: Map<string, OutboxEntry>;
|
|
69
81
|
getDeadLetter: () => OutboxEntry[];
|
|
70
82
|
setDeadLetter: (entries: OutboxEntry[]) => void;
|
|
@@ -77,6 +89,9 @@ export function createBncrStateStore(runtime: {
|
|
|
77
89
|
getLastDriftSnapshot: () => BncrPersistedState['lastDriftSnapshot'];
|
|
78
90
|
setLastDriftSnapshot: (value: BncrPersistedState['lastDriftSnapshot']) => void;
|
|
79
91
|
}) {
|
|
92
|
+
const outboundReplayCache =
|
|
93
|
+
runtime.outboundReplayCache ?? new Map<string, BncrPersistedOutboundReplayEntry[]>();
|
|
94
|
+
|
|
80
95
|
function resolvePersistedGroupHistoryLimit(key: string): number {
|
|
81
96
|
const scene = runtime.sceneRegistry.get(key);
|
|
82
97
|
const sceneLimit = scene?.kind === 'group' ? scene.historyLimit : undefined;
|
|
@@ -148,8 +163,12 @@ export function createBncrStateStore(runtime: {
|
|
|
148
163
|
return Array.from(runtime.sceneRegistry.values()).sort((a, b) => a.lastSeenAt - b.lastSeenAt);
|
|
149
164
|
}
|
|
150
165
|
|
|
151
|
-
function
|
|
152
|
-
|
|
166
|
+
function loadPersistedHistoryBuckets(
|
|
167
|
+
persisted: unknown,
|
|
168
|
+
target: Map<string, BncrPersistedGroupHistoryEntry[]>,
|
|
169
|
+
resolveLimit: (key: string) => number,
|
|
170
|
+
): void {
|
|
171
|
+
target.clear();
|
|
153
172
|
const buckets = Array.isArray(persisted)
|
|
154
173
|
? (persisted as PersistedGroupHistoryBucketInput[])
|
|
155
174
|
: [];
|
|
@@ -199,15 +218,112 @@ export function createBncrStateStore(runtime: {
|
|
|
199
218
|
entries.push(normalizedEntry);
|
|
200
219
|
}
|
|
201
220
|
if (entries.length > 0) {
|
|
202
|
-
|
|
221
|
+
const limit = resolveLimit(key);
|
|
222
|
+
target.set(key, Number.isFinite(limit) ? entries.slice(-limit) : entries);
|
|
203
223
|
}
|
|
204
224
|
}
|
|
205
225
|
}
|
|
206
226
|
|
|
227
|
+
function loadPersistedGroupHistories(persisted: unknown): void {
|
|
228
|
+
loadPersistedHistoryBuckets(persisted, runtime.groupHistories, (key) =>
|
|
229
|
+
resolvePersistedGroupHistoryLimit(key),
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function loadPersistedOutboundReplayCache(persisted: unknown): void {
|
|
234
|
+
outboundReplayCache.clear();
|
|
235
|
+
const buckets = Array.isArray(persisted)
|
|
236
|
+
? (persisted as PersistedOutboundReplayBucketInput[])
|
|
237
|
+
: [];
|
|
238
|
+
for (const bucket of buckets) {
|
|
239
|
+
const key = runtime.asString(bucket?.key || '').trim();
|
|
240
|
+
if (!key) continue;
|
|
241
|
+
const entries: BncrOutboundReplayEntry[] = [];
|
|
242
|
+
const rawEntries = Array.isArray(bucket?.entries)
|
|
243
|
+
? (bucket.entries as PersistedOutboundReplayEntryInput[])
|
|
244
|
+
: [];
|
|
245
|
+
for (const entry of rawEntries) {
|
|
246
|
+
const sender = runtime.asString(entry?.sender || '').trim();
|
|
247
|
+
const body = runtime.asString(entry?.body || '').trim();
|
|
248
|
+
if (!sender || !body) continue;
|
|
249
|
+
const timestamp = runtime.finiteNumberOr(entry?.timestamp, 0);
|
|
250
|
+
const messageId = runtime.asString(entry?.messageId || '').trim();
|
|
251
|
+
const accountId = runtime.asString(entry?.accountId || '').trim();
|
|
252
|
+
const sessionKey = runtime.asString(entry?.sessionKey || '').trim();
|
|
253
|
+
const type = runtime.asString(entry?.type || '').trim();
|
|
254
|
+
const mediaUrl = runtime.asString(entry?.mediaUrl || '').trim();
|
|
255
|
+
const createdAt = runtime.finiteNumberOr(entry?.createdAt, 0);
|
|
256
|
+
const status = runtime.asString(entry?.status || '').trim();
|
|
257
|
+
const route =
|
|
258
|
+
runtime.parseRouteLike(entry?.route) ||
|
|
259
|
+
runtime.parseRouteLike({
|
|
260
|
+
platform: entry?.route?.platform,
|
|
261
|
+
groupId: entry?.route?.groupId,
|
|
262
|
+
userId: entry?.route?.userId,
|
|
263
|
+
});
|
|
264
|
+
const media: BncrPersistedGroupHistoryMediaEntry[] = [];
|
|
265
|
+
const rawMedia = Array.isArray(entry?.media)
|
|
266
|
+
? (entry.media as Partial<BncrPersistedGroupHistoryMediaEntry>[])
|
|
267
|
+
: [];
|
|
268
|
+
for (const item of rawMedia) {
|
|
269
|
+
const path = runtime.asString(item?.path || '').trim();
|
|
270
|
+
if (!path) continue;
|
|
271
|
+
const contentType = runtime.asString(item?.contentType || '').trim();
|
|
272
|
+
const kind = runtime.asString(item?.kind || '').trim();
|
|
273
|
+
const mediaMessageId = runtime.asString(item?.messageId || '').trim();
|
|
274
|
+
media.push({
|
|
275
|
+
path,
|
|
276
|
+
...(contentType ? { contentType } : {}),
|
|
277
|
+
...(kind ? { kind: kind as BncrPersistedGroupHistoryMediaEntry['kind'] } : {}),
|
|
278
|
+
...(mediaMessageId ? { messageId: mediaMessageId } : {}),
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
entries.push({
|
|
282
|
+
sender,
|
|
283
|
+
...(runtime.asString(entry?.senderId || '').trim()
|
|
284
|
+
? { senderId: runtime.asString(entry?.senderId || '').trim() }
|
|
285
|
+
: {}),
|
|
286
|
+
body,
|
|
287
|
+
...(timestamp > 0 ? { timestamp } : {}),
|
|
288
|
+
...(messageId ? { messageId } : {}),
|
|
289
|
+
...(media.length > 0 ? { media } : {}),
|
|
290
|
+
...(accountId ? { accountId } : {}),
|
|
291
|
+
...(sessionKey ? { sessionKey } : {}),
|
|
292
|
+
...(route ? { route } : {}),
|
|
293
|
+
...(type ? { type } : {}),
|
|
294
|
+
...(mediaUrl ? { mediaUrl } : {}),
|
|
295
|
+
...(createdAt > 0 ? { createdAt } : {}),
|
|
296
|
+
...(status === 'pushed' || status === 'acked'
|
|
297
|
+
? { status: status as BncrOutboundReplayStatus }
|
|
298
|
+
: {}),
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
if (entries.length > 0) {
|
|
302
|
+
outboundReplayCache.set(key, entries);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function dumpPersistedHistoryBuckets(
|
|
308
|
+
source: Map<string, BncrPersistedGroupHistoryEntry[]>,
|
|
309
|
+
resolveLimit: (key: string) => number,
|
|
310
|
+
) {
|
|
311
|
+
return Array.from(source.entries()).map(([key, entries]) => ({
|
|
312
|
+
key,
|
|
313
|
+
entries: Number.isFinite(resolveLimit(key)) ? entries.slice(-resolveLimit(key)) : entries,
|
|
314
|
+
}));
|
|
315
|
+
}
|
|
316
|
+
|
|
207
317
|
function dumpPersistedGroupHistories() {
|
|
208
|
-
return
|
|
318
|
+
return dumpPersistedHistoryBuckets(runtime.groupHistories, (key) =>
|
|
319
|
+
resolvePersistedGroupHistoryLimit(key),
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function dumpPersistedOutboundReplayCache() {
|
|
324
|
+
return Array.from(outboundReplayCache.entries()).map(([key, entries]) => ({
|
|
209
325
|
key,
|
|
210
|
-
entries
|
|
326
|
+
entries,
|
|
211
327
|
}));
|
|
212
328
|
}
|
|
213
329
|
|
|
@@ -359,6 +475,7 @@ export function createBncrStateStore(runtime: {
|
|
|
359
475
|
loadPersistedSessionRoutes(data.sessionRoutes);
|
|
360
476
|
loadPersistedSceneRegistry(data.sceneRegistry);
|
|
361
477
|
loadPersistedGroupHistories(data.groupHistories);
|
|
478
|
+
loadPersistedOutboundReplayCache(data.outboundReplayCache ?? data.messageCache);
|
|
362
479
|
loadPersistedLastSessionMap(data.lastSessionByAccount);
|
|
363
480
|
loadPersistedAccountTimestampMap(runtime.lastActivityByAccount, data.lastActivityByAccount);
|
|
364
481
|
loadPersistedAccountTimestampMap(runtime.lastInboundByAccount, data.lastInboundByAccount);
|
|
@@ -378,6 +495,7 @@ export function createBncrStateStore(runtime: {
|
|
|
378
495
|
sessionRoutes: dumpPersistedSessionRoutes(),
|
|
379
496
|
sceneRegistry: dumpPersistedSceneRegistry(),
|
|
380
497
|
groupHistories: dumpPersistedGroupHistories(),
|
|
498
|
+
outboundReplayCache: dumpPersistedOutboundReplayCache(),
|
|
381
499
|
lastSessionByAccount: dumpPersistedLastSessionMap(),
|
|
382
500
|
lastActivityByAccount: dumpPersistedAccountTimestampMap(runtime.lastActivityByAccount),
|
|
383
501
|
lastInboundByAccount: dumpPersistedAccountTimestampMap(runtime.lastInboundByAccount),
|
|
@@ -399,6 +517,8 @@ export function createBncrStateStore(runtime: {
|
|
|
399
517
|
dumpPersistedSceneRegistry,
|
|
400
518
|
loadPersistedGroupHistories,
|
|
401
519
|
dumpPersistedGroupHistories,
|
|
520
|
+
loadPersistedOutboundReplayCache,
|
|
521
|
+
dumpPersistedOutboundReplayCache,
|
|
402
522
|
backfillAccountActivityFromSessionRoutes,
|
|
403
523
|
loadState,
|
|
404
524
|
flushState,
|
|
@@ -6,7 +6,11 @@ import type {
|
|
|
6
6
|
FileSendTransferState,
|
|
7
7
|
OutboxEntry,
|
|
8
8
|
} from '../core/types.ts';
|
|
9
|
-
import type {
|
|
9
|
+
import type {
|
|
10
|
+
BncrPersistedGroupHistoryEntry,
|
|
11
|
+
BncrPersistedOutboundReplayEntry,
|
|
12
|
+
BncrSceneRecord,
|
|
13
|
+
} from './channel-runtime-types.ts';
|
|
10
14
|
import { createBncrStateStore } from './state-store.ts';
|
|
11
15
|
import { createBncrTransientStateRuntime } from './transient-state-runtime.ts';
|
|
12
16
|
|
|
@@ -34,6 +38,7 @@ export function createBncrStateTransientRuntimeGroup(runtime: {
|
|
|
34
38
|
maxAccountActivityEntries: number;
|
|
35
39
|
sceneRegistry: Map<string, BncrSceneRecord>;
|
|
36
40
|
groupHistories: Map<string, BncrPersistedGroupHistoryEntry[]>;
|
|
41
|
+
outboundReplayCache?: Map<string, BncrPersistedOutboundReplayEntry[]>;
|
|
37
42
|
outbox: Map<string, OutboxEntry>;
|
|
38
43
|
getDeadLetter: () => OutboxEntry[];
|
|
39
44
|
setDeadLetter: (entries: OutboxEntry[]) => void;
|
|
@@ -74,6 +79,8 @@ export function createBncrStateTransientRuntimeGroup(runtime: {
|
|
|
74
79
|
maxAccountActivityEntries: runtime.maxAccountActivityEntries,
|
|
75
80
|
sceneRegistry: runtime.sceneRegistry,
|
|
76
81
|
groupHistories: runtime.groupHistories,
|
|
82
|
+
outboundReplayCache:
|
|
83
|
+
runtime.outboundReplayCache ?? new Map<string, BncrPersistedOutboundReplayEntry[]>(),
|
|
77
84
|
outbox: runtime.outbox,
|
|
78
85
|
getDeadLetter: runtime.getDeadLetter,
|
|
79
86
|
setDeadLetter: runtime.setDeadLetter,
|