@llblab/pi-kit 0.15.0 → 0.16.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 (106) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +2 -2
  3. package/node_modules/@llblab/pi-state-flow/AGENTS.md +9 -9
  4. package/node_modules/@llblab/pi-state-flow/BACKLOG.md +0 -13
  5. package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +15 -0
  6. package/node_modules/@llblab/pi-state-flow/README.md +7 -6
  7. package/node_modules/@llblab/pi-state-flow/dist/lib/config.d.ts +5 -2
  8. package/node_modules/@llblab/pi-state-flow/dist/lib/config.js +17 -16
  9. package/node_modules/@llblab/pi-state-flow/dist/lib/context.d.ts +8 -0
  10. package/node_modules/@llblab/pi-state-flow/dist/lib/context.js +24 -0
  11. package/node_modules/@llblab/pi-state-flow/dist/lib/continuation.js +4 -3
  12. package/node_modules/@llblab/pi-state-flow/dist/lib/durable.d.ts +1 -0
  13. package/node_modules/@llblab/pi-state-flow/dist/lib/durable.js +8 -3
  14. package/node_modules/@llblab/pi-state-flow/dist/lib/episode.d.ts +2 -0
  15. package/node_modules/@llblab/pi-state-flow/dist/lib/episode.js +4 -0
  16. package/node_modules/@llblab/pi-state-flow/dist/lib/extension.d.ts +5 -0
  17. package/node_modules/@llblab/pi-state-flow/dist/lib/extension.js +93 -35
  18. package/node_modules/@llblab/pi-state-flow/dist/lib/git.js +20 -23
  19. package/node_modules/@llblab/pi-state-flow/dist/lib/history.js +8 -4
  20. package/node_modules/@llblab/pi-state-flow/dist/lib/json.js +29 -3
  21. package/node_modules/@llblab/pi-state-flow/dist/lib/migration.js +34 -18
  22. package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.js +14 -16
  23. package/node_modules/@llblab/pi-state-flow/dist/lib/query.d.ts +27 -1
  24. package/node_modules/@llblab/pi-state-flow/dist/lib/query.js +182 -10
  25. package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.d.ts +2 -0
  26. package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.js +34 -5
  27. package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.d.ts +5 -5
  28. package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.js +20 -24
  29. package/node_modules/@llblab/pi-state-flow/dist/lib/state.d.ts +6 -3
  30. package/node_modules/@llblab/pi-state-flow/dist/lib/state.js +5 -3
  31. package/node_modules/@llblab/pi-state-flow/dist/lib/status.js +1 -1
  32. package/node_modules/@llblab/pi-state-flow/dist/lib/storage.d.ts +2 -0
  33. package/node_modules/@llblab/pi-state-flow/dist/lib/storage.js +55 -20
  34. package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.d.ts +8 -6
  35. package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.js +10 -3
  36. package/node_modules/@llblab/pi-state-flow/dist/lib/transition.js +7 -3
  37. package/node_modules/@llblab/pi-state-flow/dist/package.json +1 -1
  38. package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-memory/SKILL.md +11 -3
  39. package/node_modules/@llblab/pi-state-flow/docs/README.md +1 -0
  40. package/node_modules/@llblab/pi-state-flow/docs/architecture.md +5 -3
  41. package/node_modules/@llblab/pi-state-flow/docs/filesystem-recovery.md +3 -2
  42. package/node_modules/@llblab/pi-state-flow/docs/lazy-state.md +504 -0
  43. package/node_modules/@llblab/pi-state-flow/docs/usage.md +11 -8
  44. package/node_modules/@llblab/pi-state-flow/lib/config.ts +18 -15
  45. package/node_modules/@llblab/pi-state-flow/lib/context.ts +24 -0
  46. package/node_modules/@llblab/pi-state-flow/lib/continuation.ts +4 -3
  47. package/node_modules/@llblab/pi-state-flow/lib/durable.ts +8 -4
  48. package/node_modules/@llblab/pi-state-flow/lib/episode.ts +5 -0
  49. package/node_modules/@llblab/pi-state-flow/lib/extension.ts +86 -34
  50. package/node_modules/@llblab/pi-state-flow/lib/git.ts +23 -22
  51. package/node_modules/@llblab/pi-state-flow/lib/history.ts +8 -4
  52. package/node_modules/@llblab/pi-state-flow/lib/json.ts +31 -3
  53. package/node_modules/@llblab/pi-state-flow/lib/migration.ts +33 -16
  54. package/node_modules/@llblab/pi-state-flow/lib/protocol.ts +14 -16
  55. package/node_modules/@llblab/pi-state-flow/lib/query.ts +173 -9
  56. package/node_modules/@llblab/pi-state-flow/lib/runtime.ts +32 -4
  57. package/node_modules/@llblab/pi-state-flow/lib/snapshot.ts +22 -25
  58. package/node_modules/@llblab/pi-state-flow/lib/state.ts +11 -6
  59. package/node_modules/@llblab/pi-state-flow/lib/status.ts +1 -1
  60. package/node_modules/@llblab/pi-state-flow/lib/storage.ts +46 -18
  61. package/node_modules/@llblab/pi-state-flow/lib/telegram.ts +19 -6
  62. package/node_modules/@llblab/pi-state-flow/lib/transition.ts +7 -3
  63. package/node_modules/@llblab/pi-state-flow/package.json +1 -1
  64. package/node_modules/@llblab/pi-state-flow/skills/state-flow-memory/SKILL.md +11 -3
  65. package/node_modules/@llblab/pi-telegram/AGENTS.md +1 -1
  66. package/node_modules/@llblab/pi-telegram/BACKLOG.md +1 -0
  67. package/node_modules/@llblab/pi-telegram/CHANGELOG.md +7 -0
  68. package/node_modules/@llblab/pi-telegram/README.md +1 -0
  69. package/node_modules/@llblab/pi-telegram/dist/lib/bindings.d.ts +2 -1
  70. package/node_modules/@llblab/pi-telegram/dist/lib/bindings.js +2 -1
  71. package/node_modules/@llblab/pi-telegram/dist/lib/commands.d.ts +134 -2
  72. package/node_modules/@llblab/pi-telegram/dist/lib/commands.js +297 -16
  73. package/node_modules/@llblab/pi-telegram/dist/lib/delivery.d.ts +2 -0
  74. package/node_modules/@llblab/pi-telegram/dist/lib/delivery.js +11 -0
  75. package/node_modules/@llblab/pi-telegram/dist/lib/extension.js +68 -8
  76. package/node_modules/@llblab/pi-telegram/dist/lib/menu-settings.d.ts +4 -3
  77. package/node_modules/@llblab/pi-telegram/dist/lib/menu-settings.js +17 -13
  78. package/node_modules/@llblab/pi-telegram/dist/lib/pi.d.ts +2 -1
  79. package/node_modules/@llblab/pi-telegram/dist/lib/pi.js +1 -0
  80. package/node_modules/@llblab/pi-telegram/dist/lib/routing.d.ts +1 -0
  81. package/node_modules/@llblab/pi-telegram/dist/lib/routing.js +55 -5
  82. package/node_modules/@llblab/pi-telegram/dist/lib/thread-display.d.ts +23 -0
  83. package/node_modules/@llblab/pi-telegram/dist/lib/thread-display.js +20 -0
  84. package/node_modules/@llblab/pi-telegram/dist/lib/threads.d.ts +18 -0
  85. package/node_modules/@llblab/pi-telegram/dist/lib/threads.js +152 -6
  86. package/node_modules/@llblab/pi-telegram/dist/lib/updates.d.ts +1 -0
  87. package/node_modules/@llblab/pi-telegram/dist/lib/updates.js +5 -3
  88. package/node_modules/@llblab/pi-telegram/dist/package.json +1 -1
  89. package/node_modules/@llblab/pi-telegram/docs/architecture.md +3 -3
  90. package/node_modules/@llblab/pi-telegram/docs/callback-namespaces.md +1 -1
  91. package/node_modules/@llblab/pi-telegram/docs/public-api.md +4 -3
  92. package/node_modules/@llblab/pi-telegram/docs/sections.md +2 -2
  93. package/node_modules/@llblab/pi-telegram/docs/ui-style.md +2 -1
  94. package/node_modules/@llblab/pi-telegram/docs/updates.md +1 -1
  95. package/node_modules/@llblab/pi-telegram/lib/bindings.ts +3 -0
  96. package/node_modules/@llblab/pi-telegram/lib/commands.ts +466 -21
  97. package/node_modules/@llblab/pi-telegram/lib/delivery.ts +15 -0
  98. package/node_modules/@llblab/pi-telegram/lib/extension.ts +77 -9
  99. package/node_modules/@llblab/pi-telegram/lib/menu-settings.ts +25 -10
  100. package/node_modules/@llblab/pi-telegram/lib/pi.ts +3 -0
  101. package/node_modules/@llblab/pi-telegram/lib/routing.ts +84 -17
  102. package/node_modules/@llblab/pi-telegram/lib/thread-display.ts +30 -0
  103. package/node_modules/@llblab/pi-telegram/lib/threads.ts +199 -6
  104. package/node_modules/@llblab/pi-telegram/lib/updates.ts +5 -2
  105. package/node_modules/@llblab/pi-telegram/package.json +1 -1
  106. package/package.json +3 -3
@@ -25,12 +25,14 @@ function getVoiceReplyModeSetting(mode, configured) {
25
25
  export function buildTelegramSettingsMenuText() {
26
26
  return SETTINGS_MENU_TITLE;
27
27
  }
28
- export function buildThreadDisplaySettingsText(mode) {
28
+ export function buildThreadDisplaySettingsText(mode, custom = false) {
29
29
  return [
30
- `${THREAD_DISPLAY_SETTINGS_TITLE} <code>${threadDisplayModeLabel(mode)}</code>`,
30
+ `${THREAD_DISPLAY_SETTINGS_TITLE} <code>${custom ? "custom" : threadDisplayModeLabel(mode)}</code>`,
31
31
  "",
32
32
  "Choose how this bot profile labels Telegram tabs and Pi terminal status. Each slot is unique across this bot profile.",
33
33
  "",
34
+ "Set a manual name for the current Thread with /name <code>Name</code>.",
35
+ "",
34
36
  "<code>-</code> <code>letters</code> (default): show the unique slot, such as <b><i>A</i></b> or <b><i>B</i></b>.",
35
37
  "<code>-</code> <code>names</code>: show the generated dictionary name for the slot, such as <b><i>Anchor</i></b> or <b><i>Briar</i></b>.",
36
38
  "<code>-</code> <code>directory-title</code>: humanize the distinguishing directory path, such as <b><i>Api Tools</i></b> or <b><i>Frontend / Extensions A</i></b>.",
@@ -122,7 +124,7 @@ export function buildTimeInjectionModeSettingsText(mode) {
122
124
  "<code>-</code> <code>interval</code> (default): add time at most once per chat interval (1 hour unless configured).",
123
125
  ].join("\n");
124
126
  }
125
- export function buildTelegramSettingsMenuReplyMarkup(draftPreviewsEnabled, assistantRenderingModeOrVoiceReplyMode, voiceReplyModeOrTimeInjectionMode, timeInjectionModeOrSectionRegistry, sectionRegistryOrVoiceReplyModeConfigured, voiceReplyModeConfigured = true, automaticThreadCleanupEnabled = true, activityVerbosity = "quiet", threadDisplayMode) {
127
+ export function buildTelegramSettingsMenuReplyMarkup(draftPreviewsEnabled, assistantRenderingModeOrVoiceReplyMode, voiceReplyModeOrTimeInjectionMode, timeInjectionModeOrSectionRegistry, sectionRegistryOrVoiceReplyModeConfigured, voiceReplyModeConfigured = true, automaticThreadCleanupEnabled = true, activityVerbosity = "quiet", threadDisplayMode, threadDisplayCustom = false) {
126
128
  const hasRenderingMode = assistantRenderingModeOrVoiceReplyMode === "rich" ||
127
129
  assistantRenderingModeOrVoiceReplyMode === "html";
128
130
  const assistantRenderingMode = hasRenderingMode ? assistantRenderingModeOrVoiceReplyMode : "rich";
@@ -164,14 +166,13 @@ export function buildTelegramSettingsMenuReplyMarkup(draftPreviewsEnabled, assis
164
166
  text: `🕒 Time injection: ${getTelegramSettingsStateValueLabel(timeInjectionMode)}`,
165
167
  callback_data: "settings:open:time-injection",
166
168
  },
167
- {
168
- text: `🧹 Thread cleanup: ${automaticThreadCleanupEnabled ? "on" : "off"}`,
169
- callback_data: "settings:open:automatic-thread-cleanup",
170
- },
171
169
  ];
172
170
  if (threadDisplayMode)
173
171
  settingsButtons.push({
174
- text: `🧵 Thread display: ${threadDisplayMode}`,
172
+ text: `🧹 Thread cleanup: ${automaticThreadCleanupEnabled ? "on" : "off"}`,
173
+ callback_data: "settings:open:automatic-thread-cleanup",
174
+ }, {
175
+ text: `🧵 Thread display: ${threadDisplayCustom ? "custom" : threadDisplayMode}`,
175
176
  callback_data: "settings:open:thread-display",
176
177
  });
177
178
  if (sectionRegistry) {
@@ -186,7 +187,7 @@ export function buildTelegramSettingsMenuReplyMarkup(draftPreviewsEnabled, assis
186
187
  }
187
188
  export async function openTelegramSettingsMenu(deps, sectionRegistry) {
188
189
  const state = await deps.getModelMenuState();
189
- const messageId = await deps.sendSettingsMenu(state, buildTelegramSettingsMenuText(), buildTelegramSettingsMenuReplyMarkup(deps.areDraftPreviewsEnabled(), deps.getAssistantRenderingMode(), deps.getVoiceReplyMode(), deps.getTimeInjectionMode(), sectionRegistry, deps.isVoiceReplyModeConfigured(), deps.isAutomaticThreadCleanupEnabled(), deps.getActivityVerbosity(), deps.getThreadDisplayMode?.()));
190
+ const messageId = await deps.sendSettingsMenu(state, buildTelegramSettingsMenuText(), buildTelegramSettingsMenuReplyMarkup(deps.areDraftPreviewsEnabled(), deps.getAssistantRenderingMode(), deps.getVoiceReplyMode(), deps.getTimeInjectionMode(), sectionRegistry, deps.isVoiceReplyModeConfigured(), deps.isAutomaticThreadCleanupEnabled(), deps.getActivityVerbosity(), deps.getThreadDisplayMode?.(), deps.isThreadDisplayCustom?.() ?? false));
190
191
  if (messageId === undefined)
191
192
  return;
192
193
  state.messageId = messageId;
@@ -202,11 +203,11 @@ function threadDisplayModeLabel(mode) {
202
203
  default: return "letters";
203
204
  }
204
205
  }
205
- export function buildThreadDisplaySettingsReplyMarkup(mode) {
206
+ export function buildThreadDisplaySettingsReplyMarkup(mode, custom = false) {
206
207
  return { inline_keyboard: [
207
208
  [{ text: "⬆️ Back", callback_data: "settings:list" }],
208
209
  ...["letters", "names", "directory-title", "directory-snake"].map((value) => [{
209
- text: `${mode === value ? "🟢 " : ""}${threadDisplayModeLabel(value)}`,
210
+ text: `${!custom && mode === value ? "🟢 " : ""}${threadDisplayModeLabel(value)}`,
210
211
  callback_data: `settings:set:thread-display:${value}`,
211
212
  }]),
212
213
  ] };
@@ -307,7 +308,7 @@ export function buildVoiceReplyModeSettingsReplyMarkup(mode, configured = true)
307
308
  };
308
309
  }
309
310
  export async function updateTelegramSettingsMenuMessage(deps, sectionRegistry) {
310
- await deps.updateSettingsMessage(buildTelegramSettingsMenuText(), buildTelegramSettingsMenuReplyMarkup(deps.areDraftPreviewsEnabled(), deps.getAssistantRenderingMode(), deps.getVoiceReplyMode(), deps.getTimeInjectionMode(), sectionRegistry, deps.isVoiceReplyModeConfigured(), deps.isAutomaticThreadCleanupEnabled(), deps.getActivityVerbosity(), deps.getThreadDisplayMode?.()));
311
+ await deps.updateSettingsMessage(buildTelegramSettingsMenuText(), buildTelegramSettingsMenuReplyMarkup(deps.areDraftPreviewsEnabled(), deps.getAssistantRenderingMode(), deps.getVoiceReplyMode(), deps.getTimeInjectionMode(), sectionRegistry, deps.isVoiceReplyModeConfigured(), deps.isAutomaticThreadCleanupEnabled(), deps.getActivityVerbosity(), deps.getThreadDisplayMode?.(), deps.isThreadDisplayCustom?.() ?? false));
311
312
  }
312
313
  export async function updateAutomaticThreadCleanupSettingsMessage(deps) {
313
314
  const enabled = deps.isAutomaticThreadCleanupEnabled();
@@ -362,7 +363,7 @@ export async function handleTelegramSettingsMenuCallbackAction(callbackQueryId,
362
363
  await deps.answerCallbackQuery(callbackQueryId, "Threaded Mode is no longer available.");
363
364
  return true;
364
365
  }
365
- await deps.updateSettingsMessage(buildThreadDisplaySettingsText(mode), buildThreadDisplaySettingsReplyMarkup(mode));
366
+ await deps.updateSettingsMessage(buildThreadDisplaySettingsText(mode, deps.isThreadDisplayCustom?.() ?? false), buildThreadDisplaySettingsReplyMarkup(mode, deps.isThreadDisplayCustom?.() ?? false));
366
367
  await deps.answerCallbackQuery(callbackQueryId);
367
368
  return true;
368
369
  }
@@ -533,6 +534,7 @@ export function createTelegramSettingsMenuRuntime(deps, sectionRegistry) {
533
534
  getTimeInjectionMode: deps.getTimeInjectionMode,
534
535
  isAutomaticThreadCleanupEnabled: deps.isAutomaticThreadCleanupEnabled,
535
536
  getThreadDisplayMode: deps.getThreadDisplayMode,
537
+ isThreadDisplayCustom: deps.isThreadDisplayCustom,
536
538
  sendSettingsMenu: (state, text, replyMarkup) => deps.sendInteractiveMessage(state.chatId, text, "html", replyMarkup),
537
539
  storeModelMenuState: deps.storeModelMenuState,
538
540
  }, sectionRegistry);
@@ -548,6 +550,7 @@ export function createTelegramSettingsMenuRuntime(deps, sectionRegistry) {
548
550
  getTimeInjectionMode: deps.getTimeInjectionMode,
549
551
  isAutomaticThreadCleanupEnabled: deps.isAutomaticThreadCleanupEnabled,
550
552
  getThreadDisplayMode: deps.getThreadDisplayMode,
553
+ isThreadDisplayCustom: deps.isThreadDisplayCustom,
551
554
  updateSettingsMessage: (text, replyMarkup) => deps.editInteractiveMessage(state.chatId, state.messageId, text, "html", replyMarkup),
552
555
  }, sectionRegistry);
553
556
  },
@@ -577,6 +580,7 @@ export function createTelegramSettingsMenuRuntime(deps, sectionRegistry) {
577
580
  getTimeInjectionMode: deps.getTimeInjectionMode,
578
581
  isAutomaticThreadCleanupEnabled: deps.isAutomaticThreadCleanupEnabled,
579
582
  getThreadDisplayMode: deps.getThreadDisplayMode,
583
+ isThreadDisplayCustom: deps.isThreadDisplayCustom,
580
584
  setThreadDisplayMode: deps.setThreadDisplayMode,
581
585
  setDraftPreviewsEnabled: deps.setDraftPreviewsEnabled,
582
586
  setAssistantRenderingMode: deps.setAssistantRenderingMode,
@@ -56,8 +56,9 @@ export interface PiExtensionApiRuntimePorts {
56
56
  getActiveTools: ExtensionAPI["getActiveTools"];
57
57
  setActiveTools: ExtensionAPI["setActiveTools"];
58
58
  setModel: ExtensionAPI["setModel"];
59
+ registerCommand: ExtensionAPI["registerCommand"];
59
60
  }
60
- export declare function createExtensionApiRuntimePorts(api: Pick<ExtensionAPI, "sendUserMessage" | "exec" | "getCommands" | "getThinkingLevel" | "setThinkingLevel" | "getActiveTools" | "setActiveTools" | "setModel">): PiExtensionApiRuntimePorts;
61
+ export declare function createExtensionApiRuntimePorts(api: Pick<ExtensionAPI, "sendUserMessage" | "exec" | "getCommands" | "getThinkingLevel" | "setThinkingLevel" | "getActiveTools" | "setActiveTools" | "setModel" | "registerCommand">): PiExtensionApiRuntimePorts;
61
62
  export declare function normalizeSettingsManager(manager: unknown): PiSettingsManager;
62
63
  export declare function createSettingsManager(cwd: string): Promise<PiSettingsManager>;
63
64
  export declare function createScopedModelPatternPersister(deps: {
@@ -44,6 +44,7 @@ export function createExtensionApiRuntimePorts(api) {
44
44
  getActiveTools: () => api.getActiveTools(),
45
45
  setActiveTools: (names) => api.setActiveTools(names),
46
46
  setModel: (model) => api.setModel(model),
47
+ registerCommand: (name, options) => api.registerCommand(name, options),
47
48
  };
48
49
  }
49
50
  function readEnabledModels(value) {
@@ -164,6 +164,7 @@ export interface TelegramInboundRouteRuntimeDeps<TMessage extends TelegramRouted
164
164
  sendUserMessage?: (message: string, options?: Queue.TelegramPromptDeliveryOptions) => void;
165
165
  isIdle: (ctx: TContext) => boolean;
166
166
  hasPendingMessages: (ctx: TContext) => boolean;
167
+ requestNewSession?: (source: unknown) => void;
167
168
  compact: (ctx: TContext, callbacks: {
168
169
  onComplete: () => void;
169
170
  onError: (error: unknown) => void;
@@ -322,6 +322,7 @@ const TELEGRAM_OWNED_CALLBACK_PREFIXES = [
322
322
  "compact:",
323
323
  "menu:",
324
324
  "model:",
325
+ "new:",
325
326
  "queue:",
326
327
  "section:",
327
328
  "settings:",
@@ -1216,7 +1217,7 @@ export function createTelegramInboundRouteRuntime(deps) {
1216
1217
  return;
1217
1218
  }
1218
1219
  if (selected.kind === "cancel") {
1219
- await deps.editInteractiveMessage?.(chatId, dialogMessageId, "<b>✖ Rename cancelled. Returning to normal agent mode.</b>", "html", { inline_keyboard: [] });
1220
+ await deps.editInteractiveMessage?.(chatId, dialogMessageId, "<b>✖ Rename cancelled.</b>", "html", { inline_keyboard: [] });
1220
1221
  await deps.answerCallbackQuery(query.id);
1221
1222
  return;
1222
1223
  }
@@ -1241,6 +1242,36 @@ export function createTelegramInboundRouteRuntime(deps) {
1241
1242
  }
1242
1243
  return;
1243
1244
  }
1245
+ const handledByNew = await Commands.handleTelegramNewConfirmationCallback(query, {
1246
+ ctx,
1247
+ answerCallbackQuery: deps.answerCallbackQuery,
1248
+ editInteractiveMessage: deps.editInteractiveMessage ?? (async () => { }),
1249
+ deleteMessage: deps.deleteMessage ?? (async () => { }),
1250
+ runNew: async (newCtx) => {
1251
+ await Commands.handleTelegramNewCommand({
1252
+ isIdle: () => deps.isIdle(newCtx),
1253
+ hasPendingMessages: () => deps.hasPendingMessages(newCtx),
1254
+ hasActiveTelegramTurn: deps.activeTurnRuntime.has,
1255
+ hasDispatchPending: deps.bridgeRuntime.lifecycle.hasDispatchPending,
1256
+ hasQueuedTelegramItems: deps.telegramQueueStore.hasQueuedItems,
1257
+ isCompactionInProgress: deps.bridgeRuntime.lifecycle.isCompactionInProgress,
1258
+ requestNewSession: deps.requestNewSession
1259
+ ? () => deps.requestNewSession(query)
1260
+ : undefined,
1261
+ sendTextReply: async (text) => {
1262
+ const chatId = query.message?.chat?.id;
1263
+ const messageId = query.message?.message_id;
1264
+ if (typeof chatId !== "number" || typeof messageId !== "number")
1265
+ return;
1266
+ await deps.editInteractiveMessage?.(chatId, messageId, text, "html", { inline_keyboard: [] });
1267
+ },
1268
+ recordRuntimeEvent: deps.recordRuntimeEvent,
1269
+ });
1270
+ },
1271
+ });
1272
+ assertExecutionCurrent();
1273
+ if (handledByNew)
1274
+ return;
1244
1275
  const handledByCompact = await Commands.handleTelegramCompactConfirmationCallback(query, {
1245
1276
  ctx,
1246
1277
  answerCallbackQuery: deps.answerCallbackQuery,
@@ -1392,6 +1423,7 @@ export function createTelegramInboundRouteRuntime(deps) {
1392
1423
  stopTypingLoop: deps.stopTypingLoop,
1393
1424
  enqueueContinueTurn,
1394
1425
  compact: deps.compact,
1426
+ requestNewSession: deps.requestNewSession,
1395
1427
  allocateItemOrder: deps.bridgeRuntime.queue.allocateItemOrder,
1396
1428
  allocateControlOrder: deps.bridgeRuntime.queue.allocateControlOrder,
1397
1429
  appendControlItem: deps.queueMutationRuntime.append,
@@ -1652,9 +1684,18 @@ export function createTelegramInboundRouteRuntime(deps) {
1652
1684
  dialogMessageId: candidate.dialogMessageId,
1653
1685
  });
1654
1686
  }
1655
- await deps.sendTextReply(target.chatId, message.message_id, result.ok && !result.message
1687
+ const replyText = result.ok && !result.message
1656
1688
  ? Commands.formatTelegramAutomaticThreadDisplayNameRestoredHeading(result.threadName ?? name)
1657
- : Commands.formatTelegramInformationHeading(result.ok ? "✅" : "⚠️", result.message ?? "Thread display name reset failed."), { parseMode: "HTML", target });
1689
+ : Commands.formatTelegramInformationHeading(result.ok ? "✅" : "⚠️", result.message ?? "Thread display name reset failed.");
1690
+ if (result.ok) {
1691
+ Updates.assertTelegramUpdateExecutionCurrent(message);
1692
+ Updates.reportTelegramUpdateCompleted(message);
1693
+ void deps.sendTextReply(target.chatId, message.message_id, replyText, { parseMode: "HTML", target }).catch((error) => deps.recordRuntimeEvent?.("telegram-command", error, {
1694
+ command: "name", phase: "reset-result",
1695
+ }));
1696
+ return true;
1697
+ }
1698
+ await deps.sendTextReply(target.chatId, message.message_id, replyText, { parseMode: "HTML", target });
1658
1699
  return true;
1659
1700
  }
1660
1701
  const validationError = deps.validateThreadName?.(name);
@@ -1677,9 +1718,18 @@ export function createTelegramInboundRouteRuntime(deps) {
1677
1718
  dialogMessageId: candidate.dialogMessageId,
1678
1719
  });
1679
1720
  }
1680
- await deps.sendTextReply(target.chatId, message.message_id, result?.ok && !result.message
1721
+ const replyText = result?.ok && !result.message
1681
1722
  ? Commands.formatTelegramThreadDisplayNameSavedHeading(result.threadName ?? consumed.name)
1682
- : Commands.formatTelegramInformationHeading(result?.ok ? "✅" : "⚠️", result?.message ?? "Thread display name update failed."), { parseMode: "HTML", target });
1723
+ : Commands.formatTelegramInformationHeading(result?.ok ? "✅" : "⚠️", result?.message ?? "Thread display name update failed.");
1724
+ if (result?.ok) {
1725
+ Updates.assertTelegramUpdateExecutionCurrent(message);
1726
+ Updates.reportTelegramUpdateCompleted(message);
1727
+ void deps.sendTextReply(target.chatId, message.message_id, replyText, { parseMode: "HTML", target }).catch((error) => deps.recordRuntimeEvent?.("telegram-command", error, {
1728
+ command: "name", phase: "rename-result",
1729
+ }));
1730
+ return true;
1731
+ }
1732
+ await deps.sendTextReply(target.chatId, message.message_id, replyText, { parseMode: "HTML", target });
1683
1733
  return true;
1684
1734
  },
1685
1735
  handleCommand: commandHandler,
@@ -38,6 +38,29 @@ export interface TelegramThreadDisplayReconcilerDeps {
38
38
  captureLiveBindingKeys(bindings: readonly TelegramWorkspaceThreadBinding[]): ReadonlySet<string>;
39
39
  callApi<TResponse>(method: string, body: Record<string, unknown>, options?: TelegramApiCallOptions): Promise<TResponse>;
40
40
  }
41
+ export declare function createTelegramThreadDisplaySettingsRuntime(deps: {
42
+ getTarget(): {
43
+ chatId: number;
44
+ threadId?: number;
45
+ } | undefined;
46
+ getBinding(target: {
47
+ chatId: number;
48
+ threadId?: number;
49
+ }): {
50
+ manualThreadName?: string;
51
+ } | undefined;
52
+ apply(mode: TelegramThreadDisplayMode): Promise<void>;
53
+ reset(target: {
54
+ chatId: number;
55
+ threadId?: number;
56
+ }): Promise<{
57
+ ok: boolean;
58
+ message?: string;
59
+ }>;
60
+ }): {
61
+ isCustom(): boolean;
62
+ setMode(mode: TelegramThreadDisplayMode): Promise<void>;
63
+ };
41
64
  /** Caller owns triggering and reporting; no timer or background retry is created. */
42
65
  export declare function createTelegramThreadDisplayReconciler(deps: TelegramThreadDisplayReconcilerDeps): {
43
66
  reconcile(): Promise<{
@@ -192,6 +192,26 @@ export async function applyTelegramThreadDisplaySetting(mode, deps) {
192
192
  if (deps.getProfileKey() !== profile)
193
193
  throw new Error("Telegram Thread display setting changed profile.");
194
194
  }
195
+ export function createTelegramThreadDisplaySettingsRuntime(deps) {
196
+ return {
197
+ isCustom() {
198
+ const target = deps.getTarget();
199
+ return !!target && typeof deps.getBinding(target)?.manualThreadName === "string";
200
+ },
201
+ async setMode(mode) {
202
+ const target = deps.getTarget();
203
+ const hadManualName = !!target &&
204
+ typeof deps.getBinding(target)?.manualThreadName === "string";
205
+ await deps.apply(mode);
206
+ if (!hadManualName || !target)
207
+ return;
208
+ const reset = await deps.reset(target);
209
+ if (!reset.ok) {
210
+ throw new Error(reset.message ?? "Telegram Thread display override reset failed.");
211
+ }
212
+ },
213
+ };
214
+ }
195
215
  /** Caller owns triggering and reporting; no timer or background retry is created. */
196
216
  export function createTelegramThreadDisplayReconciler(deps) {
197
217
  let tail = Promise.resolve();
@@ -175,6 +175,19 @@ export interface TelegramBotStateSnapshot {
175
175
  lastSlot?: string;
176
176
  lastReconcileAction?: string;
177
177
  }
178
+ export interface TelegramSessionReplacementIntent {
179
+ continuity: "workspace-thread" | "classic-chat";
180
+ cwd: string;
181
+ profileName: string;
182
+ sourceSessionId: string;
183
+ sourceUpdateId: number;
184
+ target: TelegramTarget;
185
+ messageId: number;
186
+ slot?: string;
187
+ threadName?: string;
188
+ createdAtMs: number;
189
+ expiresAtMs: number;
190
+ }
178
191
  export interface TelegramTopicTargetFile {
179
192
  version: 1;
180
193
  source: "snapshot";
@@ -187,6 +200,7 @@ export interface TelegramTopicTargetFile {
187
200
  identities?: TelegramThreadIdentityRecord[];
188
201
  workspaceBindings?: TelegramWorkspaceThreadBinding[];
189
202
  workspaceRetirements?: TelegramWorkspaceRetirementIntent[];
203
+ sessionReplacement?: TelegramSessionReplacementIntent;
190
204
  reservations?: TelegramThreadReservation[];
191
205
  pendingProvisions?: TelegramThreadPendingProvision[];
192
206
  pendingCleanups?: TelegramThreadCleanupIntent[];
@@ -227,6 +241,10 @@ export interface TelegramTopicTargetStore {
227
241
  getIdentityByProfileKey: (profileKey: string) => TelegramThreadIdentityRecord | undefined;
228
242
  forgetIdentityByProfileKey: (profileKey: string) => boolean;
229
243
  listWorkspaceBindings: () => TelegramWorkspaceThreadBinding[];
244
+ getWorkspaceBindingByTarget: (target: TelegramTarget, sessionId?: string) => TelegramWorkspaceThreadBinding | undefined;
245
+ getSessionReplacementIntent: () => TelegramSessionReplacementIntent | undefined;
246
+ commitSessionReplacementIntent: (intent: TelegramSessionReplacementIntent, isCurrent: () => boolean) => Promise<boolean>;
247
+ removeSessionReplacementIntent: (expected: TelegramSessionReplacementIntent, isCurrent: () => boolean) => Promise<boolean>;
230
248
  listWorkspaceRetirementIntents: () => TelegramWorkspaceRetirementIntent[];
231
249
  commitWorkspaceJournalEvidence: (expected: TelegramWorkspaceThreadBinding, journalBindingKeys: readonly string[], complete: boolean) => TelegramWorkspaceThreadBinding | undefined;
232
250
  upsertWorkspaceRetirementIntent: (intent: TelegramWorkspaceRetirementIntent) => boolean;
@@ -645,6 +645,50 @@ function normalizeWorkspaceBindingRecord(value) {
645
645
  function cloneWorkspaceBinding(binding) {
646
646
  return { ...binding, target: { ...binding.target } };
647
647
  }
648
+ function cloneSessionReplacementIntent(intent) {
649
+ return { ...intent, target: { ...intent.target } };
650
+ }
651
+ function normalizeSessionReplacementIntent(value) {
652
+ if (!value || typeof value !== "object" || Array.isArray(value))
653
+ return undefined;
654
+ const record = value;
655
+ const target = record.target;
656
+ if (typeof record.cwd !== "string" || !normalizeTelegramWorkspacePath(record.cwd) ||
657
+ typeof record.profileName !== "string" || !record.profileName ||
658
+ typeof record.sourceSessionId !== "string" || !normalizeTelegramSessionId(record.sourceSessionId) ||
659
+ typeof record.sourceUpdateId !== "number" || !Number.isSafeInteger(record.sourceUpdateId) ||
660
+ !target || typeof target.chatId !== "number" ||
661
+ (target.threadId !== undefined &&
662
+ (typeof target.threadId !== "number" || !Number.isSafeInteger(target.threadId))) ||
663
+ typeof record.messageId !== "number" || !Number.isSafeInteger(record.messageId) ||
664
+ typeof record.createdAtMs !== "number" || !Number.isSafeInteger(record.createdAtMs) ||
665
+ typeof record.expiresAtMs !== "number" || !Number.isSafeInteger(record.expiresAtMs) ||
666
+ record.expiresAtMs <= record.createdAtMs)
667
+ return undefined;
668
+ const continuity = record.continuity === "workspace-thread" ||
669
+ record.continuity === "classic-chat"
670
+ ? record.continuity
671
+ : target.threadId !== undefined ? "workspace-thread" : "classic-chat";
672
+ if ((continuity === "workspace-thread") !== (target.threadId !== undefined)) {
673
+ return undefined;
674
+ }
675
+ return {
676
+ continuity,
677
+ cwd: normalizeTelegramWorkspacePath(record.cwd),
678
+ profileName: record.profileName,
679
+ sourceSessionId: normalizeTelegramSessionId(record.sourceSessionId),
680
+ sourceUpdateId: record.sourceUpdateId,
681
+ target: {
682
+ chatId: target.chatId,
683
+ ...(typeof target.threadId === "number" ? { threadId: target.threadId } : {}),
684
+ },
685
+ messageId: record.messageId,
686
+ ...(typeof record.slot === "string" ? { slot: record.slot } : {}),
687
+ ...(typeof record.threadName === "string" ? { threadName: record.threadName } : {}),
688
+ createdAtMs: record.createdAtMs,
689
+ expiresAtMs: record.expiresAtMs,
690
+ };
691
+ }
648
692
  function normalizeWorkspaceRetirementIntent(value) {
649
693
  if (!value || typeof value !== "object" || Array.isArray(value))
650
694
  return undefined;
@@ -905,6 +949,7 @@ function parseTopicTargetFile(value) {
905
949
  return normalized ? [normalized] : [];
906
950
  })
907
951
  : [],
952
+ sessionReplacement: normalizeSessionReplacementIntent(file.sessionReplacement),
908
953
  reservations: Array.isArray(file.reservations)
909
954
  ? file.reservations.flatMap((reservation) => {
910
955
  const normalized = normalizeReservation(reservation);
@@ -1026,6 +1071,7 @@ export function createTelegramTopicTargetStore(options) {
1026
1071
  let identities = new Map();
1027
1072
  let workspaceBindings = new Map();
1028
1073
  let workspaceRetirements = [];
1074
+ let sessionReplacement;
1029
1075
  let workspaceRetirementCommitInFlight = false;
1030
1076
  const hasWorkspaceRetirementConflict = (input) => workspaceRetirements.some((intent) => (input.bindingKey !== undefined && intent.binding.bindingKey === input.bindingKey) ||
1031
1077
  (input.cwd !== undefined && intent.binding.cwd === input.cwd) ||
@@ -1150,6 +1196,7 @@ export function createTelegramTopicTargetStore(options) {
1150
1196
  identities = new Map();
1151
1197
  workspaceBindings = new Map();
1152
1198
  workspaceRetirements = [];
1199
+ sessionReplacement = undefined;
1153
1200
  workspaceRetirementCommitInFlight = false;
1154
1201
  workspaceClaims = new Map();
1155
1202
  reservations = [];
@@ -1171,6 +1218,7 @@ export function createTelegramTopicTargetStore(options) {
1171
1218
  identities = new Map();
1172
1219
  workspaceBindings = new Map();
1173
1220
  workspaceRetirements = [];
1221
+ sessionReplacement = undefined;
1174
1222
  workspaceRetirementCommitInFlight = false;
1175
1223
  reservations = [];
1176
1224
  pendingProvisions = [];
@@ -1208,6 +1256,9 @@ export function createTelegramTopicTargetStore(options) {
1208
1256
  cloneWorkspaceBinding(binding),
1209
1257
  ]));
1210
1258
  workspaceRetirements = (file.workspaceRetirements ?? []).map(cloneWorkspaceRetirementIntent);
1259
+ sessionReplacement = file.sessionReplacement
1260
+ ? cloneSessionReplacementIntent(file.sessionReplacement)
1261
+ : undefined;
1211
1262
  reconcileWorkspaceSuffixExposure();
1212
1263
  for (const record of records.values())
1213
1264
  rememberIdentity(record);
@@ -1286,6 +1337,9 @@ export function createTelegramTopicTargetStore(options) {
1286
1337
  identities: Array.from(identities.values()).map(cloneIdentityRecord),
1287
1338
  workspaceBindings: Array.from(workspaceBindings.values()).map(cloneWorkspaceBinding),
1288
1339
  workspaceRetirements: workspaceRetirements.map(cloneWorkspaceRetirementIntent),
1340
+ ...(sessionReplacement
1341
+ ? { sessionReplacement: cloneSessionReplacementIntent(sessionReplacement) }
1342
+ : {}),
1289
1343
  reservations: reservations.map((reservation) => ({ ...reservation })),
1290
1344
  pendingProvisions: pendingProvisions.map((provision) => ({
1291
1345
  ...provision,
@@ -1563,6 +1617,40 @@ export function createTelegramTopicTargetStore(options) {
1563
1617
  listWorkspaceBindings() {
1564
1618
  return Array.from(workspaceBindings.values()).map(cloneWorkspaceBinding);
1565
1619
  },
1620
+ getWorkspaceBindingByTarget(target, sessionId) {
1621
+ const binding = Array.from(workspaceBindings.values()).find((candidate) => targetMatches(candidate.target, target) &&
1622
+ (sessionId === undefined || candidate.sessionId === sessionId));
1623
+ return binding ? cloneWorkspaceBinding(binding) : undefined;
1624
+ },
1625
+ getSessionReplacementIntent() {
1626
+ return sessionReplacement
1627
+ ? cloneSessionReplacementIntent(sessionReplacement)
1628
+ : undefined;
1629
+ },
1630
+ async commitSessionReplacementIntent(intent, isCurrent) {
1631
+ const next = normalizeSessionReplacementIntent(intent);
1632
+ if (!next || !isCurrent())
1633
+ return false;
1634
+ await loadFromDisk();
1635
+ if (!isCurrent())
1636
+ return false;
1637
+ const existing = sessionReplacement;
1638
+ if (existing && existing.expiresAtMs > getNowMs() &&
1639
+ !isDeepStrictEqual(existing, next))
1640
+ return false;
1641
+ sessionReplacement = cloneSessionReplacementIntent(next);
1642
+ markDirty();
1643
+ return await persistSnapshot() && isCurrent();
1644
+ },
1645
+ async removeSessionReplacementIntent(expected, isCurrent) {
1646
+ await loadFromDisk();
1647
+ if (!isCurrent() || !sessionReplacement ||
1648
+ !isDeepStrictEqual(sessionReplacement, expected))
1649
+ return false;
1650
+ sessionReplacement = undefined;
1651
+ markDirty();
1652
+ return await persistSnapshot() && isCurrent();
1653
+ },
1566
1654
  listWorkspaceRetirementIntents() {
1567
1655
  return workspaceRetirements.map(cloneWorkspaceRetirementIntent);
1568
1656
  },
@@ -1959,6 +2047,36 @@ export function createTelegramTopicTargetStore(options) {
1959
2047
  (options?.sessionId !== undefined && !normalizedSessionId) ||
1960
2048
  hasWorkspaceRetirementConflict({ cwd: normalizedCwd }))
1961
2049
  return undefined;
2050
+ let replacementPreviousInstanceId;
2051
+ const replacement = sessionReplacement;
2052
+ if (replacement?.continuity === "workspace-thread" && normalizedSessionId &&
2053
+ replacement.expiresAtMs > getNowMs() &&
2054
+ replacement.profileName === (getTelegramProfile() ?? "default") &&
2055
+ replacement.cwd === normalizedCwd &&
2056
+ replacement.sourceSessionId !== normalizedSessionId) {
2057
+ const sourceEntry = Array.from(workspaceBindings.entries()).find(([, binding]) => binding.cwd === normalizedCwd &&
2058
+ binding.sessionId === replacement.sourceSessionId &&
2059
+ targetMatches(binding.target, replacement.target));
2060
+ if (sourceEntry) {
2061
+ const [sourceKey, sourceBinding] = sourceEntry;
2062
+ const replacementIdentity = Array.from({ length: TELEGRAM_WORKSPACE_SLOTS.length })
2063
+ .map((_, ordinal) => createTelegramWorkspaceBindingIdentityWithKey(sourceBinding.cwd, sourceBinding.workspaceKey, ordinal, normalizedSessionId))
2064
+ .find((identity) => identity?.instanceSlot === sourceBinding.instanceSlot);
2065
+ if (replacementIdentity &&
2066
+ replacementIdentity.instanceSlot === sourceBinding.instanceSlot) {
2067
+ const existingTargetRecord = Array.from(records.values()).find((record) => targetMatches(record.target, replacement.target));
2068
+ replacementPreviousInstanceId = existingTargetRecord?.instanceId;
2069
+ workspaceBindings.delete(sourceKey);
2070
+ workspaceBindings.set(getWorkspaceBindingMapKey(replacementIdentity), {
2071
+ ...sourceBinding,
2072
+ ...replacementIdentity,
2073
+ updatedAtMs: getNowMs(),
2074
+ });
2075
+ markDirty();
2076
+ }
2077
+ }
2078
+ }
2079
+ const effectivePreviousInstanceId = previousInstanceId ?? replacementPreviousInstanceId;
1962
2080
  const externalReservedSlots = captureExternalReservedSlots();
1963
2081
  if (!externalReservedSlots) {
1964
2082
  options?.onCapacityUnavailable?.();
@@ -1995,9 +2113,16 @@ export function createTelegramTopicTargetStore(options) {
1995
2113
  const mapKey = getWorkspaceBindingMapKey(identity);
1996
2114
  const existingClaim = workspaceClaims.get(mapKey);
1997
2115
  if (existingClaim) {
1998
- return existingClaim.instanceId === instanceId
1999
- ? { ...existingClaim.identity }
2000
- : undefined;
2116
+ if (existingClaim.instanceId === instanceId) {
2117
+ return { ...existingClaim.identity };
2118
+ }
2119
+ if (existingClaim.instanceId !== effectivePreviousInstanceId)
2120
+ return undefined;
2121
+ workspaceClaims.set(mapKey, {
2122
+ identity: existingClaim.identity,
2123
+ instanceId,
2124
+ });
2125
+ return { ...existingClaim.identity };
2001
2126
  }
2002
2127
  const binding = workspaceBindings.get(mapKey);
2003
2128
  const liveRecord = binding
@@ -2005,7 +2130,7 @@ export function createTelegramTopicTargetStore(options) {
2005
2130
  : undefined;
2006
2131
  if (liveRecord &&
2007
2132
  liveRecord.instanceId !== instanceId &&
2008
- liveRecord.instanceId !== previousInstanceId) {
2133
+ liveRecord.instanceId !== effectivePreviousInstanceId) {
2009
2134
  return undefined;
2010
2135
  }
2011
2136
  const retainedTarget = binding?.target ?? legacyRecord?.target;
@@ -2147,8 +2272,29 @@ export function createTelegramTopicTargetStore(options) {
2147
2272
  if (!next)
2148
2273
  return undefined;
2149
2274
  const nextMapKey = getWorkspaceBindingMapKey(next);
2150
- const claim = workspaceClaims.get(nextMapKey);
2151
- if (next.slot && Array.from(workspaceBindings.values()).some((existing) => existing.bindingKey !== next.bindingKey && existing.slot === next.slot))
2275
+ let claim = workspaceClaims.get(nextMapKey);
2276
+ const replacedTargetBinding = Array.from(workspaceBindings.values()).find((existing) => existing.bindingKey !== next.bindingKey &&
2277
+ targetMatches(existing.target, next.target));
2278
+ if (claimInstanceId && claim?.instanceId === claimInstanceId &&
2279
+ replacedTargetBinding?.slot) {
2280
+ claim = {
2281
+ ...claim,
2282
+ identity: { ...claim.identity, slot: replacedTargetBinding.slot },
2283
+ };
2284
+ workspaceClaims.set(nextMapKey, claim);
2285
+ next.slot = replacedTargetBinding.slot;
2286
+ if (replacedTargetBinding.threadName && !next.threadName) {
2287
+ next.threadName = replacedTargetBinding.threadName;
2288
+ }
2289
+ if (replacedTargetBinding.manualThreadName && !next.manualThreadName) {
2290
+ next.manualThreadName = replacedTargetBinding.manualThreadName;
2291
+ }
2292
+ if (replacedTargetBinding.displayTitle && !next.displayTitle) {
2293
+ next.displayTitle = replacedTargetBinding.displayTitle;
2294
+ }
2295
+ }
2296
+ if (next.slot && Array.from(workspaceBindings.values()).some((existing) => existing.bindingKey !== next.bindingKey && existing.slot === next.slot &&
2297
+ !targetMatches(existing.target, next.target)))
2152
2298
  return undefined;
2153
2299
  for (const existing of workspaceBindings.values()) {
2154
2300
  if (existing.workspaceKey === next.workspaceKey &&
@@ -1216,6 +1216,7 @@ export interface TelegramUpdateWorkerOwnerRuntimeDeps<TContext> {
1216
1216
  isContextCurrent: (ctx: TContext) => boolean;
1217
1217
  dispatchNext: (ctx: TContext) => void;
1218
1218
  requestQueueHandoffReconciliation: (ctx: TContext) => void;
1219
+ afterUpdateCompleted?: (updateId: number) => void;
1219
1220
  }
1220
1221
  export declare function createTelegramUpdateWorkerOwnerRuntime<TContext>(deps: TelegramUpdateWorkerOwnerRuntimeDeps<TContext>): TelegramUpdateWorkerOwnerRuntime<TContext>;
1221
1222
  export interface TelegramUpdateAdmissionRuntimeBinding<TContext> {
@@ -3177,9 +3177,11 @@ export function createTelegramUpdateWorkerOwnerRuntime(deps) {
3177
3177
  deps.dispatchNext(ctx);
3178
3178
  deps.requestQueueHandoffReconciliation(ctx);
3179
3179
  },
3180
- onUpdateCompleted(_updateId, ctx) {
3181
- if (deps.isContextCurrent(ctx))
3182
- deps.dispatchNext(ctx);
3180
+ onUpdateCompleted(updateId, ctx) {
3181
+ if (!deps.isContextCurrent(ctx))
3182
+ return;
3183
+ deps.dispatchNext(ctx);
3184
+ deps.afterUpdateCompleted?.(updateId);
3183
3185
  },
3184
3186
  };
3185
3187
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-telegram",
3
- "version": "0.48.3",
3
+ "version": "0.49.0",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"