@llblab/pi-telegram 0.11.1 → 0.12.0

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.
Files changed (45) hide show
  1. package/AGENTS.md +17 -12
  2. package/BACKLOG.md +0 -10
  3. package/CHANGELOG.md +33 -2
  4. package/README.md +42 -25
  5. package/api/inbound.ts +14 -0
  6. package/api/keyboard.ts +10 -0
  7. package/api/outbound.ts +11 -0
  8. package/api/sections.ts +17 -0
  9. package/api/updates.ts +11 -0
  10. package/api/voice.ts +24 -0
  11. package/docs/README.md +7 -5
  12. package/docs/architecture.md +176 -135
  13. package/docs/callback-namespaces.md +3 -3
  14. package/docs/command-templates.md +81 -24
  15. package/docs/{inbound-handlers.md → inbound.md} +13 -10
  16. package/docs/locks.md +3 -3
  17. package/docs/{outbound-handlers.md → outbound.md} +13 -10
  18. package/docs/public-api.md +266 -0
  19. package/docs/{extension-sections.md → sections.md} +31 -27
  20. package/docs/ui-style.md +165 -0
  21. package/docs/{external-handlers.md → updates.md} +33 -31
  22. package/docs/voice.md +17 -14
  23. package/index.ts +86 -261
  24. package/lib/bindings.ts +301 -0
  25. package/lib/command-templates.ts +163 -32
  26. package/lib/commands.ts +114 -1
  27. package/lib/config.ts +45 -4
  28. package/lib/{inbound-handlers.ts → inbound.ts} +5 -4
  29. package/lib/lifecycle.ts +122 -1
  30. package/lib/menu-model.ts +3 -3
  31. package/lib/menu-queue.ts +1 -1
  32. package/lib/menu-settings.ts +63 -32
  33. package/lib/menu-status.ts +1 -1
  34. package/lib/menu.ts +1 -1
  35. package/lib/{outbound-handlers.ts → outbound.ts} +21 -11
  36. package/lib/pi.ts +4 -0
  37. package/lib/polling.ts +4 -3
  38. package/lib/preview.ts +1 -1
  39. package/lib/routing.ts +45 -13
  40. package/lib/{extension-sections.ts → sections.ts} +37 -8
  41. package/lib/time-injection.ts +1 -1
  42. package/lib/updates.ts +121 -1
  43. package/lib/voice.ts +33 -14
  44. package/package.json +11 -1
  45. package/lib/external-handlers.ts +0 -166
package/lib/commands.ts CHANGED
@@ -307,6 +307,10 @@ export interface TelegramRuntimeEventRecorderPort {
307
307
  ) => void;
308
308
  }
309
309
 
310
+ export interface TelegramCompactConfirmationReplyMarkup {
311
+ inline_keyboard: { text: string; callback_data: string }[][];
312
+ }
313
+
310
314
  export interface TelegramCompactCommandDeps extends TelegramRuntimeEventRecorderPort {
311
315
  isIdle: () => boolean;
312
316
  hasPendingMessages: () => boolean;
@@ -327,6 +331,42 @@ export interface TelegramCompactCommandDeps extends TelegramRuntimeEventRecorder
327
331
  onError: (error: unknown) => void;
328
332
  }) => void;
329
333
  sendTextReply: (text: string) => Promise<void>;
334
+ suppressStartNotice?: boolean;
335
+ }
336
+
337
+ export interface TelegramCompactConfirmationDeps {
338
+ sendInteractiveMessage: (
339
+ chatId: number,
340
+ text: string,
341
+ mode: "html" | "plain",
342
+ replyMarkup: TelegramCompactConfirmationReplyMarkup,
343
+ ) => Promise<number | undefined>;
344
+ }
345
+
346
+ export interface TelegramCompactConfirmationCallbackQuery {
347
+ id: string;
348
+ data?: string;
349
+ message?: { chat?: { id?: number }; message_id?: number };
350
+ }
351
+
352
+ export interface TelegramCompactConfirmationCallbackDeps<TContext> {
353
+ ctx: TContext;
354
+ answerCallbackQuery: (
355
+ callbackQueryId: string,
356
+ text?: string,
357
+ ) => Promise<void>;
358
+ editInteractiveMessage: (
359
+ chatId: number,
360
+ messageId: number,
361
+ text: string,
362
+ mode: "html" | "plain",
363
+ replyMarkup: TelegramCompactConfirmationReplyMarkup,
364
+ ) => Promise<void>;
365
+ runCompact: (
366
+ ctx: TContext,
367
+ chatId: number,
368
+ replyToMessageId: number,
369
+ ) => Promise<void>;
330
370
  }
331
371
 
332
372
  export type TelegramControlCommandType =
@@ -580,6 +620,7 @@ export interface TelegramCommandRuntimeDeps<
580
620
  getPromptTemplateCommands?: () => readonly TelegramPromptTemplateMenuCommand[];
581
621
  persistConfig: () => Promise<void>;
582
622
  sendTextReply: (message: TMessage, text: string) => Promise<void>;
623
+ sendInteractiveMessage?: TelegramCompactConfirmationDeps["sendInteractiveMessage"];
583
624
  }
584
625
 
585
626
  export const TELEGRAM_APP_MENU_INTRO_HTML = [
@@ -784,6 +825,69 @@ function dispatchNextQueuedTelegramTurnAfterCompact(
784
825
  deps.dispatchNextQueuedTelegramTurn();
785
826
  }
786
827
 
828
+ export function buildTelegramCompactConfirmationReplyMarkup(): TelegramCompactConfirmationReplyMarkup {
829
+ return {
830
+ inline_keyboard: [
831
+ [
832
+ { text: "🗜 Yes, compact", callback_data: "compact:confirm" },
833
+ { text: "❌ No", callback_data: "compact:cancel" },
834
+ ],
835
+ ],
836
+ };
837
+ }
838
+
839
+ export function getTelegramCompactConfirmationHtml(): string {
840
+ return "<b>Compact session?</b>";
841
+ }
842
+
843
+ export async function openTelegramCompactConfirmation(
844
+ chatId: number,
845
+ deps: TelegramCompactConfirmationDeps,
846
+ ): Promise<void> {
847
+ await deps.sendInteractiveMessage(
848
+ chatId,
849
+ getTelegramCompactConfirmationHtml(),
850
+ "html",
851
+ buildTelegramCompactConfirmationReplyMarkup(),
852
+ );
853
+ }
854
+
855
+ export async function handleTelegramCompactConfirmationCallback<TContext>(
856
+ query: TelegramCompactConfirmationCallbackQuery,
857
+ deps: TelegramCompactConfirmationCallbackDeps<TContext>,
858
+ ): Promise<boolean> {
859
+ if (query.data !== "compact:confirm" && query.data !== "compact:cancel") {
860
+ return false;
861
+ }
862
+ const chatId = query.message?.chat?.id;
863
+ const messageId = query.message?.message_id;
864
+ if (typeof chatId !== "number" || typeof messageId !== "number") {
865
+ await deps.answerCallbackQuery(query.id, "Interactive message expired.");
866
+ return true;
867
+ }
868
+ if (query.data === "compact:cancel") {
869
+ await deps.editInteractiveMessage(
870
+ chatId,
871
+ messageId,
872
+ "Compaction cancelled.",
873
+ "plain",
874
+ { inline_keyboard: [] },
875
+ );
876
+ await deps.answerCallbackQuery(query.id);
877
+ return true;
878
+ }
879
+ await deps.editInteractiveMessage(
880
+ chatId,
881
+ messageId,
882
+ "Compaction started.",
883
+ "plain",
884
+ { inline_keyboard: [] },
885
+ );
886
+ await deps.answerCallbackQuery(query.id);
887
+ await deps.runCompact(deps.ctx, chatId, messageId);
888
+ return true;
889
+ }
890
+
787
891
  export async function handleTelegramCompactCommand(
788
892
  deps: TelegramCompactCommandDeps,
789
893
  ): Promise<void> {
@@ -834,7 +938,9 @@ export async function handleTelegramCompactCommand(
834
938
  await deps.sendTextReply(`Compaction failed: ${errorMessage}`);
835
939
  return;
836
940
  }
837
- await deps.sendTextReply("Compaction started.");
941
+ if (!deps.suppressStartNotice) {
942
+ await deps.sendTextReply("Compaction started.");
943
+ }
838
944
  if (compactionStillInProgress) deps.startTypingLoop?.();
839
945
  }
840
946
 
@@ -978,6 +1084,7 @@ export function createTelegramCommandHandlerTargetRuntime<
978
1084
  stopTypingLoop: deps.stopTypingLoop,
979
1085
  enqueueContinueTurn: deps.enqueueContinueTurn,
980
1086
  compact: deps.compact,
1087
+ sendInteractiveMessage: deps.sendInteractiveMessage,
981
1088
  enqueueControlItem: commandTargetRuntime.enqueueControlItem,
982
1089
  showStatus: commandTargetRuntime.showStatus,
983
1090
  openModelMenu: commandTargetRuntime.openModelMenu,
@@ -1110,6 +1217,12 @@ async function handleTelegramCommandRuntime<
1110
1217
  await deps.openQueueMenu(nextMessage, commandCtx);
1111
1218
  },
1112
1219
  handleCompact: async (nextMessage, commandCtx) => {
1220
+ if (deps.sendInteractiveMessage) {
1221
+ await openTelegramCompactConfirmation(nextMessage.chat.id, {
1222
+ sendInteractiveMessage: deps.sendInteractiveMessage,
1223
+ });
1224
+ return;
1225
+ }
1113
1226
  await handleTelegramCompactCommand({
1114
1227
  isIdle: () => deps.isIdle(commandCtx),
1115
1228
  hasPendingMessages: () => deps.hasPendingMessages(commandCtx),
package/lib/config.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Telegram bridge config and pairing helpers
3
3
  * Zones: telegram config, pairing, filesystem
4
- * Owns persisted bot/session pairing state, local config storage, authorization policy, and first-user pairing side effects
4
+ * Owns persisted bot/session pairing state, local config storage, live config controls, authorization policy, and first-user pairing side effects
5
5
  */
6
6
 
7
7
  import { existsSync } from "node:fs";
@@ -9,7 +9,7 @@ import { chmod, mkdir, readFile, rename, writeFile } from "node:fs/promises";
9
9
  import { homedir } from "node:os";
10
10
  import { join, resolve } from "node:path";
11
11
 
12
- import type { TelegramInboundHandlerConfig } from "./inbound-handlers.ts";
12
+ import type { TelegramInboundHandlerConfig } from "./inbound.ts";
13
13
  import type { CommandTemplateObjectConfig } from "./command-templates.ts";
14
14
 
15
15
  const CONFIG_RUNTIME_KEY = "__piTelegramConfigRuntime__";
@@ -35,7 +35,7 @@ export interface TelegramOutboundHandlerConfig extends CommandTemplateObjectConf
35
35
  timeout?: number;
36
36
  }
37
37
 
38
- export type TelegramTimeMode = "off" | "always" | "interval";
38
+ export type TelegramTimeMode = "hidden" | "always" | "interval";
39
39
 
40
40
  export interface TelegramTimeConfig {
41
41
  injectionMode?: TelegramTimeMode;
@@ -110,6 +110,22 @@ export function updateTelegramVoiceConfig(
110
110
  return true;
111
111
  }
112
112
 
113
+ export function bindGlobalTelegramConfigRuntime(
114
+ configStore: Pick<TelegramConfigStore, "get" | "set" | "persist">,
115
+ ): void {
116
+ setGlobalTelegramConfigRuntime({
117
+ updateVoiceConfig(voice) {
118
+ const current = configStore.get();
119
+ const next = {
120
+ ...current,
121
+ voice: { ...(current.voice ?? {}), ...voice },
122
+ };
123
+ configStore.set(next);
124
+ void configStore.persist(next);
125
+ },
126
+ });
127
+ }
128
+
113
129
  export async function readTelegramConfig(
114
130
  configPath: string,
115
131
  ): Promise<TelegramConfig> {
@@ -240,7 +256,7 @@ export function resolveTelegramTimeConfig(
240
256
  const injectionMode: TelegramTimeMode =
241
257
  raw?.injectionMode === "always" || raw?.injectionMode === "interval"
242
258
  ? raw.injectionMode
243
- : "off";
259
+ : "hidden";
244
260
  const interval =
245
261
  typeof raw?.interval === "number" && raw.interval > 0
246
262
  ? raw.interval
@@ -266,6 +282,16 @@ export function createTelegramTimeInjectionModeSetter(
266
282
  ): (injectionMode: TelegramTimeMode) => Promise<void> {
267
283
  return async (injectionMode) => {
268
284
  const current = configStore.get();
285
+ if (injectionMode === "hidden") {
286
+ const { injectionMode: _injectionMode, ...remainingTime } =
287
+ current.time ?? {};
288
+ const next = { ...current };
289
+ if (Object.keys(remainingTime).length > 0) next.time = remainingTime;
290
+ else delete next.time;
291
+ configStore.set(next);
292
+ await configStore.persist(next);
293
+ return;
294
+ }
269
295
  const next = {
270
296
  ...current,
271
297
  time: { ...(current.time ?? {}), injectionMode },
@@ -282,6 +308,21 @@ export function createTelegramProactivePushChatIdGetter(deps: {
282
308
  return () => deps.getActiveTurnChatId() ?? deps.getAllowedUserId();
283
309
  }
284
310
 
311
+ export function createTelegramConfigControls(
312
+ configStore: Pick<TelegramConfigStore, "get" | "set" | "persist">,
313
+ ) {
314
+ return {
315
+ isProactivePushEnabled: createTelegramProactivePushChecker(configStore),
316
+ setProactivePushEnabled: createTelegramProactivePushSetter(configStore),
317
+ getVoiceReplyMode: createTelegramVoiceReplyModeGetter(configStore),
318
+ isVoiceReplyModeConfigured:
319
+ createTelegramVoiceReplyModeConfiguredChecker(configStore),
320
+ setVoiceReplyMode: createTelegramVoiceReplyModeSetter(configStore),
321
+ getTimeInjectionMode: createTelegramTimeInjectionModeGetter(configStore),
322
+ setTimeInjectionMode: createTelegramTimeInjectionModeSetter(configStore),
323
+ };
324
+ }
325
+
285
326
  export type TelegramAuthorizationState =
286
327
  | { kind: "pair"; userId: number }
287
328
  | { kind: "allow" }
@@ -216,9 +216,7 @@ function matchesWildcard(pattern: string, value: string | undefined): boolean {
216
216
  return new RegExp(`^${escaped}$`).test(normalizedValue);
217
217
  }
218
218
 
219
- function handlerHasSelectors(
220
- handler: TelegramInboundHandlerConfig,
221
- ): boolean {
219
+ function handlerHasSelectors(handler: TelegramInboundHandlerConfig): boolean {
222
220
  return (
223
221
  normalizeStringList(handler.match).length > 0 ||
224
222
  normalizeStringList(handler.mime).length > 0 ||
@@ -642,7 +640,10 @@ async function readBuiltInTelegramTextAttachment(
642
640
  if (!isTelegramTextMimeType(file.mimeType)) return undefined;
643
641
  const content = await readFile(file.path, "utf8");
644
642
  const normalized = content.trim();
645
- if (!normalized || Buffer.byteLength(normalized, "utf8") > BUILT_IN_TEXT_ATTACHMENT_MAX_BYTES) {
643
+ if (
644
+ !normalized ||
645
+ Buffer.byteLength(normalized, "utf8") > BUILT_IN_TEXT_ATTACHMENT_MAX_BYTES
646
+ ) {
646
647
  return undefined;
647
648
  }
648
649
  const name = file.fileName || basename(file.path);
package/lib/lifecycle.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Telegram lifecycle hook registration helpers
3
3
  * Zones: pi agent lifecycle, telegram session
4
- * Owns binding prepared Telegram lifecycle runtimes to pi extension lifecycle events
4
+ * Binds prepared Telegram lifecycle runtimes to pi extension lifecycle events
5
5
  */
6
6
 
7
7
  import type {
@@ -10,6 +10,8 @@ import type {
10
10
  BeforeAgentStartEvent,
11
11
  ExtensionAPI,
12
12
  ExtensionContext,
13
+ SessionBeforeCompactEvent,
14
+ SessionCompactEvent,
13
15
  SessionShutdownEvent,
14
16
  SessionStartEvent,
15
17
  } from "./pi.ts";
@@ -50,6 +52,14 @@ export interface TelegramLifecycleRegistrationDeps {
50
52
  event: SessionShutdownEvent,
51
53
  ctx: ExtensionContext,
52
54
  ) => Promise<void>;
55
+ onSessionBeforeCompact?: (
56
+ event: SessionBeforeCompactEvent,
57
+ ctx: ExtensionContext,
58
+ ) => Promise<void> | void;
59
+ onSessionCompact?: (
60
+ event: SessionCompactEvent,
61
+ ctx: ExtensionContext,
62
+ ) => Promise<void> | void;
53
63
  onBeforeAgentStart: (
54
64
  event: BeforeAgentStartEvent,
55
65
  ctx: ExtensionContext,
@@ -92,6 +102,111 @@ export interface TelegramSessionLifecycleHooks {
92
102
  ) => Promise<void>;
93
103
  }
94
104
 
105
+ type TelegramLifecycleTimer = number | ReturnType<typeof setTimeout>;
106
+
107
+ export interface TelegramCompactionObserverRuntimeDeps<TContext> {
108
+ setCompactionInProgress: (inProgress: boolean) => void;
109
+ updateStatus: (ctx: TContext) => void;
110
+ startTypingLoop?: (ctx: TContext) => void;
111
+ stopTypingLoop?: () => void;
112
+ requestDeferredDispatchNextQueuedTelegramTurn: (
113
+ dispatch: (ctx: TContext) => void,
114
+ ) => void;
115
+ dispatchNextQueuedTelegramTurn: (ctx: TContext) => void;
116
+ recordRuntimeEvent?: (category: string, error: unknown) => void;
117
+ timeoutMs?: number;
118
+ setTimer?: (callback: () => void, ms: number) => TelegramLifecycleTimer;
119
+ clearTimer?: (timer: TelegramLifecycleTimer) => void;
120
+ }
121
+
122
+ export interface TelegramCompactionObserverRuntime<TContext> {
123
+ onSessionBeforeCompact: (
124
+ event: SessionBeforeCompactEvent,
125
+ ctx: TContext,
126
+ ) => void;
127
+ onSessionCompact: (event: SessionCompactEvent, ctx: TContext) => void;
128
+ onSessionShutdown: () => void;
129
+ }
130
+
131
+ export function createTelegramCompactionObserverRuntime<TContext>(
132
+ deps: TelegramCompactionObserverRuntimeDeps<TContext>,
133
+ ): TelegramCompactionObserverRuntime<TContext> {
134
+ const timeoutMs = deps.timeoutMs ?? 300_000;
135
+ const setTimer = deps.setTimer ?? setTimeout;
136
+ const clearTimer = deps.clearTimer ?? clearTimeout;
137
+ let fallbackTimer: TelegramLifecycleTimer | undefined;
138
+ const clearFallbackTimer = (): void => {
139
+ if (!fallbackTimer) return;
140
+ clearTimer(fallbackTimer);
141
+ fallbackTimer = undefined;
142
+ };
143
+ const requestDispatch = (): void => {
144
+ deps.requestDeferredDispatchNextQueuedTelegramTurn(
145
+ deps.dispatchNextQueuedTelegramTurn,
146
+ );
147
+ };
148
+ return {
149
+ onSessionBeforeCompact: (_event, ctx) => {
150
+ deps.setCompactionInProgress(true);
151
+ deps.startTypingLoop?.(ctx);
152
+ deps.updateStatus(ctx);
153
+ clearFallbackTimer();
154
+ fallbackTimer = setTimer(() => {
155
+ fallbackTimer = undefined;
156
+ deps.setCompactionInProgress(false);
157
+ deps.stopTypingLoop?.();
158
+ deps.updateStatus(ctx);
159
+ deps.recordRuntimeEvent?.(
160
+ "compact",
161
+ new Error("Compaction observer timed out"),
162
+ );
163
+ requestDispatch();
164
+ }, timeoutMs);
165
+ },
166
+ onSessionCompact: (_event, ctx) => {
167
+ clearFallbackTimer();
168
+ deps.setCompactionInProgress(false);
169
+ deps.stopTypingLoop?.();
170
+ deps.updateStatus(ctx);
171
+ requestDispatch();
172
+ },
173
+ onSessionShutdown: () => {
174
+ clearFallbackTimer();
175
+ deps.stopTypingLoop?.();
176
+ },
177
+ };
178
+ }
179
+
180
+ export interface TelegramMessageActivityTypingDeps<TContext> {
181
+ hasActiveTurn: () => boolean;
182
+ startTypingLoop: (ctx: TContext) => void;
183
+ onMessageStart: TelegramLifecycleRegistrationDeps["onMessageStart"];
184
+ onMessageUpdate: TelegramLifecycleRegistrationDeps["onMessageUpdate"];
185
+ }
186
+
187
+ export function createTelegramMessageActivityTypingHooks<
188
+ TContext extends ExtensionContext,
189
+ >(
190
+ deps: TelegramMessageActivityTypingDeps<TContext>,
191
+ ): Pick<
192
+ TelegramLifecycleRegistrationDeps,
193
+ "onMessageStart" | "onMessageUpdate"
194
+ > {
195
+ const ensureTyping = (ctx: TContext): void => {
196
+ if (deps.hasActiveTurn()) deps.startTypingLoop(ctx);
197
+ };
198
+ return {
199
+ onMessageStart: async (event, ctx) => {
200
+ ensureTyping(ctx as TContext);
201
+ await deps.onMessageStart(event, ctx);
202
+ },
203
+ onMessageUpdate: async (event, ctx) => {
204
+ ensureTyping(ctx as TContext);
205
+ await deps.onMessageUpdate(event, ctx);
206
+ },
207
+ };
208
+ }
209
+
95
210
  export function createDedupAgentStartHook(
96
211
  dedup: { reset(): void },
97
212
  inner: (event: AgentStartEvent, ctx: ExtensionContext) => Promise<void>,
@@ -139,6 +254,12 @@ export function registerTelegramLifecycleHooks(
139
254
  pi.on("session_shutdown", async (event, ctx) => {
140
255
  await deps.onSessionShutdown(event, ctx);
141
256
  });
257
+ pi.on("session_before_compact", async (event, ctx) => {
258
+ await deps.onSessionBeforeCompact?.(event, ctx);
259
+ });
260
+ pi.on("session_compact", async (event, ctx) => {
261
+ await deps.onSessionCompact?.(event, ctx);
262
+ });
142
263
  pi.on("before_agent_start", async (event, ctx) => {
143
264
  return deps.onBeforeAgentStart(event, ctx);
144
265
  });
package/lib/menu-model.ts CHANGED
@@ -983,7 +983,7 @@ export function buildModelMenuReplyMarkup(
983
983
  callback_data: "model:scope:scoped",
984
984
  },
985
985
  {
986
- text: state.scope === "all" ? "🟡 All" : "⚫️ All",
986
+ text: state.scope === "all" ? "🟣 All" : "⚫️ All",
987
987
  callback_data: "model:scope:all",
988
988
  },
989
989
  ]);
@@ -1050,7 +1050,7 @@ export function buildModelDetailMenuReplyMarkup(
1050
1050
  callback_data: "model:scope-enable",
1051
1051
  },
1052
1052
  {
1053
- text: scoped ? "⚫️ All" : "🟡 All",
1053
+ text: scoped ? "⚫️ All" : "🟣 All",
1054
1054
  callback_data: "model:scope-disable",
1055
1055
  },
1056
1056
  ],
@@ -1091,7 +1091,7 @@ export function buildModelPageMenuReplyMarkup(
1091
1091
  return {
1092
1092
  text:
1093
1093
  pageIndex === menuPage.page
1094
- ? `🟢 ${pageIndex + 1}`
1094
+ ? `🟣 ${pageIndex + 1}`
1095
1095
  : String(pageIndex + 1),
1096
1096
  callback_data: `model:page:${pageIndex}`,
1097
1097
  };
package/lib/menu-queue.ts CHANGED
@@ -162,7 +162,7 @@ function buildTelegramQueueItemSubmenuReplyMarkup(
162
162
  callback_data: `queue:prio-set:${chatId}:${replyToMessageId}:priority`,
163
163
  },
164
164
  {
165
- text: isPriority ? "⚫️ Normal" : "🟡 Normal",
165
+ text: isPriority ? "⚫️ Normal" : "🟣 Normal",
166
166
  callback_data: `queue:prio-set:${chatId}:${replyToMessageId}:normal`,
167
167
  },
168
168
  ],