@xmoxmo/bncr 0.6.0 → 0.6.2
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 +4 -1
- package/src/bootstrap/register-runtime-gateway.ts +1 -0
- package/src/bootstrap/register-runtime.ts +1 -0
- package/src/channel.ts +66 -5
- package/src/core/types.ts +41 -0
- package/src/messaging/inbound/context-facts.ts +88 -58
- package/src/messaging/inbound/{group-history.ts → conversation-history.ts} +92 -25
- package/src/messaging/inbound/dispatch.ts +90 -47
- package/src/messaging/inbound/native-command.ts +1 -1
- package/src/messaging/inbound/outbound-replay-cache.ts +312 -0
- package/src/messaging/inbound/scene-admin.ts +24 -9
- package/src/messaging/inbound/turn-context.ts +105 -33
- 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 +88 -1
- package/src/plugin/channel-components.ts +10 -5
- 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 +4 -2
- package/src/plugin/channel-runtime-types.ts +34 -8
- 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-acceptance.ts +9 -24
- package/src/plugin/inbound-handlers.ts +8 -4
- package/src/plugin/inbound-surface-handlers-group.ts +7 -2
- package/src/plugin/outbox-ack-outcome.ts +2 -0
- package/src/plugin/scene-registry.ts +4 -4
- package/src/plugin/state-store.ts +153 -29
- package/src/plugin/state-transient-runtime-group.ts +10 -3
|
@@ -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
|
+
}
|
|
@@ -66,30 +66,12 @@ function shouldDispatchForScene(args: {
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
function shouldAccumulateForScene(args: {
|
|
69
|
-
|
|
69
|
+
shouldDispatch: boolean;
|
|
70
70
|
admission: ReturnType<typeof decideSceneAdmission>;
|
|
71
|
-
cfg: BncrChannelConfigRoot;
|
|
72
71
|
}) {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const isBncrNativeCommand =
|
|
78
|
-
parseBncrNativeCommand(parsed.extracted.text, {
|
|
79
|
-
allowBareWhoami: parsed.isAdmin !== true,
|
|
80
|
-
}) !== null;
|
|
81
|
-
const isAdminOpenClawNativeCommand =
|
|
82
|
-
parsed.isAdmin === true &&
|
|
83
|
-
!isBncrNativeCommand &&
|
|
84
|
-
hasControlCommand(parsed.extracted.text, cfg as Parameters<typeof hasControlCommand>[1]);
|
|
85
|
-
if (isAdminOpenClawNativeCommand) return true;
|
|
86
|
-
|
|
87
|
-
const mode = admission.scene.groupReplyMode || 'admin';
|
|
88
|
-
// admin and all: no accumulation needed - messages are either dispatched directly or discarded
|
|
89
|
-
if (mode === 'admin') return false;
|
|
90
|
-
if (mode === 'all') return false;
|
|
91
|
-
// mention and hybrid: non-trigger messages accumulate for future context
|
|
92
|
-
return true;
|
|
72
|
+
if (args.shouldDispatch) return true;
|
|
73
|
+
const mode = args.admission.scene.groupReplyMode || 'admin';
|
|
74
|
+
return mode === 'mention' || mode === 'hybrid';
|
|
93
75
|
}
|
|
94
76
|
|
|
95
77
|
export async function prepareBncrInboundAcceptance(args: {
|
|
@@ -206,6 +188,9 @@ export async function prepareBncrInboundAcceptance(args: {
|
|
|
206
188
|
resolveAgentRoute: args.resolveAgentRoute,
|
|
207
189
|
});
|
|
208
190
|
|
|
191
|
+
const shouldDispatch = shouldDispatchForScene({ parsed, admission, cfg });
|
|
192
|
+
const shouldAccumulate = shouldAccumulateForScene({ shouldDispatch, admission });
|
|
193
|
+
|
|
209
194
|
return {
|
|
210
195
|
ok: true as const,
|
|
211
196
|
accountId,
|
|
@@ -213,8 +198,8 @@ export async function prepareBncrInboundAcceptance(args: {
|
|
|
213
198
|
inboundText,
|
|
214
199
|
hasMedia: Boolean(mediaBase64 || mediaPathFromTransfer),
|
|
215
200
|
resolvedAgentId: admission.agentId,
|
|
216
|
-
shouldDispatch
|
|
217
|
-
shouldAccumulate
|
|
201
|
+
shouldDispatch,
|
|
202
|
+
shouldAccumulate,
|
|
218
203
|
dispatchBy: resolveDispatchBy({ parsed, admission, cfg }),
|
|
219
204
|
};
|
|
220
205
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
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
|
-
import type {
|
|
4
|
+
import type { BncrConversationHistoryMap } from '../messaging/inbound/conversation-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 {
|
|
@@ -133,7 +134,8 @@ export type BncrInboundHandlersRuntime = {
|
|
|
133
134
|
}) => string;
|
|
134
135
|
defaultPublicAgentId: () => string;
|
|
135
136
|
sceneRegistry: Map<string, BncrSceneRecord>;
|
|
136
|
-
|
|
137
|
+
conversationHistories: BncrConversationHistoryMap;
|
|
138
|
+
outboundReplayCache: BncrOutboundReplayCache;
|
|
137
139
|
prepareInboundAcceptance: (args: {
|
|
138
140
|
parsed: ParsedInboundParams;
|
|
139
141
|
canonicalAgentId: string;
|
|
@@ -163,7 +165,8 @@ export type BncrInboundHandlersRuntime = {
|
|
|
163
165
|
shouldDispatch: boolean;
|
|
164
166
|
shouldAccumulate: boolean;
|
|
165
167
|
sceneRegistry: Map<string, BncrSceneRecord>;
|
|
166
|
-
|
|
168
|
+
conversationHistories: BncrConversationHistoryMap;
|
|
169
|
+
outboundReplayCache: BncrOutboundReplayCache;
|
|
167
170
|
defaultAdminAgentId: string;
|
|
168
171
|
defaultPublicAgentId: string;
|
|
169
172
|
now: () => number;
|
|
@@ -332,7 +335,8 @@ export function createBncrInboundHandlers(runtime: Omit<BncrInboundHandlersRunti
|
|
|
332
335
|
shouldDispatch,
|
|
333
336
|
shouldAccumulate,
|
|
334
337
|
sceneRegistry: runtime.sceneRegistry,
|
|
335
|
-
|
|
338
|
+
conversationHistories: runtime.conversationHistories,
|
|
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,
|
|
@@ -85,7 +86,10 @@ export function createBncrInboundSurfaceHandlersGroup(runtime: {
|
|
|
85
86
|
typeof createBncrInboundHandlersComponent
|
|
86
87
|
>[0]['defaultPublicAgentId'];
|
|
87
88
|
sceneRegistry: Parameters<typeof createBncrInboundHandlersComponent>[0]['sceneRegistry'];
|
|
88
|
-
|
|
89
|
+
conversationHistories: Parameters<
|
|
90
|
+
typeof createBncrInboundHandlersComponent
|
|
91
|
+
>[0]['conversationHistories'];
|
|
92
|
+
outboundReplayCache: BncrOutboundReplayCache;
|
|
89
93
|
prepareInboundAcceptance: Parameters<
|
|
90
94
|
typeof createBncrInboundHandlersComponent
|
|
91
95
|
>[0]['prepareInboundAcceptance'];
|
|
@@ -151,7 +155,8 @@ export function createBncrInboundSurfaceHandlersGroup(runtime: {
|
|
|
151
155
|
defaultAdminAgentId: runtime.defaultAdminAgentId,
|
|
152
156
|
defaultPublicAgentId: runtime.defaultPublicAgentId,
|
|
153
157
|
sceneRegistry: runtime.sceneRegistry,
|
|
154
|
-
|
|
158
|
+
conversationHistories: runtime.conversationHistories,
|
|
159
|
+
outboundReplayCache: runtime.outboundReplayCache,
|
|
155
160
|
prepareInboundAcceptance: runtime.prepareInboundAcceptance,
|
|
156
161
|
logInboundSummary: runtime.logInboundSummary,
|
|
157
162
|
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
|
{
|
|
@@ -56,8 +56,8 @@ function buildSceneRecord(args: {
|
|
|
56
56
|
...(kind === 'group' && parsed.groupName ? { groupName: parsed.groupName } : {}),
|
|
57
57
|
...(agentId ? { agentId } : {}),
|
|
58
58
|
...(kind === 'group' ? { groupReplyMode: defaultGroupReplyMode(kind) } : {}),
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
historyLimit: 50,
|
|
60
|
+
historyForce: true,
|
|
61
61
|
lastSeenAt,
|
|
62
62
|
};
|
|
63
63
|
}
|
|
@@ -90,8 +90,8 @@ export function decideSceneAdmission(args: {
|
|
|
90
90
|
...(kind === 'group' && !existing.groupReplyMode
|
|
91
91
|
? { groupReplyMode: defaultGroupReplyMode(kind) }
|
|
92
92
|
: {}),
|
|
93
|
-
...(
|
|
94
|
-
...(
|
|
93
|
+
...(existing.historyLimit === undefined ? { historyLimit: 50 } : {}),
|
|
94
|
+
...(existing.historyForce === undefined ? { historyForce: true } : {}),
|
|
95
95
|
lastSeenAt: now,
|
|
96
96
|
} satisfies BncrSceneRecord;
|
|
97
97
|
|
|
@@ -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,
|
|
@@ -10,24 +14,30 @@ import {
|
|
|
10
14
|
import type {
|
|
11
15
|
BncrGroupReplyMode,
|
|
12
16
|
BncrPersistedAccountTimestamp,
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
17
|
+
BncrPersistedConversationHistoryBucket,
|
|
18
|
+
BncrPersistedConversationHistoryEntry,
|
|
19
|
+
BncrPersistedConversationHistoryMediaEntry,
|
|
16
20
|
BncrPersistedLastSession,
|
|
21
|
+
BncrPersistedOutboundReplayBucket,
|
|
22
|
+
BncrPersistedOutboundReplayEntry,
|
|
17
23
|
BncrPersistedSessionRoute,
|
|
18
24
|
PersistedState as BncrPersistedState,
|
|
19
25
|
BncrSceneRecord,
|
|
20
26
|
} from './channel-runtime-types.ts';
|
|
21
27
|
|
|
22
28
|
const GROUP_REPLY_MODES = new Set<BncrGroupReplyMode>(['admin', 'mention', 'hybrid', 'all']);
|
|
23
|
-
const
|
|
29
|
+
const DEFAULT_PERSISTED_CONVERSATION_HISTORY_LIMIT = 50;
|
|
24
30
|
|
|
25
31
|
type BncrPersistedStateStoreInput = {
|
|
26
32
|
outbox?: unknown;
|
|
27
33
|
deadLetter?: unknown;
|
|
28
34
|
sessionRoutes?: unknown;
|
|
29
35
|
sceneRegistry?: unknown;
|
|
36
|
+
conversationHistories?: unknown;
|
|
30
37
|
groupHistories?: unknown;
|
|
38
|
+
outboundReplayCache?: unknown;
|
|
39
|
+
// Legacy persisted key from before the outboundReplayCache rename.
|
|
40
|
+
messageCache?: unknown;
|
|
31
41
|
lastSessionByAccount?: unknown;
|
|
32
42
|
lastActivityByAccount?: unknown;
|
|
33
43
|
lastInboundByAccount?: unknown;
|
|
@@ -39,8 +49,10 @@ type PersistedAccountTimestampInput = Partial<BncrPersistedAccountTimestamp>;
|
|
|
39
49
|
type PersistedLastSessionInput = Partial<BncrPersistedLastSession>;
|
|
40
50
|
type PersistedSessionRouteInput = Partial<BncrPersistedSessionRoute>;
|
|
41
51
|
type PersistedSceneRecordInput = Partial<BncrSceneRecord>;
|
|
42
|
-
type
|
|
43
|
-
type
|
|
52
|
+
type PersistedConversationHistoryBucketInput = Partial<BncrPersistedConversationHistoryBucket>;
|
|
53
|
+
type PersistedConversationHistoryEntryInput = Partial<BncrPersistedConversationHistoryEntry>;
|
|
54
|
+
type PersistedOutboundReplayBucketInput = Partial<BncrPersistedOutboundReplayBucket>;
|
|
55
|
+
type PersistedOutboundReplayEntryInput = Partial<BncrOutboundReplayEntry>;
|
|
44
56
|
|
|
45
57
|
export function createBncrStateStore(runtime: {
|
|
46
58
|
getStatePath: () => string | null;
|
|
@@ -64,7 +76,8 @@ export function createBncrStateStore(runtime: {
|
|
|
64
76
|
maxSessionRouteEntries: number;
|
|
65
77
|
maxAccountActivityEntries: number;
|
|
66
78
|
sceneRegistry: Map<string, BncrSceneRecord>;
|
|
67
|
-
|
|
79
|
+
conversationHistories: Map<string, BncrPersistedConversationHistoryEntry[]>;
|
|
80
|
+
outboundReplayCache?: Map<string, BncrPersistedOutboundReplayEntry[]>;
|
|
68
81
|
outbox: Map<string, OutboxEntry>;
|
|
69
82
|
getDeadLetter: () => OutboxEntry[];
|
|
70
83
|
setDeadLetter: (entries: OutboxEntry[]) => void;
|
|
@@ -77,16 +90,19 @@ export function createBncrStateStore(runtime: {
|
|
|
77
90
|
getLastDriftSnapshot: () => BncrPersistedState['lastDriftSnapshot'];
|
|
78
91
|
setLastDriftSnapshot: (value: BncrPersistedState['lastDriftSnapshot']) => void;
|
|
79
92
|
}) {
|
|
80
|
-
|
|
93
|
+
const outboundReplayCache =
|
|
94
|
+
runtime.outboundReplayCache ?? new Map<string, BncrPersistedOutboundReplayEntry[]>();
|
|
95
|
+
|
|
96
|
+
function resolvePersistedConversationHistoryLimit(key: string): number {
|
|
81
97
|
const scene = runtime.sceneRegistry.get(key);
|
|
82
|
-
const sceneLimit = scene?.
|
|
98
|
+
const sceneLimit = scene?.historyLimit;
|
|
83
99
|
if (typeof sceneLimit === 'number' && Number.isFinite(sceneLimit) && sceneLimit >= 0) {
|
|
84
100
|
return Math.max(
|
|
85
|
-
|
|
101
|
+
DEFAULT_PERSISTED_CONVERSATION_HISTORY_LIMIT,
|
|
86
102
|
Math.ceil(Math.floor(sceneLimit) * 1.2),
|
|
87
103
|
);
|
|
88
104
|
}
|
|
89
|
-
return
|
|
105
|
+
return DEFAULT_PERSISTED_CONVERSATION_HISTORY_LIMIT;
|
|
90
106
|
}
|
|
91
107
|
|
|
92
108
|
function loadPersistedSceneRegistry(persisted: unknown): void {
|
|
@@ -148,17 +164,21 @@ export function createBncrStateStore(runtime: {
|
|
|
148
164
|
return Array.from(runtime.sceneRegistry.values()).sort((a, b) => a.lastSeenAt - b.lastSeenAt);
|
|
149
165
|
}
|
|
150
166
|
|
|
151
|
-
function
|
|
152
|
-
|
|
167
|
+
function loadPersistedHistoryBuckets(
|
|
168
|
+
persisted: unknown,
|
|
169
|
+
target: Map<string, BncrPersistedConversationHistoryEntry[]>,
|
|
170
|
+
resolveLimit: (key: string) => number,
|
|
171
|
+
): void {
|
|
172
|
+
target.clear();
|
|
153
173
|
const buckets = Array.isArray(persisted)
|
|
154
|
-
? (persisted as
|
|
174
|
+
? (persisted as PersistedConversationHistoryBucketInput[])
|
|
155
175
|
: [];
|
|
156
176
|
for (const bucket of buckets) {
|
|
157
177
|
const key = runtime.asString(bucket?.key || '').trim();
|
|
158
178
|
if (!key) continue;
|
|
159
|
-
const entries:
|
|
179
|
+
const entries: BncrPersistedConversationHistoryEntry[] = [];
|
|
160
180
|
const rawEntries = Array.isArray(bucket?.entries)
|
|
161
|
-
? (bucket.entries as
|
|
181
|
+
? (bucket.entries as PersistedConversationHistoryEntryInput[])
|
|
162
182
|
: [];
|
|
163
183
|
for (const entry of rawEntries) {
|
|
164
184
|
const sender = runtime.asString(entry?.sender || '').trim();
|
|
@@ -167,9 +187,9 @@ export function createBncrStateStore(runtime: {
|
|
|
167
187
|
const timestamp = runtime.finiteNumberOr(entry?.timestamp, 0);
|
|
168
188
|
const messageId = runtime.asString(entry?.messageId || '').trim();
|
|
169
189
|
|
|
170
|
-
const media:
|
|
190
|
+
const media: BncrPersistedConversationHistoryMediaEntry[] = [];
|
|
171
191
|
const rawMedia = Array.isArray(entry?.media)
|
|
172
|
-
? (entry.media as Partial<
|
|
192
|
+
? (entry.media as Partial<BncrPersistedConversationHistoryMediaEntry>[])
|
|
173
193
|
: [];
|
|
174
194
|
for (const item of rawMedia) {
|
|
175
195
|
const path = runtime.asString(item?.path || '').trim();
|
|
@@ -177,20 +197,23 @@ export function createBncrStateStore(runtime: {
|
|
|
177
197
|
const contentType = runtime.asString(item?.contentType || '').trim();
|
|
178
198
|
const kind = runtime.asString(item?.kind || '').trim();
|
|
179
199
|
const mediaMessageId = runtime.asString(item?.messageId || '').trim();
|
|
180
|
-
const normalizedMedia:
|
|
200
|
+
const normalizedMedia: BncrPersistedConversationHistoryMediaEntry = {
|
|
181
201
|
path,
|
|
182
202
|
...(contentType ? { contentType } : {}),
|
|
183
|
-
...(kind ? { kind: kind as
|
|
203
|
+
...(kind ? { kind: kind as BncrPersistedConversationHistoryMediaEntry['kind'] } : {}),
|
|
184
204
|
...(mediaMessageId ? { messageId: mediaMessageId } : {}),
|
|
185
205
|
};
|
|
186
206
|
media.push(normalizedMedia);
|
|
187
207
|
}
|
|
188
208
|
|
|
189
|
-
const normalizedEntry:
|
|
209
|
+
const normalizedEntry: BncrPersistedConversationHistoryEntry = {
|
|
190
210
|
sender,
|
|
191
211
|
...(runtime.asString(entry?.senderId || '').trim()
|
|
192
212
|
? { senderId: runtime.asString(entry?.senderId || '').trim() }
|
|
193
213
|
: {}),
|
|
214
|
+
...(entry?.role === 'user' || entry?.role === 'assistant' || entry?.role === 'system'
|
|
215
|
+
? { role: entry.role }
|
|
216
|
+
: {}),
|
|
194
217
|
body,
|
|
195
218
|
...(timestamp > 0 ? { timestamp } : {}),
|
|
196
219
|
...(messageId ? { messageId } : {}),
|
|
@@ -199,15 +222,112 @@ export function createBncrStateStore(runtime: {
|
|
|
199
222
|
entries.push(normalizedEntry);
|
|
200
223
|
}
|
|
201
224
|
if (entries.length > 0) {
|
|
202
|
-
|
|
225
|
+
const limit = resolveLimit(key);
|
|
226
|
+
target.set(key, Number.isFinite(limit) ? entries.slice(-limit) : entries);
|
|
203
227
|
}
|
|
204
228
|
}
|
|
205
229
|
}
|
|
206
230
|
|
|
207
|
-
function
|
|
208
|
-
|
|
231
|
+
function loadPersistedConversationHistories(persisted: unknown): void {
|
|
232
|
+
loadPersistedHistoryBuckets(persisted, runtime.conversationHistories, (key) =>
|
|
233
|
+
resolvePersistedConversationHistoryLimit(key),
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function loadPersistedOutboundReplayCache(persisted: unknown): void {
|
|
238
|
+
outboundReplayCache.clear();
|
|
239
|
+
const buckets = Array.isArray(persisted)
|
|
240
|
+
? (persisted as PersistedOutboundReplayBucketInput[])
|
|
241
|
+
: [];
|
|
242
|
+
for (const bucket of buckets) {
|
|
243
|
+
const key = runtime.asString(bucket?.key || '').trim();
|
|
244
|
+
if (!key) continue;
|
|
245
|
+
const entries: BncrOutboundReplayEntry[] = [];
|
|
246
|
+
const rawEntries = Array.isArray(bucket?.entries)
|
|
247
|
+
? (bucket.entries as PersistedOutboundReplayEntryInput[])
|
|
248
|
+
: [];
|
|
249
|
+
for (const entry of rawEntries) {
|
|
250
|
+
const sender = runtime.asString(entry?.sender || '').trim();
|
|
251
|
+
const body = runtime.asString(entry?.body || '').trim();
|
|
252
|
+
if (!sender || !body) continue;
|
|
253
|
+
const timestamp = runtime.finiteNumberOr(entry?.timestamp, 0);
|
|
254
|
+
const messageId = runtime.asString(entry?.messageId || '').trim();
|
|
255
|
+
const accountId = runtime.asString(entry?.accountId || '').trim();
|
|
256
|
+
const sessionKey = runtime.asString(entry?.sessionKey || '').trim();
|
|
257
|
+
const type = runtime.asString(entry?.type || '').trim();
|
|
258
|
+
const mediaUrl = runtime.asString(entry?.mediaUrl || '').trim();
|
|
259
|
+
const createdAt = runtime.finiteNumberOr(entry?.createdAt, 0);
|
|
260
|
+
const status = runtime.asString(entry?.status || '').trim();
|
|
261
|
+
const route =
|
|
262
|
+
runtime.parseRouteLike(entry?.route) ||
|
|
263
|
+
runtime.parseRouteLike({
|
|
264
|
+
platform: entry?.route?.platform,
|
|
265
|
+
groupId: entry?.route?.groupId,
|
|
266
|
+
userId: entry?.route?.userId,
|
|
267
|
+
});
|
|
268
|
+
const media: BncrPersistedConversationHistoryMediaEntry[] = [];
|
|
269
|
+
const rawMedia = Array.isArray(entry?.media)
|
|
270
|
+
? (entry.media as Partial<BncrPersistedConversationHistoryMediaEntry>[])
|
|
271
|
+
: [];
|
|
272
|
+
for (const item of rawMedia) {
|
|
273
|
+
const path = runtime.asString(item?.path || '').trim();
|
|
274
|
+
if (!path) continue;
|
|
275
|
+
const contentType = runtime.asString(item?.contentType || '').trim();
|
|
276
|
+
const kind = runtime.asString(item?.kind || '').trim();
|
|
277
|
+
const mediaMessageId = runtime.asString(item?.messageId || '').trim();
|
|
278
|
+
media.push({
|
|
279
|
+
path,
|
|
280
|
+
...(contentType ? { contentType } : {}),
|
|
281
|
+
...(kind ? { kind: kind as BncrPersistedConversationHistoryMediaEntry['kind'] } : {}),
|
|
282
|
+
...(mediaMessageId ? { messageId: mediaMessageId } : {}),
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
entries.push({
|
|
286
|
+
sender,
|
|
287
|
+
...(runtime.asString(entry?.senderId || '').trim()
|
|
288
|
+
? { senderId: runtime.asString(entry?.senderId || '').trim() }
|
|
289
|
+
: {}),
|
|
290
|
+
body,
|
|
291
|
+
...(timestamp > 0 ? { timestamp } : {}),
|
|
292
|
+
...(messageId ? { messageId } : {}),
|
|
293
|
+
...(media.length > 0 ? { media } : {}),
|
|
294
|
+
...(accountId ? { accountId } : {}),
|
|
295
|
+
...(sessionKey ? { sessionKey } : {}),
|
|
296
|
+
...(route ? { route } : {}),
|
|
297
|
+
...(type ? { type } : {}),
|
|
298
|
+
...(mediaUrl ? { mediaUrl } : {}),
|
|
299
|
+
...(createdAt > 0 ? { createdAt } : {}),
|
|
300
|
+
...(status === 'pushed' || status === 'acked'
|
|
301
|
+
? { status: status as BncrOutboundReplayStatus }
|
|
302
|
+
: {}),
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
if (entries.length > 0) {
|
|
306
|
+
outboundReplayCache.set(key, entries);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function dumpPersistedHistoryBuckets(
|
|
312
|
+
source: Map<string, BncrPersistedConversationHistoryEntry[]>,
|
|
313
|
+
resolveLimit: (key: string) => number,
|
|
314
|
+
) {
|
|
315
|
+
return Array.from(source.entries()).map(([key, entries]) => ({
|
|
316
|
+
key,
|
|
317
|
+
entries: Number.isFinite(resolveLimit(key)) ? entries.slice(-resolveLimit(key)) : entries,
|
|
318
|
+
}));
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function dumpPersistedConversationHistories() {
|
|
322
|
+
return dumpPersistedHistoryBuckets(runtime.conversationHistories, (key) =>
|
|
323
|
+
resolvePersistedConversationHistoryLimit(key),
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function dumpPersistedOutboundReplayCache() {
|
|
328
|
+
return Array.from(outboundReplayCache.entries()).map(([key, entries]) => ({
|
|
209
329
|
key,
|
|
210
|
-
entries
|
|
330
|
+
entries,
|
|
211
331
|
}));
|
|
212
332
|
}
|
|
213
333
|
|
|
@@ -358,7 +478,8 @@ export function createBncrStateStore(runtime: {
|
|
|
358
478
|
|
|
359
479
|
loadPersistedSessionRoutes(data.sessionRoutes);
|
|
360
480
|
loadPersistedSceneRegistry(data.sceneRegistry);
|
|
361
|
-
|
|
481
|
+
loadPersistedConversationHistories(data.conversationHistories ?? data.groupHistories);
|
|
482
|
+
loadPersistedOutboundReplayCache(data.outboundReplayCache ?? data.messageCache);
|
|
362
483
|
loadPersistedLastSessionMap(data.lastSessionByAccount);
|
|
363
484
|
loadPersistedAccountTimestampMap(runtime.lastActivityByAccount, data.lastActivityByAccount);
|
|
364
485
|
loadPersistedAccountTimestampMap(runtime.lastInboundByAccount, data.lastInboundByAccount);
|
|
@@ -377,7 +498,8 @@ export function createBncrStateStore(runtime: {
|
|
|
377
498
|
deadLetter: runtime.getDeadLetter().slice(-runtime.maxDeadLetterEntries),
|
|
378
499
|
sessionRoutes: dumpPersistedSessionRoutes(),
|
|
379
500
|
sceneRegistry: dumpPersistedSceneRegistry(),
|
|
380
|
-
|
|
501
|
+
conversationHistories: dumpPersistedConversationHistories(),
|
|
502
|
+
outboundReplayCache: dumpPersistedOutboundReplayCache(),
|
|
381
503
|
lastSessionByAccount: dumpPersistedLastSessionMap(),
|
|
382
504
|
lastActivityByAccount: dumpPersistedAccountTimestampMap(runtime.lastActivityByAccount),
|
|
383
505
|
lastInboundByAccount: dumpPersistedAccountTimestampMap(runtime.lastInboundByAccount),
|
|
@@ -397,8 +519,10 @@ export function createBncrStateStore(runtime: {
|
|
|
397
519
|
dumpPersistedSessionRoutes,
|
|
398
520
|
loadPersistedSceneRegistry,
|
|
399
521
|
dumpPersistedSceneRegistry,
|
|
400
|
-
|
|
401
|
-
|
|
522
|
+
loadPersistedConversationHistories,
|
|
523
|
+
dumpPersistedConversationHistories,
|
|
524
|
+
loadPersistedOutboundReplayCache,
|
|
525
|
+
dumpPersistedOutboundReplayCache,
|
|
402
526
|
backfillAccountActivityFromSessionRoutes,
|
|
403
527
|
loadState,
|
|
404
528
|
flushState,
|