@xmoxmo/bncr 0.4.6 → 0.4.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/channel.ts +41 -2
- package/src/core/targets.ts +106 -17
- package/src/messaging/inbound/commands.ts +263 -51
- package/src/messaging/inbound/context-facts.ts +126 -14
- package/src/messaging/inbound/contracts.ts +24 -0
- package/src/messaging/inbound/dispatch-prep.ts +214 -39
- package/src/messaging/inbound/dispatch.ts +71 -5
- package/src/messaging/inbound/gate.ts +56 -86
- package/src/messaging/inbound/group-history.ts +189 -0
- package/src/messaging/inbound/native-command-runtime.ts +77 -61
- package/src/messaging/inbound/native-command.ts +92 -8
- package/src/messaging/inbound/parse.ts +113 -8
- package/src/messaging/inbound/reply-dispatch-serial.ts +62 -0
- package/src/messaging/inbound/reply-dispatch.ts +252 -77
- package/src/messaging/inbound/scene-admin.ts +269 -0
- package/src/messaging/inbound/session-label.ts +122 -13
- package/src/messaging/inbound/session-meta-task.ts +17 -0
- package/src/messaging/inbound/turn-context.ts +184 -71
- package/src/openclaw/channel-runtime-contracts.ts +1 -0
- package/src/plugin/channel-components.ts +34 -1
- package/src/plugin/channel-inbound-helpers.ts +9 -2
- package/src/plugin/channel-runtime-builders-delivery.ts +24 -1
- package/src/plugin/channel-runtime-types.ts +42 -0
- package/src/plugin/file-inbound-init.ts +27 -12
- package/src/plugin/file-inbound-runtime.ts +2 -0
- package/src/plugin/inbound-acceptance.ts +82 -1
- package/src/plugin/inbound-handlers.ts +55 -2
- package/src/plugin/inbound-surface-handlers-group.ts +16 -0
- package/src/plugin/messaging.ts +22 -5
- package/src/plugin/scene-registry.ts +155 -0
- package/src/plugin/state-store.ts +133 -0
- package/src/plugin/state-transient-runtime-group.ts +5 -0
- package/src/plugin/target-runtime.ts +2 -2
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { emitBncrLogLine } from '../../core/logging.ts';
|
|
2
|
+
|
|
3
|
+
const bncrReplyDispatchChains = new Map<string, Promise<void>>();
|
|
4
|
+
|
|
5
|
+
export async function runBncrReplyDispatchSerial<T>(
|
|
6
|
+
sessionKey: string,
|
|
7
|
+
task: () => Promise<T>,
|
|
8
|
+
meta?: { msgId?: string | null; to?: string | null; debugEnabled?: boolean },
|
|
9
|
+
) {
|
|
10
|
+
const key = String(sessionKey || '').trim();
|
|
11
|
+
if (!key) return await task();
|
|
12
|
+
|
|
13
|
+
const metaSuffix = `|msgId=${String(meta?.msgId || '-')}|to=${String(meta?.to || '-')}`;
|
|
14
|
+
|
|
15
|
+
const previous = bncrReplyDispatchChains.get(key) || Promise.resolve();
|
|
16
|
+
let release!: () => void;
|
|
17
|
+
const current = new Promise<void>((resolve) => {
|
|
18
|
+
release = resolve;
|
|
19
|
+
});
|
|
20
|
+
const chain = previous.then(() => current);
|
|
21
|
+
bncrReplyDispatchChains.set(key, chain);
|
|
22
|
+
|
|
23
|
+
const debugGate = () => meta?.debugEnabled === true;
|
|
24
|
+
|
|
25
|
+
emitBncrLogLine(
|
|
26
|
+
'info',
|
|
27
|
+
`[bncr] reply-serial queued|sessionKey=${key}${metaSuffix}`,
|
|
28
|
+
{ debugOnly: true },
|
|
29
|
+
debugGate,
|
|
30
|
+
);
|
|
31
|
+
await previous;
|
|
32
|
+
emitBncrLogLine(
|
|
33
|
+
'info',
|
|
34
|
+
`[bncr] reply-serial acquired|sessionKey=${key}${metaSuffix}`,
|
|
35
|
+
{ debugOnly: true },
|
|
36
|
+
debugGate,
|
|
37
|
+
);
|
|
38
|
+
try {
|
|
39
|
+
return await task();
|
|
40
|
+
} finally {
|
|
41
|
+
emitBncrLogLine(
|
|
42
|
+
'info',
|
|
43
|
+
`[bncr] reply-serial releasing|sessionKey=${key}${metaSuffix}`,
|
|
44
|
+
{ debugOnly: true },
|
|
45
|
+
debugGate,
|
|
46
|
+
);
|
|
47
|
+
release();
|
|
48
|
+
if (bncrReplyDispatchChains.get(key) === chain) {
|
|
49
|
+
bncrReplyDispatchChains.delete(key);
|
|
50
|
+
}
|
|
51
|
+
emitBncrLogLine(
|
|
52
|
+
'info',
|
|
53
|
+
`[bncr] reply-serial released|sessionKey=${key}${metaSuffix}`,
|
|
54
|
+
{ debugOnly: true },
|
|
55
|
+
debugGate,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function resetBncrReplyDispatchSerialForTest() {
|
|
61
|
+
bncrReplyDispatchChains.clear();
|
|
62
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { emitBncrLogLine } from '../../core/logging.ts';
|
|
2
2
|
import { resolveBncrChannelPolicy } from '../../core/policy.ts';
|
|
3
3
|
import {
|
|
4
|
+
readBncrSessionUpdatedAt,
|
|
4
5
|
recordBncrInboundSession,
|
|
5
6
|
resolveBncrPinnedMainDmOwnerFromAllowlist,
|
|
6
7
|
} from '../../openclaw/inbound-session-runtime.ts';
|
|
@@ -17,9 +18,97 @@ import type {
|
|
|
17
18
|
ParsedInbound,
|
|
18
19
|
} from './dispatch-prep.ts';
|
|
19
20
|
import { buildBncrInboundRecordUpdateLastRoute } from './last-route.ts';
|
|
21
|
+
import { parseBncrNativeCommand } from './native-command.ts';
|
|
20
22
|
import { buildBncrReplyConfig } from './reply-config.ts';
|
|
23
|
+
import { runBncrReplyDispatchSerial } from './reply-dispatch-serial.ts';
|
|
21
24
|
import { resolveBncrChannelInboundRuntime } from './runtime-compat.ts';
|
|
22
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
buildBncrInboundSessionIdentityPatch,
|
|
27
|
+
correctBncrInboundSessionLabel,
|
|
28
|
+
wrapBncrInboundRecordSessionLabelCorrection,
|
|
29
|
+
} from './session-label.ts';
|
|
30
|
+
import { createBncrSessionMetaTaskBarrier } from './session-meta-task.ts';
|
|
31
|
+
|
|
32
|
+
function mergeBncrCommandOwnerAllowFrom(args: {
|
|
33
|
+
cfg: BncrInboundConfig;
|
|
34
|
+
parsed: ParsedInbound;
|
|
35
|
+
isBncrNativeCommand: boolean;
|
|
36
|
+
senderIdForContext: string;
|
|
37
|
+
}) {
|
|
38
|
+
const { cfg, parsed, isBncrNativeCommand, senderIdForContext } = args;
|
|
39
|
+
if (parsed.isAdmin !== true || isBncrNativeCommand) return cfg;
|
|
40
|
+
const senderId = String(senderIdForContext || '').trim();
|
|
41
|
+
if (!senderId) return cfg;
|
|
42
|
+
|
|
43
|
+
const currentCommands = (cfg.commands || {}) as { ownerAllowFrom?: string[] };
|
|
44
|
+
const currentOwnerAllowFrom = Array.isArray(currentCommands.ownerAllowFrom)
|
|
45
|
+
? currentCommands.ownerAllowFrom
|
|
46
|
+
: [];
|
|
47
|
+
if (currentOwnerAllowFrom.includes(senderId)) return cfg;
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
...cfg,
|
|
51
|
+
commands: {
|
|
52
|
+
...currentCommands,
|
|
53
|
+
ownerAllowFrom: [...currentOwnerAllowFrom, senderId],
|
|
54
|
+
},
|
|
55
|
+
} satisfies BncrInboundConfig;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function sleep(ms: number) {
|
|
59
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function waitForBncrReplySessionQuiescence(args: {
|
|
63
|
+
api: BncrInboundApi;
|
|
64
|
+
storePath: string;
|
|
65
|
+
sessionKey: string;
|
|
66
|
+
msgId?: string | null;
|
|
67
|
+
to: string;
|
|
68
|
+
debugEnabled?: boolean;
|
|
69
|
+
}) {
|
|
70
|
+
const settleWindowMs = 120;
|
|
71
|
+
const pollIntervalMs = 40;
|
|
72
|
+
const maxWaitMs = 1500;
|
|
73
|
+
const startedAt = Date.now();
|
|
74
|
+
const debugGate = () => args.debugEnabled === true;
|
|
75
|
+
let lastUpdatedAt = null as number | null;
|
|
76
|
+
let stableSince = Date.now();
|
|
77
|
+
|
|
78
|
+
const readUpdatedAt = async () => {
|
|
79
|
+
try {
|
|
80
|
+
const updatedAt = await readBncrSessionUpdatedAt(args.api, {
|
|
81
|
+
storePath: args.storePath,
|
|
82
|
+
sessionKey: args.sessionKey,
|
|
83
|
+
});
|
|
84
|
+
return typeof updatedAt === 'number' && Number.isFinite(updatedAt) ? updatedAt : null;
|
|
85
|
+
} catch {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
lastUpdatedAt = await readUpdatedAt();
|
|
91
|
+
|
|
92
|
+
while (Date.now() - startedAt < maxWaitMs) {
|
|
93
|
+
await sleep(pollIntervalMs);
|
|
94
|
+
const currentUpdatedAt = await readUpdatedAt();
|
|
95
|
+
if (currentUpdatedAt !== lastUpdatedAt) {
|
|
96
|
+
lastUpdatedAt = currentUpdatedAt;
|
|
97
|
+
stableSince = Date.now();
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (Date.now() - stableSince >= settleWindowMs) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
emitBncrLogLine(
|
|
106
|
+
'warn',
|
|
107
|
+
`[bncr] reply-dispatch settle timeout|sessionKey=${args.sessionKey}|msgId=${args.msgId || '-'}|to=${args.to}|waitMs=${Date.now() - startedAt}`,
|
|
108
|
+
{ debugOnly: true },
|
|
109
|
+
debugGate,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
23
112
|
|
|
24
113
|
export async function runBncrInboundReplyDispatch(args: {
|
|
25
114
|
api: BncrInboundApi;
|
|
@@ -34,6 +123,8 @@ export async function runBncrInboundReplyDispatch(args: {
|
|
|
34
123
|
resolution: BncrInboundConversationResolution;
|
|
35
124
|
replyRouteFact: BncrInboundReplyRouteFact;
|
|
36
125
|
senderIdForContext: string;
|
|
126
|
+
senderDisplayName: string;
|
|
127
|
+
shouldDispatch: boolean;
|
|
37
128
|
setInboundActivity: (accountId: string, at: number) => void;
|
|
38
129
|
scheduleSave: () => void;
|
|
39
130
|
enqueueFromReply: BncrEnqueueFromReply;
|
|
@@ -51,12 +142,20 @@ export async function runBncrInboundReplyDispatch(args: {
|
|
|
51
142
|
resolution,
|
|
52
143
|
replyRouteFact,
|
|
53
144
|
senderIdForContext,
|
|
145
|
+
shouldDispatch,
|
|
54
146
|
setInboundActivity,
|
|
55
147
|
scheduleSave,
|
|
56
148
|
enqueueFromReply,
|
|
57
149
|
} = args;
|
|
58
150
|
|
|
59
151
|
const effectiveReply = buildBncrReplyConfig(cfg);
|
|
152
|
+
const sessionIdentityPatch = buildBncrInboundSessionIdentityPatch({
|
|
153
|
+
channelId,
|
|
154
|
+
accountId: resolution.accountId,
|
|
155
|
+
chatType: resolution.chatType,
|
|
156
|
+
displayTo: resolution.canonicalTo,
|
|
157
|
+
senderId: senderIdForContext,
|
|
158
|
+
});
|
|
60
159
|
const channelPolicy = resolveBncrChannelPolicy(cfg?.channels?.bncr || {});
|
|
61
160
|
const pinnedMainDmOwner =
|
|
62
161
|
peer.kind === 'direct'
|
|
@@ -76,87 +175,163 @@ export async function runBncrInboundReplyDispatch(args: {
|
|
|
76
175
|
sessionKey: resolution.dispatchSessionKey,
|
|
77
176
|
pinnedMainDmOwner,
|
|
78
177
|
});
|
|
178
|
+
const isBncrNativeCommand =
|
|
179
|
+
parseBncrNativeCommand(rawBody, {
|
|
180
|
+
allowBareWhoami: parsed.isAdmin !== true,
|
|
181
|
+
}) !== null;
|
|
182
|
+
const commandDispatchCfg = mergeBncrCommandOwnerAllowFrom({
|
|
183
|
+
cfg,
|
|
184
|
+
parsed,
|
|
185
|
+
isBncrNativeCommand,
|
|
186
|
+
senderIdForContext,
|
|
187
|
+
});
|
|
79
188
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
189
|
+
if (!shouldDispatch) {
|
|
190
|
+
await wrapBncrInboundRecordSessionLabelCorrection({
|
|
191
|
+
recordInboundSession: recordBncrInboundSession as (
|
|
192
|
+
...args: unknown[]
|
|
193
|
+
) => Promise<unknown> | unknown,
|
|
194
|
+
expectedPatch: sessionIdentityPatch,
|
|
195
|
+
})({
|
|
196
|
+
storePath,
|
|
197
|
+
sessionKey: resolution.resolvedRoute.sessionKey,
|
|
198
|
+
ctx: ctxPayload,
|
|
199
|
+
updateLastRoute,
|
|
200
|
+
onRecordError: (err: unknown) => {
|
|
201
|
+
emitBncrLogLine('warn', `[bncr] inbound record session failed: ${String(err)}`);
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
const inboundAt = Date.now();
|
|
206
|
+
setInboundActivity(resolution.accountId, inboundAt);
|
|
207
|
+
scheduleSave();
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
await runBncrReplyDispatchSerial(
|
|
212
|
+
resolution.dispatchSessionKey,
|
|
213
|
+
async () =>
|
|
214
|
+
resolveBncrChannelInboundRuntime(api).run({
|
|
94
215
|
channel: channelId,
|
|
95
216
|
accountId: resolution.accountId,
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
ctx: ctxPayload,
|
|
114
|
-
cfg: effectiveReply.replyCfg,
|
|
115
|
-
dispatcherOptions: {
|
|
116
|
-
deliver: async (
|
|
117
|
-
payload: {
|
|
118
|
-
text?: string;
|
|
119
|
-
mediaUrl?: string;
|
|
120
|
-
mediaUrls?: string[];
|
|
121
|
-
audioAsVoice?: boolean;
|
|
122
|
-
},
|
|
123
|
-
info?: { kind?: 'tool' | 'block' | 'final' },
|
|
124
|
-
) => {
|
|
125
|
-
const kind = info?.kind;
|
|
126
|
-
const shouldForwardTool = effectiveReply.blockStreaming && effectiveReply.allowTool;
|
|
127
|
-
|
|
128
|
-
if (kind === 'tool' && !shouldForwardTool) {
|
|
129
|
-
return;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
await enqueueFromReply({
|
|
133
|
-
accountId: replyRouteFact.accountId,
|
|
134
|
-
sessionKey: replyRouteFact.sessionKey,
|
|
135
|
-
route: replyRouteFact.route,
|
|
136
|
-
payload: {
|
|
137
|
-
text: payload.text,
|
|
138
|
-
mediaUrl: payload.mediaUrl,
|
|
139
|
-
mediaUrls: payload.mediaUrls,
|
|
140
|
-
kind: kind || 'final',
|
|
141
|
-
replyToId: msgId || undefined,
|
|
217
|
+
raw: parsed,
|
|
218
|
+
adapter: {
|
|
219
|
+
ingest: () => ({
|
|
220
|
+
id: msgId ?? `${resolution.canonicalTo}:${Date.now()}`,
|
|
221
|
+
timestamp: Date.now(),
|
|
222
|
+
rawText: rawBody,
|
|
223
|
+
textForAgent: ctxPayload.BodyForAgent,
|
|
224
|
+
textForCommands: ctxPayload.CommandBody,
|
|
225
|
+
raw: parsed,
|
|
226
|
+
}),
|
|
227
|
+
preflight: () =>
|
|
228
|
+
shouldDispatch
|
|
229
|
+
? undefined
|
|
230
|
+
: {
|
|
231
|
+
admission: {
|
|
232
|
+
kind: 'observeOnly' as const,
|
|
233
|
+
reason: 'bncr-group-mode-no-reply',
|
|
142
234
|
},
|
|
143
|
-
}
|
|
235
|
+
},
|
|
236
|
+
resolveTurn: () => {
|
|
237
|
+
const sessionMetaBarrier = createBncrSessionMetaTaskBarrier();
|
|
238
|
+
return {
|
|
239
|
+
channel: channelId,
|
|
240
|
+
accountId: resolution.accountId,
|
|
241
|
+
routeSessionKey: resolution.resolvedRoute.sessionKey,
|
|
242
|
+
storePath,
|
|
243
|
+
ctxPayload,
|
|
244
|
+
recordInboundSession: wrapBncrInboundRecordSessionLabelCorrection({
|
|
245
|
+
recordInboundSession: recordBncrInboundSession as (
|
|
246
|
+
...args: unknown[]
|
|
247
|
+
) => Promise<unknown> | unknown,
|
|
248
|
+
expectedPatch: sessionIdentityPatch,
|
|
249
|
+
}),
|
|
250
|
+
record: {
|
|
251
|
+
updateLastRoute,
|
|
252
|
+
onRecordError: (err: unknown) => {
|
|
253
|
+
emitBncrLogLine('warn', `[bncr] inbound record session failed: ${String(err)}`);
|
|
254
|
+
},
|
|
255
|
+
trackSessionMetaTask: (task: Promise<unknown>) => {
|
|
256
|
+
sessionMetaBarrier.track(task);
|
|
257
|
+
},
|
|
144
258
|
},
|
|
145
|
-
|
|
146
|
-
|
|
259
|
+
runDispatch: async () => {
|
|
260
|
+
await sessionMetaBarrier.wait();
|
|
261
|
+
await correctBncrInboundSessionLabel({
|
|
262
|
+
storePath,
|
|
263
|
+
sessionKey: resolution.dispatchSessionKey,
|
|
264
|
+
expectedPatch: sessionIdentityPatch,
|
|
265
|
+
});
|
|
266
|
+
return Promise.resolve(
|
|
267
|
+
dispatchOpenClawReplyWithBufferedBlockDispatcher(api, {
|
|
268
|
+
ctx: ctxPayload,
|
|
269
|
+
cfg: buildBncrReplyConfig(commandDispatchCfg).replyCfg,
|
|
270
|
+
dispatcherOptions: {
|
|
271
|
+
deliver: async (
|
|
272
|
+
payload: {
|
|
273
|
+
text?: string;
|
|
274
|
+
mediaUrl?: string;
|
|
275
|
+
mediaUrls?: string[];
|
|
276
|
+
audioAsVoice?: boolean;
|
|
277
|
+
},
|
|
278
|
+
info?: { kind?: 'tool' | 'block' | 'final' },
|
|
279
|
+
) => {
|
|
280
|
+
const kind = info?.kind;
|
|
281
|
+
const shouldForwardTool =
|
|
282
|
+
effectiveReply.blockStreaming && effectiveReply.allowTool;
|
|
283
|
+
|
|
284
|
+
if (kind === 'tool' && !shouldForwardTool) {
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
await enqueueFromReply({
|
|
289
|
+
accountId: replyRouteFact.accountId,
|
|
290
|
+
sessionKey: replyRouteFact.sessionKey,
|
|
291
|
+
route: replyRouteFact.route,
|
|
292
|
+
payload: {
|
|
293
|
+
text: payload.text,
|
|
294
|
+
mediaUrl: payload.mediaUrl,
|
|
295
|
+
mediaUrls: payload.mediaUrls,
|
|
296
|
+
kind: kind || 'final',
|
|
297
|
+
replyToId: msgId || undefined,
|
|
298
|
+
},
|
|
299
|
+
});
|
|
300
|
+
},
|
|
301
|
+
onError: (err: unknown) => {
|
|
302
|
+
emitBncrLogLine('error', `[bncr] outbound reply failed: ${String(err)}`);
|
|
303
|
+
},
|
|
304
|
+
},
|
|
305
|
+
replyOptions: {
|
|
306
|
+
disableBlockStreaming: !effectiveReply.blockStreaming,
|
|
307
|
+
shouldEmitToolResult: effectiveReply.allowTool ? () => true : undefined,
|
|
308
|
+
},
|
|
309
|
+
}),
|
|
310
|
+
).finally(async () => {
|
|
311
|
+
await waitForBncrReplySessionQuiescence({
|
|
312
|
+
api,
|
|
313
|
+
storePath,
|
|
314
|
+
sessionKey: resolution.dispatchSessionKey,
|
|
315
|
+
msgId,
|
|
316
|
+
to: resolution.canonicalTo,
|
|
317
|
+
debugEnabled: cfg?.channels?.bncr?.debug?.verbose === true,
|
|
318
|
+
});
|
|
319
|
+
await correctBncrInboundSessionLabel({
|
|
320
|
+
storePath,
|
|
321
|
+
sessionKey: resolution.dispatchSessionKey,
|
|
322
|
+
expectedPatch: sessionIdentityPatch,
|
|
323
|
+
});
|
|
324
|
+
});
|
|
147
325
|
},
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
326
|
+
};
|
|
327
|
+
},
|
|
328
|
+
onFinalize: () => {
|
|
329
|
+
const inboundAt = Date.now();
|
|
330
|
+
setInboundActivity(resolution.accountId, inboundAt);
|
|
331
|
+
scheduleSave();
|
|
332
|
+
},
|
|
333
|
+
},
|
|
154
334
|
}),
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
setInboundActivity(resolution.accountId, inboundAt);
|
|
158
|
-
scheduleSave();
|
|
159
|
-
},
|
|
160
|
-
},
|
|
161
|
-
});
|
|
335
|
+
{ msgId, to: resolution.canonicalTo },
|
|
336
|
+
);
|
|
162
337
|
}
|