@llblab/pi-kit 0.15.0 → 0.17.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 (120) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +2 -2
  3. package/node_modules/@llblab/pi-state-flow/AGENTS.md +12 -12
  4. package/node_modules/@llblab/pi-state-flow/BACKLOG.md +2 -13
  5. package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +31 -0
  6. package/node_modules/@llblab/pi-state-flow/README.md +11 -7
  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 +2 -1
  13. package/node_modules/@llblab/pi-state-flow/dist/lib/durable.js +15 -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 +7 -4
  17. package/node_modules/@llblab/pi-state-flow/dist/lib/extension.js +124 -274
  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/logging.d.ts +18 -0
  22. package/node_modules/@llblab/pi-state-flow/dist/lib/logging.js +44 -1
  23. package/node_modules/@llblab/pi-state-flow/dist/lib/migration.d.ts +2 -2
  24. package/node_modules/@llblab/pi-state-flow/dist/lib/migration.js +55 -21
  25. package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.d.ts +7 -0
  26. package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.js +65 -17
  27. package/node_modules/@llblab/pi-state-flow/dist/lib/publication.d.ts +17 -0
  28. package/node_modules/@llblab/pi-state-flow/dist/lib/publication.js +102 -0
  29. package/node_modules/@llblab/pi-state-flow/dist/lib/query.d.ts +43 -1
  30. package/node_modules/@llblab/pi-state-flow/dist/lib/query.js +268 -10
  31. package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.d.ts +2 -0
  32. package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.js +34 -5
  33. package/node_modules/@llblab/pi-state-flow/dist/lib/session.d.ts +17 -0
  34. package/node_modules/@llblab/pi-state-flow/dist/lib/session.js +38 -0
  35. package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.d.ts +5 -5
  36. package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.js +20 -24
  37. package/node_modules/@llblab/pi-state-flow/dist/lib/state.d.ts +11 -3
  38. package/node_modules/@llblab/pi-state-flow/dist/lib/state.js +14 -4
  39. package/node_modules/@llblab/pi-state-flow/dist/lib/status.js +1 -1
  40. package/node_modules/@llblab/pi-state-flow/dist/lib/storage.d.ts +2 -0
  41. package/node_modules/@llblab/pi-state-flow/dist/lib/storage.js +55 -20
  42. package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.d.ts +9 -6
  43. package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.js +10 -3
  44. package/node_modules/@llblab/pi-state-flow/dist/lib/transition.js +8 -3
  45. package/node_modules/@llblab/pi-state-flow/dist/package.json +1 -1
  46. package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-guide/SKILL.md +79 -0
  47. package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-memory/SKILL.md +23 -121
  48. package/node_modules/@llblab/pi-state-flow/docs/README.md +1 -0
  49. package/node_modules/@llblab/pi-state-flow/docs/architecture.md +27 -20
  50. package/node_modules/@llblab/pi-state-flow/docs/compatibility.md +1 -1
  51. package/node_modules/@llblab/pi-state-flow/docs/filesystem-recovery.md +3 -2
  52. package/node_modules/@llblab/pi-state-flow/docs/lazy-state.md +513 -0
  53. package/node_modules/@llblab/pi-state-flow/docs/usage.md +13 -10
  54. package/node_modules/@llblab/pi-state-flow/lib/config.ts +18 -15
  55. package/node_modules/@llblab/pi-state-flow/lib/context.ts +24 -0
  56. package/node_modules/@llblab/pi-state-flow/lib/continuation.ts +4 -3
  57. package/node_modules/@llblab/pi-state-flow/lib/durable.ts +14 -5
  58. package/node_modules/@llblab/pi-state-flow/lib/episode.ts +5 -0
  59. package/node_modules/@llblab/pi-state-flow/lib/extension.ts +119 -262
  60. package/node_modules/@llblab/pi-state-flow/lib/git.ts +23 -22
  61. package/node_modules/@llblab/pi-state-flow/lib/history.ts +8 -4
  62. package/node_modules/@llblab/pi-state-flow/lib/json.ts +31 -3
  63. package/node_modules/@llblab/pi-state-flow/lib/logging.ts +53 -1
  64. package/node_modules/@llblab/pi-state-flow/lib/migration.ts +51 -20
  65. package/node_modules/@llblab/pi-state-flow/lib/protocol.ts +61 -17
  66. package/node_modules/@llblab/pi-state-flow/lib/publication.ts +96 -0
  67. package/node_modules/@llblab/pi-state-flow/lib/query.ts +261 -9
  68. package/node_modules/@llblab/pi-state-flow/lib/runtime.ts +32 -4
  69. package/node_modules/@llblab/pi-state-flow/lib/session.ts +45 -0
  70. package/node_modules/@llblab/pi-state-flow/lib/snapshot.ts +22 -25
  71. package/node_modules/@llblab/pi-state-flow/lib/state.ts +22 -6
  72. package/node_modules/@llblab/pi-state-flow/lib/status.ts +1 -1
  73. package/node_modules/@llblab/pi-state-flow/lib/storage.ts +46 -18
  74. package/node_modules/@llblab/pi-state-flow/lib/telegram.ts +20 -6
  75. package/node_modules/@llblab/pi-state-flow/lib/transition.ts +8 -3
  76. package/node_modules/@llblab/pi-state-flow/package.json +1 -1
  77. package/node_modules/@llblab/pi-state-flow/skills/state-flow-guide/SKILL.md +79 -0
  78. package/node_modules/@llblab/pi-state-flow/skills/state-flow-memory/SKILL.md +23 -121
  79. package/node_modules/@llblab/pi-telegram/AGENTS.md +1 -1
  80. package/node_modules/@llblab/pi-telegram/BACKLOG.md +1 -0
  81. package/node_modules/@llblab/pi-telegram/CHANGELOG.md +7 -0
  82. package/node_modules/@llblab/pi-telegram/README.md +1 -0
  83. package/node_modules/@llblab/pi-telegram/dist/lib/bindings.d.ts +2 -1
  84. package/node_modules/@llblab/pi-telegram/dist/lib/bindings.js +2 -1
  85. package/node_modules/@llblab/pi-telegram/dist/lib/commands.d.ts +134 -2
  86. package/node_modules/@llblab/pi-telegram/dist/lib/commands.js +297 -16
  87. package/node_modules/@llblab/pi-telegram/dist/lib/delivery.d.ts +2 -0
  88. package/node_modules/@llblab/pi-telegram/dist/lib/delivery.js +11 -0
  89. package/node_modules/@llblab/pi-telegram/dist/lib/extension.js +68 -8
  90. package/node_modules/@llblab/pi-telegram/dist/lib/menu-settings.d.ts +4 -3
  91. package/node_modules/@llblab/pi-telegram/dist/lib/menu-settings.js +17 -13
  92. package/node_modules/@llblab/pi-telegram/dist/lib/pi.d.ts +2 -1
  93. package/node_modules/@llblab/pi-telegram/dist/lib/pi.js +1 -0
  94. package/node_modules/@llblab/pi-telegram/dist/lib/routing.d.ts +1 -0
  95. package/node_modules/@llblab/pi-telegram/dist/lib/routing.js +55 -5
  96. package/node_modules/@llblab/pi-telegram/dist/lib/thread-display.d.ts +23 -0
  97. package/node_modules/@llblab/pi-telegram/dist/lib/thread-display.js +20 -0
  98. package/node_modules/@llblab/pi-telegram/dist/lib/threads.d.ts +18 -0
  99. package/node_modules/@llblab/pi-telegram/dist/lib/threads.js +152 -6
  100. package/node_modules/@llblab/pi-telegram/dist/lib/updates.d.ts +1 -0
  101. package/node_modules/@llblab/pi-telegram/dist/lib/updates.js +5 -3
  102. package/node_modules/@llblab/pi-telegram/dist/package.json +1 -1
  103. package/node_modules/@llblab/pi-telegram/docs/architecture.md +3 -3
  104. package/node_modules/@llblab/pi-telegram/docs/callback-namespaces.md +1 -1
  105. package/node_modules/@llblab/pi-telegram/docs/public-api.md +4 -3
  106. package/node_modules/@llblab/pi-telegram/docs/sections.md +2 -2
  107. package/node_modules/@llblab/pi-telegram/docs/ui-style.md +2 -1
  108. package/node_modules/@llblab/pi-telegram/docs/updates.md +1 -1
  109. package/node_modules/@llblab/pi-telegram/lib/bindings.ts +3 -0
  110. package/node_modules/@llblab/pi-telegram/lib/commands.ts +466 -21
  111. package/node_modules/@llblab/pi-telegram/lib/delivery.ts +15 -0
  112. package/node_modules/@llblab/pi-telegram/lib/extension.ts +77 -9
  113. package/node_modules/@llblab/pi-telegram/lib/menu-settings.ts +25 -10
  114. package/node_modules/@llblab/pi-telegram/lib/pi.ts +3 -0
  115. package/node_modules/@llblab/pi-telegram/lib/routing.ts +84 -17
  116. package/node_modules/@llblab/pi-telegram/lib/thread-display.ts +30 -0
  117. package/node_modules/@llblab/pi-telegram/lib/threads.ts +199 -6
  118. package/node_modules/@llblab/pi-telegram/lib/updates.ts +5 -2
  119. package/node_modules/@llblab/pi-telegram/package.json +1 -1
  120. package/package.json +3 -3
@@ -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
 
@@ -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:",
@@ -1970,7 +1972,7 @@ export function createTelegramInboundRouteRuntime<
1970
1972
  await deps.editInteractiveMessage?.(
1971
1973
  chatId,
1972
1974
  dialogMessageId,
1973
- "<b>✖ Rename cancelled. Returning to normal agent mode.</b>",
1975
+ "<b>✖ Rename cancelled.</b>",
1974
1976
  "html",
1975
1977
  { inline_keyboard: [] },
1976
1978
  );
@@ -2005,6 +2007,42 @@ export function createTelegramInboundRouteRuntime<
2005
2007
  }
2006
2008
  return;
2007
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;
2008
2046
  const handledByCompact =
2009
2047
  await Commands.handleTelegramCompactConfirmationCallback(query, {
2010
2048
  ctx,
@@ -2190,6 +2228,7 @@ export function createTelegramInboundRouteRuntime<
2190
2228
  stopTypingLoop: deps.stopTypingLoop,
2191
2229
  enqueueContinueTurn,
2192
2230
  compact: deps.compact,
2231
+ requestNewSession: deps.requestNewSession,
2193
2232
  allocateItemOrder: deps.bridgeRuntime.queue.allocateItemOrder,
2194
2233
  allocateControlOrder: deps.bridgeRuntime.queue.allocateControlOrder,
2195
2234
  appendControlItem: deps.queueMutationRuntime.append,
@@ -2519,17 +2558,31 @@ export function createTelegramInboundRouteRuntime<
2519
2558
  dialogMessageId: candidate.dialogMessageId,
2520
2559
  });
2521
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
+ }
2522
2582
  await deps.sendTextReply(
2523
2583
  target.chatId,
2524
2584
  message.message_id,
2525
- result.ok && !result.message
2526
- ? Commands.formatTelegramAutomaticThreadDisplayNameRestoredHeading(
2527
- result.threadName ?? name,
2528
- )
2529
- : Commands.formatTelegramInformationHeading(
2530
- result.ok ? "✅" : "⚠️",
2531
- result.message ?? "Thread display name reset failed.",
2532
- ),
2585
+ replyText,
2533
2586
  { parseMode: "HTML", target },
2534
2587
  );
2535
2588
  return true;
@@ -2561,17 +2614,31 @@ export function createTelegramInboundRouteRuntime<
2561
2614
  dialogMessageId: candidate.dialogMessageId,
2562
2615
  });
2563
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
+ }
2564
2638
  await deps.sendTextReply(
2565
2639
  target.chatId,
2566
2640
  message.message_id,
2567
- result?.ok && !result.message
2568
- ? Commands.formatTelegramThreadDisplayNameSavedHeading(
2569
- result.threadName ?? consumed.name,
2570
- )
2571
- : Commands.formatTelegramInformationHeading(
2572
- result?.ok ? "✅" : "⚠️",
2573
- result?.message ?? "Thread display name update failed.",
2574
- ),
2641
+ replyText,
2575
2642
  { parseMode: "HTML", target },
2576
2643
  );
2577
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,