@llblab/pi-kit 0.14.1 → 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 (119) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +3 -3
  3. package/node_modules/@llblab/pi-codex-usage/CHANGELOG.md +5 -0
  4. package/node_modules/@llblab/pi-codex-usage/README.md +6 -0
  5. package/node_modules/@llblab/pi-codex-usage/index.ts +81 -7
  6. package/node_modules/@llblab/pi-codex-usage/package.json +1 -1
  7. package/node_modules/@llblab/pi-state-flow/AGENTS.md +9 -9
  8. package/node_modules/@llblab/pi-state-flow/BACKLOG.md +0 -13
  9. package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +15 -0
  10. package/node_modules/@llblab/pi-state-flow/README.md +7 -6
  11. package/node_modules/@llblab/pi-state-flow/dist/lib/config.d.ts +5 -2
  12. package/node_modules/@llblab/pi-state-flow/dist/lib/config.js +17 -16
  13. package/node_modules/@llblab/pi-state-flow/dist/lib/context.d.ts +8 -0
  14. package/node_modules/@llblab/pi-state-flow/dist/lib/context.js +24 -0
  15. package/node_modules/@llblab/pi-state-flow/dist/lib/continuation.js +4 -3
  16. package/node_modules/@llblab/pi-state-flow/dist/lib/durable.d.ts +1 -0
  17. package/node_modules/@llblab/pi-state-flow/dist/lib/durable.js +8 -3
  18. package/node_modules/@llblab/pi-state-flow/dist/lib/episode.d.ts +2 -0
  19. package/node_modules/@llblab/pi-state-flow/dist/lib/episode.js +4 -0
  20. package/node_modules/@llblab/pi-state-flow/dist/lib/extension.d.ts +5 -0
  21. package/node_modules/@llblab/pi-state-flow/dist/lib/extension.js +93 -35
  22. package/node_modules/@llblab/pi-state-flow/dist/lib/git.js +20 -23
  23. package/node_modules/@llblab/pi-state-flow/dist/lib/history.js +8 -4
  24. package/node_modules/@llblab/pi-state-flow/dist/lib/json.js +29 -3
  25. package/node_modules/@llblab/pi-state-flow/dist/lib/migration.js +34 -18
  26. package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.js +14 -16
  27. package/node_modules/@llblab/pi-state-flow/dist/lib/query.d.ts +27 -1
  28. package/node_modules/@llblab/pi-state-flow/dist/lib/query.js +182 -10
  29. package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.d.ts +2 -0
  30. package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.js +34 -5
  31. package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.d.ts +5 -5
  32. package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.js +20 -24
  33. package/node_modules/@llblab/pi-state-flow/dist/lib/state.d.ts +6 -3
  34. package/node_modules/@llblab/pi-state-flow/dist/lib/state.js +5 -3
  35. package/node_modules/@llblab/pi-state-flow/dist/lib/status.js +1 -1
  36. package/node_modules/@llblab/pi-state-flow/dist/lib/storage.d.ts +2 -0
  37. package/node_modules/@llblab/pi-state-flow/dist/lib/storage.js +55 -20
  38. package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.d.ts +8 -6
  39. package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.js +10 -3
  40. package/node_modules/@llblab/pi-state-flow/dist/lib/transition.js +7 -3
  41. package/node_modules/@llblab/pi-state-flow/dist/package.json +1 -1
  42. package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-memory/SKILL.md +11 -3
  43. package/node_modules/@llblab/pi-state-flow/docs/README.md +1 -0
  44. package/node_modules/@llblab/pi-state-flow/docs/architecture.md +5 -3
  45. package/node_modules/@llblab/pi-state-flow/docs/filesystem-recovery.md +3 -2
  46. package/node_modules/@llblab/pi-state-flow/docs/lazy-state.md +504 -0
  47. package/node_modules/@llblab/pi-state-flow/docs/usage.md +11 -8
  48. package/node_modules/@llblab/pi-state-flow/lib/config.ts +18 -15
  49. package/node_modules/@llblab/pi-state-flow/lib/context.ts +24 -0
  50. package/node_modules/@llblab/pi-state-flow/lib/continuation.ts +4 -3
  51. package/node_modules/@llblab/pi-state-flow/lib/durable.ts +8 -4
  52. package/node_modules/@llblab/pi-state-flow/lib/episode.ts +5 -0
  53. package/node_modules/@llblab/pi-state-flow/lib/extension.ts +86 -34
  54. package/node_modules/@llblab/pi-state-flow/lib/git.ts +23 -22
  55. package/node_modules/@llblab/pi-state-flow/lib/history.ts +8 -4
  56. package/node_modules/@llblab/pi-state-flow/lib/json.ts +31 -3
  57. package/node_modules/@llblab/pi-state-flow/lib/migration.ts +33 -16
  58. package/node_modules/@llblab/pi-state-flow/lib/protocol.ts +14 -16
  59. package/node_modules/@llblab/pi-state-flow/lib/query.ts +173 -9
  60. package/node_modules/@llblab/pi-state-flow/lib/runtime.ts +32 -4
  61. package/node_modules/@llblab/pi-state-flow/lib/snapshot.ts +22 -25
  62. package/node_modules/@llblab/pi-state-flow/lib/state.ts +11 -6
  63. package/node_modules/@llblab/pi-state-flow/lib/status.ts +1 -1
  64. package/node_modules/@llblab/pi-state-flow/lib/storage.ts +46 -18
  65. package/node_modules/@llblab/pi-state-flow/lib/telegram.ts +19 -6
  66. package/node_modules/@llblab/pi-state-flow/lib/transition.ts +7 -3
  67. package/node_modules/@llblab/pi-state-flow/package.json +1 -1
  68. package/node_modules/@llblab/pi-state-flow/skills/state-flow-memory/SKILL.md +11 -3
  69. package/node_modules/@llblab/pi-telegram/AGENTS.md +1 -1
  70. package/node_modules/@llblab/pi-telegram/BACKLOG.md +1 -0
  71. package/node_modules/@llblab/pi-telegram/CHANGELOG.md +12 -0
  72. package/node_modules/@llblab/pi-telegram/README.md +1 -0
  73. package/node_modules/@llblab/pi-telegram/dist/lib/activity.d.ts +2 -0
  74. package/node_modules/@llblab/pi-telegram/dist/lib/activity.js +12 -0
  75. package/node_modules/@llblab/pi-telegram/dist/lib/bindings.d.ts +3 -2
  76. package/node_modules/@llblab/pi-telegram/dist/lib/bindings.js +7 -2
  77. package/node_modules/@llblab/pi-telegram/dist/lib/commands.d.ts +134 -2
  78. package/node_modules/@llblab/pi-telegram/dist/lib/commands.js +297 -16
  79. package/node_modules/@llblab/pi-telegram/dist/lib/delivery.d.ts +2 -0
  80. package/node_modules/@llblab/pi-telegram/dist/lib/delivery.js +11 -0
  81. package/node_modules/@llblab/pi-telegram/dist/lib/extension.js +68 -8
  82. package/node_modules/@llblab/pi-telegram/dist/lib/menu-settings.d.ts +4 -3
  83. package/node_modules/@llblab/pi-telegram/dist/lib/menu-settings.js +17 -13
  84. package/node_modules/@llblab/pi-telegram/dist/lib/pi.d.ts +2 -1
  85. package/node_modules/@llblab/pi-telegram/dist/lib/pi.js +1 -0
  86. package/node_modules/@llblab/pi-telegram/dist/lib/queue.d.ts +2 -0
  87. package/node_modules/@llblab/pi-telegram/dist/lib/queue.js +5 -1
  88. package/node_modules/@llblab/pi-telegram/dist/lib/replies.d.ts +1 -0
  89. package/node_modules/@llblab/pi-telegram/dist/lib/replies.js +1 -0
  90. package/node_modules/@llblab/pi-telegram/dist/lib/routing.d.ts +1 -0
  91. package/node_modules/@llblab/pi-telegram/dist/lib/routing.js +59 -7
  92. package/node_modules/@llblab/pi-telegram/dist/lib/thread-display.d.ts +23 -0
  93. package/node_modules/@llblab/pi-telegram/dist/lib/thread-display.js +20 -0
  94. package/node_modules/@llblab/pi-telegram/dist/lib/threads.d.ts +18 -0
  95. package/node_modules/@llblab/pi-telegram/dist/lib/threads.js +152 -6
  96. package/node_modules/@llblab/pi-telegram/dist/lib/updates.d.ts +1 -0
  97. package/node_modules/@llblab/pi-telegram/dist/lib/updates.js +5 -3
  98. package/node_modules/@llblab/pi-telegram/dist/package.json +1 -1
  99. package/node_modules/@llblab/pi-telegram/docs/architecture.md +3 -3
  100. package/node_modules/@llblab/pi-telegram/docs/callback-namespaces.md +1 -1
  101. package/node_modules/@llblab/pi-telegram/docs/public-api.md +4 -3
  102. package/node_modules/@llblab/pi-telegram/docs/sections.md +2 -2
  103. package/node_modules/@llblab/pi-telegram/docs/ui-style.md +2 -1
  104. package/node_modules/@llblab/pi-telegram/docs/updates.md +1 -1
  105. package/node_modules/@llblab/pi-telegram/lib/activity.ts +14 -0
  106. package/node_modules/@llblab/pi-telegram/lib/bindings.ts +10 -2
  107. package/node_modules/@llblab/pi-telegram/lib/commands.ts +466 -21
  108. package/node_modules/@llblab/pi-telegram/lib/delivery.ts +15 -0
  109. package/node_modules/@llblab/pi-telegram/lib/extension.ts +77 -9
  110. package/node_modules/@llblab/pi-telegram/lib/menu-settings.ts +25 -10
  111. package/node_modules/@llblab/pi-telegram/lib/pi.ts +3 -0
  112. package/node_modules/@llblab/pi-telegram/lib/queue.ts +7 -1
  113. package/node_modules/@llblab/pi-telegram/lib/replies.ts +2 -0
  114. package/node_modules/@llblab/pi-telegram/lib/routing.ts +90 -19
  115. package/node_modules/@llblab/pi-telegram/lib/thread-display.ts +30 -0
  116. package/node_modules/@llblab/pi-telegram/lib/threads.ts +199 -6
  117. package/node_modules/@llblab/pi-telegram/lib/updates.ts +5 -2
  118. package/node_modules/@llblab/pi-telegram/package.json +1 -1
  119. package/package.json +4 -4
@@ -77,6 +77,7 @@ export default function (pi: Pi.ExtensionAPI) {
77
77
  getCommands,
78
78
  getThinkingLevel,
79
79
  sendUserMessage,
80
+ registerCommand,
80
81
  setActiveTools,
81
82
  setModel,
82
83
  setThinkingLevel,
@@ -237,6 +238,26 @@ export default function (pi: Pi.ExtensionAPI) {
237
238
  canReset: lockRuntime.owns,
238
239
  commitReset: lockRuntime.commitIfOwned,
239
240
  });
241
+ const sessionActionAssembly = Commands.createTelegramSessionActionAssembly({
242
+ registerCommand,
243
+ sendUserMessage,
244
+ store: threadStore,
245
+ getProfileName: configStore.getActiveProfileName,
246
+ ownsPersistence: lockRuntime.owns,
247
+ async sendResult(target, html) {
248
+ const delivery = await Delivery.sendTelegramView(
249
+ { text: html, parseMode: "html", replyMarkup: { inline_keyboard: [] } },
250
+ { scope: { kind: "target", target } },
251
+ );
252
+ return delivery.ok ? { ok: true } : { ok: false,
253
+ retryable: delivery.reason === "runtime-unavailable" ||
254
+ delivery.reason === "target-unavailable" ||
255
+ delivery.reason === "transport-retryable" };
256
+ },
257
+ handoffTtlMs: Threads.TELEGRAM_LEADER_SESSION_HANDOFF_TTL_MS,
258
+ recordRuntimeEvent,
259
+ });
260
+ const sessionActionsRuntime = sessionActionAssembly.action;
240
261
  const lockOwnershipGuard =
241
262
  Locks.createTelegramLockOwnershipGuard(lockRuntime);
242
263
  const getCurrentLeaderEpoch = lockRuntime.getOwnedLeaderEpoch;
@@ -851,6 +872,24 @@ export default function (pi: Pi.ExtensionAPI) {
851
872
  updateStatusMessage: menuActions.updateStatusMessage,
852
873
  updateStatus,
853
874
  });
875
+ const threadDisplaySettingsRuntime =
876
+ ThreadDisplay.createTelegramThreadDisplaySettingsRuntime({
877
+ getTarget() {
878
+ return telegramBusFollowerRegistrationState.getTarget() ??
879
+ telegramBusLeaderState.getTarget();
880
+ },
881
+ getBinding(target) { return threadStore.getWorkspaceBindingByTarget(target); },
882
+ apply(mode) {
883
+ return ThreadDisplay.applyTelegramThreadDisplaySetting(mode, {
884
+ getProfileKey: configStore.getActiveProfileName,
885
+ ownsLeader() { return getCurrentLeaderEpoch() !== undefined; },
886
+ getLeaderSetter() { return telegramBusLeaderRuntime.setThreadDisplayMode; },
887
+ getFollowerSetter() { return telegramBusFollowerRegistration.setThreadDisplayMode; },
888
+ reloadConfig: configStore.load,
889
+ });
890
+ },
891
+ reset(target) { return telegramThreadDisplayNameResetBinding.reset(target); },
892
+ });
854
893
  const settingsMenuRuntime = MenuSettings.createTelegramSettingsMenuRuntime(
855
894
  {
856
895
  reloadConfig: configStore.load,
@@ -866,16 +905,10 @@ export default function (pi: Pi.ExtensionAPI) {
866
905
  return threadStore.getBotState().threadMode === "enabled"
867
906
  ? Config.resolveTelegramThreadDisplayMode(configStore.get()) : undefined;
868
907
  },
908
+ isThreadDisplayCustom: threadDisplaySettingsRuntime.isCustom,
869
909
  async setThreadDisplayMode(mode) {
870
- try {
871
- await ThreadDisplay.applyTelegramThreadDisplaySetting(mode, {
872
- getProfileKey: configStore.getActiveProfileName,
873
- ownsLeader() { return getCurrentLeaderEpoch() !== undefined; },
874
- getLeaderSetter() { return telegramBusLeaderRuntime.setThreadDisplayMode; },
875
- getFollowerSetter() { return telegramBusFollowerRegistration.setThreadDisplayMode; },
876
- reloadConfig: configStore.load,
877
- });
878
- } catch (error) {
910
+ try { await threadDisplaySettingsRuntime.setMode(mode); }
911
+ catch (error) {
879
912
  recordRuntimeEvent("bus", error, { phase: "thread-display-setting" });
880
913
  throw error;
881
914
  }
@@ -959,6 +992,36 @@ export default function (pi: Pi.ExtensionAPI) {
959
992
  foreignOwnedUpdateForwarder,
960
993
  replaceFollowerThreadTarget: restoreFollowerThreadTarget,
961
994
  bridgeRuntime,
995
+ requestNewSession(source) {
996
+ const updateId = Updates.getTelegramUpdateExecutionFence(source)?.updateId;
997
+ if (updateId === undefined) {
998
+ throw new Error("Telegram session replacement requires durable update authority.");
999
+ }
1000
+ const callbackMessage = source && typeof source === "object" && "message" in source
1001
+ ? (source as { message?: unknown }).message
1002
+ : source;
1003
+ const messageTarget = callbackMessage as {
1004
+ chat?: { id?: unknown };
1005
+ message_id?: unknown;
1006
+ message_thread_id?: unknown;
1007
+ } | undefined;
1008
+ const target = typeof messageTarget?.chat?.id === "number" &&
1009
+ typeof messageTarget.message_id === "number"
1010
+ ? {
1011
+ chatId: messageTarget.chat.id,
1012
+ messageId: messageTarget.message_id,
1013
+ ...(typeof messageTarget.message_thread_id === "number"
1014
+ ? { threadId: messageTarget.message_thread_id }
1015
+ : {}),
1016
+ }
1017
+ : undefined;
1018
+ if (!target) {
1019
+ throw new Error("Telegram session replacement target is unavailable.");
1020
+ }
1021
+ if (!sessionActionsRuntime.scheduleAfterUpdate(updateId, target)) {
1022
+ throw new Error("A Telegram session replacement is already pending.");
1023
+ }
1024
+ },
962
1025
  activeTurnRuntime,
963
1026
  mediaGroupRuntime,
964
1027
  textGroupRuntime,
@@ -1066,6 +1129,7 @@ export default function (pi: Pi.ExtensionAPI) {
1066
1129
  dispatchNext: dispatchNextQueuedTelegramTurn,
1067
1130
  requestQueueHandoffReconciliation:
1068
1131
  queueHandoffReconciliationBinding.request,
1132
+ afterUpdateCompleted: sessionActionsRuntime.onUpdateCompleted,
1069
1133
  },
1070
1134
  worker: {
1071
1135
  defaultHandle: inboundRouteRuntime.handleUpdate,
@@ -1638,6 +1702,7 @@ export default function (pi: Pi.ExtensionAPI) {
1638
1702
  }
1639
1703
  },
1640
1704
  }.reset);
1705
+ sessionActionsRuntime.register();
1641
1706
  Bindings.registerTelegramCommandsAndTools({
1642
1707
  pi,
1643
1708
  agentDir: Paths.resolveAgentDir(),
@@ -1840,6 +1905,9 @@ export default function (pi: Pi.ExtensionAPI) {
1840
1905
  shutdownGenerativeAppLiveSurfaces: generativeAppLiveSurfaceBinding.shutdown,
1841
1906
  resolveAutomaticThreadCleanupEnabled:
1842
1907
  configControls.resolveAutomaticThreadCleanupEnabled,
1908
+ onSessionStarted(_event, ctx) {
1909
+ sessionActionAssembly.settlement.onSessionStart(ctx);
1910
+ },
1843
1911
  buttonActionStore,
1844
1912
  callMultipart,
1845
1913
  sendChatAction,
@@ -23,6 +23,7 @@ export type TelegramSettingsMenuReplyMarkup = TelegramInlineKeyboardMarkup;
23
23
 
24
24
  export interface TelegramSettingsStateDeps {
25
25
  getThreadDisplayMode?: () => TelegramThreadDisplayMode | undefined;
26
+ isThreadDisplayCustom?: () => boolean;
26
27
  areDraftPreviewsEnabled: () => boolean;
27
28
  getAssistantRenderingMode: () => TelegramAssistantRenderingMode;
28
29
  getActivityVerbosity: () => TelegramActivityVerbosity;
@@ -177,12 +178,15 @@ export function buildTelegramSettingsMenuText(): string {
177
178
 
178
179
  export function buildThreadDisplaySettingsText(
179
180
  mode: TelegramThreadDisplayMode,
181
+ custom = false,
180
182
  ): string {
181
183
  return [
182
- `${THREAD_DISPLAY_SETTINGS_TITLE} <code>${threadDisplayModeLabel(mode)}</code>`,
184
+ `${THREAD_DISPLAY_SETTINGS_TITLE} <code>${custom ? "custom" : threadDisplayModeLabel(mode)}</code>`,
183
185
  "",
184
186
  "Choose how this bot profile labels Telegram tabs and Pi terminal status. Each slot is unique across this bot profile.",
185
187
  "",
188
+ "Set a manual name for the current Thread with /name <code>Name</code>.",
189
+ "",
186
190
  "<code>-</code> <code>letters</code> (default): show the unique slot, such as <b><i>A</i></b> or <b><i>B</i></b>.",
187
191
  "<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>.",
188
192
  "<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>.",
@@ -311,6 +315,7 @@ export function buildTelegramSettingsMenuReplyMarkup(
311
315
  automaticThreadCleanupEnabled = true,
312
316
  activityVerbosity: TelegramActivityVerbosity = "quiet",
313
317
  threadDisplayMode?: TelegramThreadDisplayMode,
318
+ threadDisplayCustom = false,
314
319
  ): TelegramSettingsMenuReplyMarkup {
315
320
  const hasRenderingMode =
316
321
  assistantRenderingModeOrVoiceReplyMode === "rich" ||
@@ -364,15 +369,17 @@ export function buildTelegramSettingsMenuReplyMarkup(
364
369
  text: `🕒 Time injection: ${getTelegramSettingsStateValueLabel(timeInjectionMode)}`,
365
370
  callback_data: "settings:open:time-injection",
366
371
  },
372
+ ];
373
+ if (threadDisplayMode) settingsButtons.push(
367
374
  {
368
375
  text: `🧹 Thread cleanup: ${automaticThreadCleanupEnabled ? "on" : "off"}`,
369
376
  callback_data: "settings:open:automatic-thread-cleanup",
370
377
  },
371
- ];
372
- if (threadDisplayMode) settingsButtons.push({
373
- text: `🧵 Thread display: ${threadDisplayMode}`,
374
- callback_data: "settings:open:thread-display",
375
- });
378
+ {
379
+ text: `🧵 Thread display: ${threadDisplayCustom ? "custom" : threadDisplayMode}`,
380
+ callback_data: "settings:open:thread-display",
381
+ },
382
+ );
376
383
  if (sectionRegistry) {
377
384
  const extensionRows = getTelegramExtensionSettingsRows(sectionRegistry);
378
385
  settingsButtons.push(
@@ -406,6 +413,7 @@ export async function openTelegramSettingsMenu<
406
413
  deps.isAutomaticThreadCleanupEnabled(),
407
414
  deps.getActivityVerbosity(),
408
415
  deps.getThreadDisplayMode?.(),
416
+ deps.isThreadDisplayCustom?.() ?? false,
409
417
  ),
410
418
  );
411
419
  if (messageId === undefined) return;
@@ -424,11 +432,14 @@ function threadDisplayModeLabel(mode: TelegramThreadDisplayMode): string {
424
432
  }
425
433
  }
426
434
 
427
- export function buildThreadDisplaySettingsReplyMarkup(mode: TelegramThreadDisplayMode): TelegramSettingsMenuReplyMarkup {
435
+ export function buildThreadDisplaySettingsReplyMarkup(
436
+ mode: TelegramThreadDisplayMode,
437
+ custom = false,
438
+ ): TelegramSettingsMenuReplyMarkup {
428
439
  return { inline_keyboard: [
429
440
  [{ text: "⬆️ Back", callback_data: "settings:list" }],
430
441
  ...(["letters", "names", "directory-title", "directory-snake"] as const).map((value) => [{
431
- text: `${mode === value ? "🟢 " : ""}${threadDisplayModeLabel(value)}`,
442
+ text: `${!custom && mode === value ? "🟢 " : ""}${threadDisplayModeLabel(value)}`,
432
443
  callback_data: `settings:set:thread-display:${value}`,
433
444
  }]),
434
445
  ] };
@@ -565,6 +576,7 @@ export async function updateTelegramSettingsMenuMessage(
565
576
  deps.isAutomaticThreadCleanupEnabled(),
566
577
  deps.getActivityVerbosity(),
567
578
  deps.getThreadDisplayMode?.(),
579
+ deps.isThreadDisplayCustom?.() ?? false,
568
580
  ),
569
581
  );
570
582
  }
@@ -661,8 +673,8 @@ export async function handleTelegramSettingsMenuCallbackAction(
661
673
  return true;
662
674
  }
663
675
  await deps.updateSettingsMessage(
664
- buildThreadDisplaySettingsText(mode),
665
- buildThreadDisplaySettingsReplyMarkup(mode),
676
+ buildThreadDisplaySettingsText(mode, deps.isThreadDisplayCustom?.() ?? false),
677
+ buildThreadDisplaySettingsReplyMarkup(mode, deps.isThreadDisplayCustom?.() ?? false),
666
678
  );
667
679
  await deps.answerCallbackQuery(callbackQueryId);
668
680
  return true;
@@ -877,6 +889,7 @@ export function createTelegramSettingsMenuRuntime<
877
889
  getTimeInjectionMode: deps.getTimeInjectionMode,
878
890
  isAutomaticThreadCleanupEnabled: deps.isAutomaticThreadCleanupEnabled,
879
891
  getThreadDisplayMode: deps.getThreadDisplayMode,
892
+ isThreadDisplayCustom: deps.isThreadDisplayCustom,
880
893
  sendSettingsMenu: (state, text, replyMarkup) =>
881
894
  deps.sendInteractiveMessage(
882
895
  state.chatId,
@@ -901,6 +914,7 @@ export function createTelegramSettingsMenuRuntime<
901
914
  getTimeInjectionMode: deps.getTimeInjectionMode,
902
915
  isAutomaticThreadCleanupEnabled: deps.isAutomaticThreadCleanupEnabled,
903
916
  getThreadDisplayMode: deps.getThreadDisplayMode,
917
+ isThreadDisplayCustom: deps.isThreadDisplayCustom,
904
918
  updateSettingsMessage: (text, replyMarkup) =>
905
919
  deps.editInteractiveMessage(
906
920
  state.chatId,
@@ -945,6 +959,7 @@ export function createTelegramSettingsMenuRuntime<
945
959
  getTimeInjectionMode: deps.getTimeInjectionMode,
946
960
  isAutomaticThreadCleanupEnabled: deps.isAutomaticThreadCleanupEnabled,
947
961
  getThreadDisplayMode: deps.getThreadDisplayMode,
962
+ isThreadDisplayCustom: deps.isThreadDisplayCustom,
948
963
  setThreadDisplayMode: deps.setThreadDisplayMode,
949
964
  setDraftPreviewsEnabled: deps.setDraftPreviewsEnabled,
950
965
  setAssistantRenderingMode: deps.setAssistantRenderingMode,
@@ -138,6 +138,7 @@ export interface PiExtensionApiRuntimePorts {
138
138
  getActiveTools: ExtensionAPI["getActiveTools"];
139
139
  setActiveTools: ExtensionAPI["setActiveTools"];
140
140
  setModel: ExtensionAPI["setModel"];
141
+ registerCommand: ExtensionAPI["registerCommand"];
141
142
  }
142
143
 
143
144
  export function createExtensionApiRuntimePorts(
@@ -151,6 +152,7 @@ export function createExtensionApiRuntimePorts(
151
152
  | "getActiveTools"
152
153
  | "setActiveTools"
153
154
  | "setModel"
155
+ | "registerCommand"
154
156
  >,
155
157
  ): PiExtensionApiRuntimePorts {
156
158
  return {
@@ -163,6 +165,7 @@ export function createExtensionApiRuntimePorts(
163
165
  getActiveTools: () => api.getActiveTools(),
164
166
  setActiveTools: (names) => api.setActiveTools(names),
165
167
  setModel: (model) => api.setModel(model),
168
+ registerCommand: (name, options) => api.registerCommand(name, options),
166
169
  };
167
170
  }
168
171
 
@@ -1474,6 +1474,7 @@ export interface TelegramAgentEndAssistantResult {
1474
1474
  text?: string;
1475
1475
  stopReason?: string;
1476
1476
  errorMessage?: string;
1477
+ recoveredFromEarlier?: boolean;
1477
1478
  }
1478
1479
 
1479
1480
  export interface TelegramAgentEndOutboundVoiceReply {
@@ -1592,6 +1593,7 @@ export interface TelegramAgentEndHookRuntimeDeps<
1592
1593
  extractAssistant: (
1593
1594
  messages: readonly TMessage[],
1594
1595
  ) => TelegramAgentEndAssistantResult;
1596
+ isRecoveredAssistantAlreadyPublished?: (assistant: TelegramAgentEndAssistantResult) => boolean;
1595
1597
  getFoldQueuedPromptsIntoHistory: () => boolean;
1596
1598
  resetRuntimeState: () => void;
1597
1599
  isSessionActive?: (ctx: TContext) => boolean;
@@ -1736,7 +1738,11 @@ export function createTelegramAgentEndHook<
1736
1738
  ): Promise<void> => {
1737
1739
  if (deps.isSessionActive?.(ctx) === false) return;
1738
1740
  const turn = deps.getActiveTurn();
1739
- const assistant = assistantOverride ?? (turn ? deps.extractAssistant(event.messages) : {});
1741
+ const extractedAssistant = assistantOverride ?? (turn ? deps.extractAssistant(event.messages) : {});
1742
+ const assistant = extractedAssistant.recoveredFromEarlier &&
1743
+ deps.isRecoveredAssistantAlreadyPublished?.(extractedAssistant)
1744
+ ? { stopReason: extractedAssistant.stopReason }
1745
+ : extractedAssistant;
1740
1746
  const hasPublication = !!assistant.text || assistant.stopReason === "error" || !!turn?.queuedAttachments.length;
1741
1747
  const reservation = turn && !turn.guestQueryId && hasPublication ? deps.reserveActiveTurnDelivery?.() : undefined;
1742
1748
  const scheduleDelivery = reservation?.schedule ?? deps.scheduleActiveTurnDelivery;
@@ -191,6 +191,7 @@ export function extractRunAssistantMessage(
191
191
  text?: string;
192
192
  stopReason?: string;
193
193
  errorMessage?: string;
194
+ recoveredFromEarlier?: boolean;
194
195
  } {
195
196
  const latest = extractLatestAssistantMessageText(messages);
196
197
  if (latest.text || latest.stopReason !== "stop") return latest;
@@ -213,6 +214,7 @@ export function extractRunAssistantMessage(
213
214
  stopReason: typeof rawStopReason === "string" ? rawStopReason : undefined,
214
215
  errorMessage:
215
216
  typeof rawErrorMessage === "string" ? rawErrorMessage : undefined,
217
+ recoveredFromEarlier: true,
216
218
  };
217
219
  }
218
220
  return latest;
@@ -698,6 +698,7 @@ export interface TelegramInboundRouteRuntimeDeps<
698
698
  ) => void;
699
699
  isIdle: (ctx: TContext) => boolean;
700
700
  hasPendingMessages: (ctx: TContext) => boolean;
701
+ requestNewSession?: (source: unknown) => void;
701
702
  compact: (
702
703
  ctx: TContext,
703
704
  callbacks: { onComplete: () => void; onError: (error: unknown) => void },
@@ -721,6 +722,7 @@ const TELEGRAM_OWNED_CALLBACK_PREFIXES = [
721
722
  "compact:",
722
723
  "menu:",
723
724
  "model:",
725
+ "new:",
724
726
  "queue:",
725
727
  "section:",
726
728
  "settings:",
@@ -1560,7 +1562,9 @@ export function createTelegramInboundRouteRuntime<
1560
1562
  await deps.callApi("editForumTopic", {
1561
1563
  chat_id: sourceTarget.chatId,
1562
1564
  message_thread_id: sourceTarget.threadId,
1563
- name: Threads.getTelegramTopicTitleForThreadName(threadName, slot),
1565
+ name:
1566
+ deps.getDisplayTitle?.(sourceTarget) ??
1567
+ Threads.getTelegramTopicTitleForThreadName(threadName, slot),
1564
1568
  });
1565
1569
  assertExecutionCurrent();
1566
1570
  } catch (renameError) {
@@ -1717,7 +1721,9 @@ export function createTelegramInboundRouteRuntime<
1717
1721
  await deps.callApi("editForumTopic", {
1718
1722
  chat_id: sourceTarget.chatId,
1719
1723
  message_thread_id: sourceTarget.threadId,
1720
- name: Threads.getTelegramTopicTitleForThreadName(threadName, slot),
1724
+ name:
1725
+ deps.getDisplayTitle?.(sourceTarget) ??
1726
+ Threads.getTelegramTopicTitleForThreadName(threadName, slot),
1721
1727
  });
1722
1728
  assertExecutionCurrent();
1723
1729
  } catch (renameError) {
@@ -1966,7 +1972,7 @@ export function createTelegramInboundRouteRuntime<
1966
1972
  await deps.editInteractiveMessage?.(
1967
1973
  chatId,
1968
1974
  dialogMessageId,
1969
- "<b>✖ Rename cancelled. Returning to normal agent mode.</b>",
1975
+ "<b>✖ Rename cancelled.</b>",
1970
1976
  "html",
1971
1977
  { inline_keyboard: [] },
1972
1978
  );
@@ -2001,6 +2007,42 @@ export function createTelegramInboundRouteRuntime<
2001
2007
  }
2002
2008
  return;
2003
2009
  }
2010
+ const handledByNew =
2011
+ await Commands.handleTelegramNewConfirmationCallback(query, {
2012
+ ctx,
2013
+ answerCallbackQuery: deps.answerCallbackQuery,
2014
+ editInteractiveMessage: deps.editInteractiveMessage ?? (async () => {}),
2015
+ deleteMessage: deps.deleteMessage ?? (async () => {}),
2016
+ runNew: async (newCtx) => {
2017
+ await Commands.handleTelegramNewCommand({
2018
+ isIdle: () => deps.isIdle(newCtx),
2019
+ hasPendingMessages: () => deps.hasPendingMessages(newCtx),
2020
+ hasActiveTelegramTurn: deps.activeTurnRuntime.has,
2021
+ hasDispatchPending: deps.bridgeRuntime.lifecycle.hasDispatchPending,
2022
+ hasQueuedTelegramItems: deps.telegramQueueStore.hasQueuedItems,
2023
+ isCompactionInProgress:
2024
+ deps.bridgeRuntime.lifecycle.isCompactionInProgress,
2025
+ requestNewSession: deps.requestNewSession
2026
+ ? () => deps.requestNewSession!(query)
2027
+ : undefined,
2028
+ sendTextReply: async (text) => {
2029
+ const chatId = query.message?.chat?.id;
2030
+ const messageId = query.message?.message_id;
2031
+ if (typeof chatId !== "number" || typeof messageId !== "number") return;
2032
+ await deps.editInteractiveMessage?.(
2033
+ chatId,
2034
+ messageId,
2035
+ text,
2036
+ "html",
2037
+ { inline_keyboard: [] },
2038
+ );
2039
+ },
2040
+ recordRuntimeEvent: deps.recordRuntimeEvent,
2041
+ });
2042
+ },
2043
+ });
2044
+ assertExecutionCurrent();
2045
+ if (handledByNew) return;
2004
2046
  const handledByCompact =
2005
2047
  await Commands.handleTelegramCompactConfirmationCallback(query, {
2006
2048
  ctx,
@@ -2186,6 +2228,7 @@ export function createTelegramInboundRouteRuntime<
2186
2228
  stopTypingLoop: deps.stopTypingLoop,
2187
2229
  enqueueContinueTurn,
2188
2230
  compact: deps.compact,
2231
+ requestNewSession: deps.requestNewSession,
2189
2232
  allocateItemOrder: deps.bridgeRuntime.queue.allocateItemOrder,
2190
2233
  allocateControlOrder: deps.bridgeRuntime.queue.allocateControlOrder,
2191
2234
  appendControlItem: deps.queueMutationRuntime.append,
@@ -2515,17 +2558,31 @@ export function createTelegramInboundRouteRuntime<
2515
2558
  dialogMessageId: candidate.dialogMessageId,
2516
2559
  });
2517
2560
  }
2561
+ const replyText = result.ok && !result.message
2562
+ ? Commands.formatTelegramAutomaticThreadDisplayNameRestoredHeading(
2563
+ result.threadName ?? name,
2564
+ )
2565
+ : Commands.formatTelegramInformationHeading(
2566
+ result.ok ? "✅" : "⚠️",
2567
+ result.message ?? "Thread display name reset failed.",
2568
+ );
2569
+ if (result.ok) {
2570
+ Updates.assertTelegramUpdateExecutionCurrent(message);
2571
+ Updates.reportTelegramUpdateCompleted(message);
2572
+ void deps.sendTextReply(
2573
+ target.chatId,
2574
+ message.message_id,
2575
+ replyText,
2576
+ { parseMode: "HTML", target },
2577
+ ).catch((error) => deps.recordRuntimeEvent?.("telegram-command", error, {
2578
+ command: "name", phase: "reset-result",
2579
+ }));
2580
+ return true;
2581
+ }
2518
2582
  await deps.sendTextReply(
2519
2583
  target.chatId,
2520
2584
  message.message_id,
2521
- result.ok && !result.message
2522
- ? Commands.formatTelegramAutomaticThreadDisplayNameRestoredHeading(
2523
- result.threadName ?? name,
2524
- )
2525
- : Commands.formatTelegramInformationHeading(
2526
- result.ok ? "✅" : "⚠️",
2527
- result.message ?? "Thread display name reset failed.",
2528
- ),
2585
+ replyText,
2529
2586
  { parseMode: "HTML", target },
2530
2587
  );
2531
2588
  return true;
@@ -2557,17 +2614,31 @@ export function createTelegramInboundRouteRuntime<
2557
2614
  dialogMessageId: candidate.dialogMessageId,
2558
2615
  });
2559
2616
  }
2617
+ const replyText = result?.ok && !result.message
2618
+ ? Commands.formatTelegramThreadDisplayNameSavedHeading(
2619
+ result.threadName ?? consumed.name,
2620
+ )
2621
+ : Commands.formatTelegramInformationHeading(
2622
+ result?.ok ? "✅" : "⚠️",
2623
+ result?.message ?? "Thread display name update failed.",
2624
+ );
2625
+ if (result?.ok) {
2626
+ Updates.assertTelegramUpdateExecutionCurrent(message);
2627
+ Updates.reportTelegramUpdateCompleted(message);
2628
+ void deps.sendTextReply(
2629
+ target.chatId,
2630
+ message.message_id,
2631
+ replyText,
2632
+ { parseMode: "HTML", target },
2633
+ ).catch((error) => deps.recordRuntimeEvent?.("telegram-command", error, {
2634
+ command: "name", phase: "rename-result",
2635
+ }));
2636
+ return true;
2637
+ }
2560
2638
  await deps.sendTextReply(
2561
2639
  target.chatId,
2562
2640
  message.message_id,
2563
- result?.ok && !result.message
2564
- ? Commands.formatTelegramThreadDisplayNameSavedHeading(
2565
- result.threadName ?? consumed.name,
2566
- )
2567
- : Commands.formatTelegramInformationHeading(
2568
- result?.ok ? "✅" : "⚠️",
2569
- result?.message ?? "Thread display name update failed.",
2570
- ),
2641
+ replyText,
2571
2642
  { parseMode: "HTML", target },
2572
2643
  );
2573
2644
  return true;
@@ -239,6 +239,36 @@ export interface TelegramThreadDisplayReconcilerDeps {
239
239
  ): Promise<TResponse>;
240
240
  }
241
241
 
242
+ export function createTelegramThreadDisplaySettingsRuntime(deps: {
243
+ getTarget(): { chatId: number; threadId?: number } | undefined;
244
+ getBinding(target: { chatId: number; threadId?: number }):
245
+ { manualThreadName?: string } | undefined;
246
+ apply(mode: TelegramThreadDisplayMode): Promise<void>;
247
+ reset(target: { chatId: number; threadId?: number }):
248
+ Promise<{ ok: boolean; message?: string }>;
249
+ }): {
250
+ isCustom(): boolean;
251
+ setMode(mode: TelegramThreadDisplayMode): Promise<void>;
252
+ } {
253
+ return {
254
+ isCustom() {
255
+ const target = deps.getTarget();
256
+ return !!target && typeof deps.getBinding(target)?.manualThreadName === "string";
257
+ },
258
+ async setMode(mode) {
259
+ const target = deps.getTarget();
260
+ const hadManualName = !!target &&
261
+ typeof deps.getBinding(target)?.manualThreadName === "string";
262
+ await deps.apply(mode);
263
+ if (!hadManualName || !target) return;
264
+ const reset = await deps.reset(target);
265
+ if (!reset.ok) {
266
+ throw new Error(reset.message ?? "Telegram Thread display override reset failed.");
267
+ }
268
+ },
269
+ };
270
+ }
271
+
242
272
  /** Caller owns triggering and reporting; no timer or background retry is created. */
243
273
  export function createTelegramThreadDisplayReconciler(
244
274
  deps: TelegramThreadDisplayReconcilerDeps,