@llblab/pi-kit 0.5.2 → 0.7.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 (141) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +5 -5
  3. package/node_modules/@llblab/pi-actors/AGENTS.md +1 -1
  4. package/node_modules/@llblab/pi-actors/CHANGELOG.md +6 -0
  5. package/node_modules/@llblab/pi-actors/README.md +1 -1
  6. package/node_modules/@llblab/pi-actors/dist/lib/async-runs.d.ts +3 -0
  7. package/node_modules/@llblab/pi-actors/dist/lib/async-runs.js +14 -1
  8. package/node_modules/@llblab/pi-actors/dist/lib/command-templates.js +45 -3
  9. package/node_modules/@llblab/pi-actors/dist/lib/extension-runtime.js +1 -1
  10. package/node_modules/@llblab/pi-actors/dist/lib/observability.d.ts +16 -3
  11. package/node_modules/@llblab/pi-actors/dist/lib/observability.js +92 -7
  12. package/node_modules/@llblab/pi-actors/dist/lib/pi.d.ts +0 -1
  13. package/node_modules/@llblab/pi-actors/dist/lib/pi.js +15 -24
  14. package/node_modules/@llblab/pi-actors/dist/lib/run-delivery-lineage.d.ts +17 -0
  15. package/node_modules/@llblab/pi-actors/dist/lib/run-delivery-lineage.js +44 -0
  16. package/node_modules/@llblab/pi-actors/dist/lib/run-delivery.d.ts +4 -0
  17. package/node_modules/@llblab/pi-actors/dist/lib/run-delivery.js +102 -4
  18. package/node_modules/@llblab/pi-actors/dist/lib/run-ui-runtime.js +58 -39
  19. package/node_modules/@llblab/pi-actors/dist/lib/runtime.js +14 -6
  20. package/node_modules/@llblab/pi-actors/dist/skills/actors/SKILL.md +1 -1
  21. package/node_modules/@llblab/pi-actors/docs/async-runs.md +1 -1
  22. package/node_modules/@llblab/pi-actors/docs/coordinator-delivery.md +18 -23
  23. package/node_modules/@llblab/pi-actors/lib/async-runs.ts +18 -1
  24. package/node_modules/@llblab/pi-actors/lib/command-templates.ts +41 -3
  25. package/node_modules/@llblab/pi-actors/lib/extension-runtime.ts +1 -1
  26. package/node_modules/@llblab/pi-actors/lib/observability.ts +119 -5
  27. package/node_modules/@llblab/pi-actors/lib/pi.ts +15 -28
  28. package/node_modules/@llblab/pi-actors/lib/run-delivery-lineage.ts +68 -0
  29. package/node_modules/@llblab/pi-actors/lib/run-delivery.ts +120 -4
  30. package/node_modules/@llblab/pi-actors/lib/run-ui-runtime.ts +69 -44
  31. package/node_modules/@llblab/pi-actors/lib/runtime.ts +17 -6
  32. package/node_modules/@llblab/pi-actors/package.json +1 -1
  33. package/node_modules/@llblab/pi-actors/skills/actors/SKILL.md +1 -1
  34. package/node_modules/@llblab/pi-grow-loop/AGENTS.md +2 -2
  35. package/node_modules/@llblab/pi-grow-loop/CHANGELOG.md +4 -0
  36. package/node_modules/@llblab/pi-grow-loop/README.md +6 -6
  37. package/node_modules/@llblab/pi-grow-loop/index.ts +6 -3
  38. package/node_modules/@llblab/pi-grow-loop/package.json +1 -1
  39. package/node_modules/@llblab/pi-state-flow/AGENTS.md +33 -12
  40. package/node_modules/@llblab/pi-state-flow/BACKLOG.md +125 -2
  41. package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +87 -45
  42. package/node_modules/@llblab/pi-state-flow/README.md +203 -107
  43. package/node_modules/@llblab/pi-state-flow/docs/README.md +4 -0
  44. package/node_modules/@llblab/pi-state-flow/docs/architecture.md +188 -0
  45. package/node_modules/@llblab/pi-state-flow/docs/temporal-acceptance.md +42 -0
  46. package/node_modules/@llblab/pi-state-flow/index.ts +164 -2
  47. package/node_modules/@llblab/pi-state-flow/lib/acquisition.ts +138 -0
  48. package/node_modules/@llblab/pi-state-flow/lib/artifact.ts +273 -0
  49. package/node_modules/@llblab/pi-state-flow/lib/config.ts +48 -0
  50. package/node_modules/@llblab/pi-state-flow/lib/context.ts +18 -5
  51. package/node_modules/@llblab/pi-state-flow/lib/continuation.ts +268 -0
  52. package/node_modules/@llblab/pi-state-flow/lib/discovery.ts +117 -0
  53. package/node_modules/@llblab/pi-state-flow/lib/durable.ts +562 -0
  54. package/node_modules/@llblab/pi-state-flow/lib/episode.ts +24 -12
  55. package/node_modules/@llblab/pi-state-flow/lib/extension.ts +606 -70
  56. package/node_modules/@llblab/pi-state-flow/lib/git.ts +666 -0
  57. package/node_modules/@llblab/pi-state-flow/lib/history.ts +95 -0
  58. package/node_modules/@llblab/pi-state-flow/lib/json.ts +24 -0
  59. package/node_modules/@llblab/pi-state-flow/lib/maintenance.ts +141 -0
  60. package/node_modules/@llblab/pi-state-flow/lib/memory.ts +52 -0
  61. package/node_modules/@llblab/pi-state-flow/lib/migration.ts +88 -0
  62. package/node_modules/@llblab/pi-state-flow/lib/publication.ts +296 -0
  63. package/node_modules/@llblab/pi-state-flow/lib/recovery.ts +23 -7
  64. package/node_modules/@llblab/pi-state-flow/lib/rehydration.ts +79 -0
  65. package/node_modules/@llblab/pi-state-flow/lib/runtime.ts +264 -0
  66. package/node_modules/@llblab/pi-state-flow/lib/session.ts +6 -0
  67. package/node_modules/@llblab/pi-state-flow/lib/skills.ts +99 -7
  68. package/node_modules/@llblab/pi-state-flow/lib/snapshot.ts +305 -48
  69. package/node_modules/@llblab/pi-state-flow/lib/state.ts +74 -7
  70. package/node_modules/@llblab/pi-state-flow/lib/status.ts +125 -6
  71. package/node_modules/@llblab/pi-state-flow/lib/storage.ts +196 -0
  72. package/node_modules/@llblab/pi-state-flow/lib/temporal.ts +233 -0
  73. package/node_modules/@llblab/pi-state-flow/lib/terminal.ts +70 -24
  74. package/node_modules/@llblab/pi-state-flow/lib/transition.ts +254 -29
  75. package/node_modules/@llblab/pi-state-flow/package.json +8 -2
  76. package/node_modules/@llblab/pi-state-flow/skills/state-flow-memory/SKILL.md +128 -0
  77. package/node_modules/@llblab/pi-telegram/AGENTS.md +14 -9
  78. package/node_modules/@llblab/pi-telegram/BACKLOG.md +20 -4
  79. package/node_modules/@llblab/pi-telegram/CHANGELOG.md +22 -5
  80. package/node_modules/@llblab/pi-telegram/README.md +13 -9
  81. package/node_modules/@llblab/pi-telegram/docs/README.md +1 -0
  82. package/node_modules/@llblab/pi-telegram/docs/architecture.md +220 -18
  83. package/node_modules/@llblab/pi-telegram/docs/generative-apps.md +1 -1
  84. package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +70 -19
  85. package/node_modules/@llblab/pi-telegram/docs/outbound.md +13 -7
  86. package/node_modules/@llblab/pi-telegram/docs/public-api.md +13 -5
  87. package/node_modules/@llblab/pi-telegram/docs/ui-style.md +3 -1
  88. package/node_modules/@llblab/pi-telegram/index.ts +4 -1415
  89. package/node_modules/@llblab/pi-telegram/lib/activity.ts +19 -5
  90. package/node_modules/@llblab/pi-telegram/lib/agent-messages.ts +6 -3
  91. package/node_modules/@llblab/pi-telegram/lib/bindings.ts +37 -2
  92. package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +600 -135
  93. package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +962 -55
  94. package/node_modules/@llblab/pi-telegram/lib/bus.ts +350 -26
  95. package/node_modules/@llblab/pi-telegram/lib/channel-posts.ts +544 -0
  96. package/node_modules/@llblab/pi-telegram/lib/commands.ts +234 -11
  97. package/node_modules/@llblab/pi-telegram/lib/config.ts +178 -25
  98. package/node_modules/@llblab/pi-telegram/lib/delivery.ts +18 -18
  99. package/node_modules/@llblab/pi-telegram/lib/extension.ts +1792 -0
  100. package/node_modules/@llblab/pi-telegram/lib/generative-apps.ts +20 -2
  101. package/node_modules/@llblab/pi-telegram/lib/journal.ts +2184 -126
  102. package/node_modules/@llblab/pi-telegram/lib/lifecycle.ts +7 -1
  103. package/node_modules/@llblab/pi-telegram/lib/locks.ts +38 -1
  104. package/node_modules/@llblab/pi-telegram/lib/menu-settings.ts +154 -15
  105. package/node_modules/@llblab/pi-telegram/lib/outbound-attachments.ts +74 -40
  106. package/node_modules/@llblab/pi-telegram/lib/outbound-voice.ts +28 -42
  107. package/node_modules/@llblab/pi-telegram/lib/outbound.ts +18 -14
  108. package/node_modules/@llblab/pi-telegram/lib/paths.ts +29 -0
  109. package/node_modules/@llblab/pi-telegram/lib/polling.ts +85 -17
  110. package/node_modules/@llblab/pi-telegram/lib/preview.ts +115 -70
  111. package/node_modules/@llblab/pi-telegram/lib/prompts.ts +5 -2
  112. package/node_modules/@llblab/pi-telegram/lib/queue.ts +66 -22
  113. package/node_modules/@llblab/pi-telegram/lib/replies.ts +47 -39
  114. package/node_modules/@llblab/pi-telegram/lib/routing.ts +305 -112
  115. package/node_modules/@llblab/pi-telegram/lib/status.ts +10 -0
  116. package/node_modules/@llblab/pi-telegram/lib/sync.ts +308 -39
  117. package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +315 -7
  118. package/node_modules/@llblab/pi-telegram/lib/thread-cleanup-manager.ts +664 -0
  119. package/node_modules/@llblab/pi-telegram/lib/thread-display.ts +226 -0
  120. package/node_modules/@llblab/pi-telegram/lib/thread-naming.ts +118 -0
  121. package/node_modules/@llblab/pi-telegram/lib/threads.ts +1686 -129
  122. package/node_modules/@llblab/pi-telegram/lib/updates.ts +1319 -97
  123. package/node_modules/@llblab/pi-telegram/lib/workspace-admission.ts +1643 -0
  124. package/node_modules/@llblab/pi-telegram/lib/workspace-retirement.ts +968 -0
  125. package/node_modules/@llblab/pi-telegram/lib/workspace-slots.ts +84 -0
  126. package/node_modules/@llblab/pi-telegram/package.json +1 -1
  127. package/node_modules/@llblab/pi-telegram/screenshot.png +0 -0
  128. package/node_modules/@llblab/pi-telegram/scripts/measure-bus.mjs +83 -0
  129. package/node_modules/@llblab/pi-telegram/scripts/measure-workspace.mjs +101 -0
  130. package/node_modules/@llblab/{skills → pi-telegram/skills}/show-me/SKILL.md +28 -6
  131. package/node_modules/@llblab/pi-telegram/skills/show-me/references/telegram-surfaces.md +43 -0
  132. package/node_modules/@llblab/pi-telegram/skills/telegram-bridge/references/delivery-and-threads.md +1 -1
  133. package/node_modules/@llblab/skills/abcd-context/AGENTS.md +1 -0
  134. package/node_modules/@llblab/skills/abcd-context/CHANGELOG.md +6 -2
  135. package/node_modules/@llblab/skills/abcd-context/SKILL.md +1 -1
  136. package/node_modules/@llblab/skills/abcd-context/docs/validation-design.md +11 -5
  137. package/node_modules/@llblab/skills/abcd-context/scripts/_self-test.mjs +61 -0
  138. package/node_modules/@llblab/skills/abcd-context/scripts/validate-context.mjs +67 -0
  139. package/node_modules/@llblab/skills/package.json +2 -3
  140. package/node_modules/@llblab/skills/release-flow/SKILL.md +2 -4
  141. package/package.json +7 -6
@@ -19,6 +19,7 @@ import * as Queue from "./queue.ts";
19
19
  import type { TelegramBridgeRuntime } from "./runtime.ts";
20
20
  import type { TelegramSectionRegistry } from "./sections.ts";
21
21
  import * as TextGroups from "./text-groups.ts";
22
+ import * as ThreadNaming from "./thread-naming.ts";
22
23
  import * as ThreadReconciler from "./thread-reconciler.ts";
23
24
  import type {
24
25
  TelegramInstanceThreadIdentityCandidate,
@@ -175,6 +176,8 @@ function escapeHtml(text: string): string {
175
176
  const TELEGRAM_UNBOUND_REROUTE_CALLBACK_PREFIX = "reroute:";
176
177
  const TELEGRAM_UNBOUND_REROUTE_RESTORE_MENU_CALLBACK_PREFIX = "rerouterestore:";
177
178
  const TELEGRAM_UNBOUND_REROUTE_NEW_SLOT_CALLBACK_PREFIX = "reroutenew:";
179
+ const TELEGRAM_SLOT_CAPACITY_MESSAGE =
180
+ "No Telegram instance slot is available. Automatic reclamation is disabled for safety.";
178
181
 
179
182
  function formatTelegramUnboundRerouteCallbackData(
180
183
  rerouteId: string,
@@ -218,20 +221,9 @@ function parseTelegramUnboundRerouteCallbackData(
218
221
 
219
222
  function getTelegramThreadRecordLabel(
220
223
  record: Threads.TelegramTopicTargetRecord,
224
+ getDisplayTitle?: Threads.TelegramCurrentThreadAssembly["getDisplayTitle"],
221
225
  ): string {
222
- return getRestoredThreadName(record, record.slot ?? "");
223
- }
224
-
225
- function getTelegramRouteThreadButtonLabel(
226
- record: Threads.TelegramTopicTargetRecord,
227
- ): string {
228
- return `↪️ ${getTelegramThreadRecordLabel(record)}`;
229
- }
230
-
231
- function getTelegramReplaceThreadButtonLabel(
232
- record: Threads.TelegramTopicTargetRecord,
233
- ): string {
234
- return `➡️ ${getTelegramThreadRecordLabel(record)}`;
226
+ return getDisplayTitle?.(record.target) ?? getRestoredThreadName(record, record.slot ?? "");
235
227
  }
236
228
 
237
229
  function getNextTelegramSlotPreference(
@@ -288,13 +280,16 @@ function formatTelegramAllTabMenuChooserText(command: string): string {
288
280
  function buildTelegramUnboundRerouteChooserMarkup(
289
281
  rerouteId: string,
290
282
  records: readonly Threads.TelegramTopicTargetRecord[],
291
- options: { canRestore: boolean },
283
+ options: {
284
+ canRestore: boolean;
285
+ getDisplayTitle?: Threads.TelegramCurrentThreadAssembly["getDisplayTitle"];
286
+ },
292
287
  ): Menu.TelegramReplyMarkup {
293
288
  const activeRecords = records.filter((record) => record.status === "active");
294
289
  const canRestoreAnyLiveThread = options.canRestore && activeRecords.length > 0;
295
290
  const rows = activeRecords.map((record) => [
296
291
  {
297
- text: getTelegramRouteThreadButtonLabel(record),
292
+ text: `↪️ ${getTelegramThreadRecordLabel(record, options.getDisplayTitle)}`,
298
293
  callback_data: formatTelegramUnboundRerouteCallbackData(
299
294
  rerouteId,
300
295
  record.target.threadId,
@@ -320,13 +315,14 @@ function buildTelegramUnboundRerouteChooserMarkup(
320
315
  function buildTelegramUnboundRerouteRestoreChooserMarkup(
321
316
  rerouteId: string,
322
317
  records: readonly Threads.TelegramTopicTargetRecord[],
318
+ getDisplayTitle?: Threads.TelegramCurrentThreadAssembly["getDisplayTitle"],
323
319
  ): Menu.TelegramReplyMarkup {
324
320
  return {
325
321
  inline_keyboard: records
326
322
  .filter((record) => record.status === "active")
327
323
  .map((record) => [
328
324
  {
329
- text: getTelegramReplaceThreadButtonLabel(record),
325
+ text: `➡️ ${getTelegramThreadRecordLabel(record, getDisplayTitle)}`,
330
326
  callback_data: formatTelegramUnboundRerouteNewSlotCallbackData(
331
327
  rerouteId,
332
328
  record.target.threadId,
@@ -527,7 +523,7 @@ export interface TelegramInboundRouteRuntimeDeps<
527
523
  > {
528
524
  configStore: Pick<
529
525
  TelegramConfigStore,
530
- "get" | "getAllowedUserId" | "setAllowedUserId" | "persist"
526
+ "get" | "getAllowedUserId" | "persistAllowedUserId" | "persist"
531
527
  > & { set?: TelegramConfigStore["set"] };
532
528
  callApi?: <TResponse>(
533
529
  method: string,
@@ -540,6 +536,7 @@ export interface TelegramInboundRouteRuntimeDeps<
540
536
  getTargetOwnership?: Updates.TelegramTargetOwnershipLookup;
541
537
  recordMessageOwnership?: Updates.TelegramMessageOwnershipRecorder;
542
538
  getLiveThreadTargets?: () => Queue.TelegramQueueTarget[];
539
+ getDisplayTitle?: Threads.TelegramCurrentThreadAssembly["getDisplayTitle"];
543
540
  getLocalThreadLabelForTarget?: (
544
541
  target: Queue.TelegramQueueTarget,
545
542
  ) => string | undefined;
@@ -616,6 +613,14 @@ export interface TelegramInboundRouteRuntimeDeps<
616
613
  ) => Promise<false | "new" | "edit">;
617
614
  inboundHandlerRuntime: TelegramInboundHandlerRuntime<TContext>;
618
615
  threadStore?: Threads.TelegramTopicTargetStore;
616
+ runWorkspaceOperation?: <T>(
617
+ input: {
618
+ operationId: string;
619
+ operationKind: string;
620
+ scopes: readonly [{ kind: "profile" }];
621
+ },
622
+ operation: () => Promise<T>,
623
+ ) => Promise<T>;
619
624
  updateStatus: (ctx: TContext, error?: string) => void;
620
625
  isContextActive?: (ctx: TContext) => boolean;
621
626
  dispatchNextQueuedTelegramTurn: (ctx: TContext) => void;
@@ -661,6 +666,9 @@ export interface TelegramInboundRouteRuntimeDeps<
661
666
  options?: { parseMode?: "HTML"; target?: Queue.TelegramQueueTarget },
662
667
  ) => Promise<number | undefined>;
663
668
  setMyCommands: Commands.TelegramBotCommandRegistrationDeps["setMyCommands"];
669
+ validateThreadName?: (threadName: string) => string | undefined;
670
+ renameCurrentThread?: Commands.TelegramThreadDisplayNameRenamePort;
671
+ resetCurrentThreadName?: Commands.TelegramThreadDisplayNameResetPort;
664
672
  getCommands: () => Parameters<
665
673
  typeof PromptTemplates.getTelegramPromptTemplateCommands
666
674
  >[0];
@@ -785,7 +793,8 @@ export function createTelegramInboundRouteRuntime<
785
793
  const chatId = message.chat.id;
786
794
  const threadId = message.message_thread_id;
787
795
  if (!threadId) return undefined;
788
- const localLabel = deps.getLocalThreadLabelForTarget?.({ chatId, threadId });
796
+ const localLabel = deps.getDisplayTitle?.({ chatId, threadId }) ??
797
+ deps.getLocalThreadLabelForTarget?.({ chatId, threadId });
789
798
  if (localLabel) return localLabel;
790
799
  if (!deps.threadStore) return undefined;
791
800
  const records = deps.threadStore.list();
@@ -964,6 +973,8 @@ export function createTelegramInboundRouteRuntime<
964
973
  timer: ReturnType<typeof setTimeout>;
965
974
  }
966
975
  >();
976
+ const threadNameDialog = ThreadNaming.createTelegramThreadNameDialogRuntime();
977
+ const getThreadNameDialogScope = () => deps.getCurrentInstanceId?.() ?? "local";
967
978
  const menuCallbackHandler = Menu.createTelegramMenuCallbackHandlerForContext<
968
979
  TCallbackQuery,
969
980
  TContext,
@@ -1328,6 +1339,7 @@ export function createTelegramInboundRouteRuntime<
1328
1339
  const replyMarkup = buildTelegramUnboundRerouteRestoreChooserMarkup(
1329
1340
  parsed.rerouteId,
1330
1341
  activeRecords,
1342
+ deps.getDisplayTitle,
1331
1343
  );
1332
1344
  if (deps.editInteractiveMessage) {
1333
1345
  await deps.editInteractiveMessage(
@@ -1352,7 +1364,7 @@ export function createTelegramInboundRouteRuntime<
1352
1364
  await deps.answerCallbackQuery(query.id, "Choose instance to restore.");
1353
1365
  return true;
1354
1366
  };
1355
- const executeUnboundRerouteCallback = async (
1367
+ const executeUnboundRerouteCallbackOperation = async (
1356
1368
  query: TCallbackQuery,
1357
1369
  ctx: TContext,
1358
1370
  ): Promise<boolean> => {
@@ -1471,6 +1483,13 @@ export function createTelegramInboundRouteRuntime<
1471
1483
  currentInstanceId,
1472
1484
  );
1473
1485
  if (parsed.useNewSlot && !isCurrentLeaderRecord) {
1486
+ if (!record.slot || !/^[A-Z]$/u.test(record.slot)) {
1487
+ await deps.answerCallbackQuery(
1488
+ query.id,
1489
+ "Follower thread slot authority is unavailable.",
1490
+ );
1491
+ return true;
1492
+ }
1474
1493
  if (
1475
1494
  !sourceTarget ||
1476
1495
  !deps.replaceFollowerThreadTarget ||
@@ -1497,7 +1516,7 @@ export function createTelegramInboundRouteRuntime<
1497
1516
  return true;
1498
1517
  }
1499
1518
  const nowMs = Date.now();
1500
- const slot = record.slot ?? "?";
1519
+ const slot = record.slot;
1501
1520
  const threadName = getRestoredThreadName(record, slot);
1502
1521
  assertExecutionCurrent();
1503
1522
  deps.threadStore.markStaleByTarget(
@@ -1640,18 +1659,21 @@ export function createTelegramInboundRouteRuntime<
1640
1659
  (record.target.chatId !== sourceTarget.chatId ||
1641
1660
  record.target.threadId !== sourceTarget.threadId)
1642
1661
  ) {
1662
+ const slot = deps.threadStore.allocateSlot(
1663
+ leaderProfileKey ?? record.profileKey,
1664
+ getNextTelegramSlotPreference(record.slot),
1665
+ undefined,
1666
+ { excludeCurrentRecord: true },
1667
+ );
1668
+ if (!slot) {
1669
+ await deps.answerCallbackQuery(query.id, TELEGRAM_SLOT_CAPACITY_MESSAGE);
1670
+ return true;
1671
+ }
1643
1672
  deps.threadStore.markStaleByTarget(
1644
1673
  record.target,
1645
1674
  "deleted",
1646
1675
  "Current leader thread was replaced by a new-slot reroute source.",
1647
1676
  );
1648
- const slot =
1649
- deps.threadStore.allocateSlot(
1650
- leaderProfileKey ?? record.profileKey,
1651
- getNextTelegramSlotPreference(record.slot),
1652
- ) ??
1653
- record.slot ??
1654
- "?";
1655
1677
  const nowMs = Date.now();
1656
1678
  const threadName = getRestoredThreadName(record, slot);
1657
1679
  deps.threadStore.upsert({
@@ -1759,6 +1781,23 @@ export function createTelegramInboundRouteRuntime<
1759
1781
  );
1760
1782
  return true;
1761
1783
  };
1784
+ const executeUnboundRerouteCallback = (
1785
+ query: TCallbackQuery,
1786
+ ctx: TContext,
1787
+ ): Promise<boolean> => {
1788
+ if (!parseTelegramUnboundRerouteCallbackData(query.data) ||
1789
+ !deps.runWorkspaceOperation) {
1790
+ return executeUnboundRerouteCallbackOperation(query, ctx);
1791
+ }
1792
+ return deps.runWorkspaceOperation(
1793
+ {
1794
+ operationId: `workspace-reroute:${query.id}`,
1795
+ operationKind: "workspace.route-unbound-thread",
1796
+ scopes: [{ kind: "profile" }],
1797
+ },
1798
+ () => executeUnboundRerouteCallbackOperation(query, ctx),
1799
+ );
1800
+ };
1762
1801
  const handleUnboundRerouteCallback = async (
1763
1802
  query: TCallbackQuery,
1764
1803
  ctx: TContext,
@@ -1881,6 +1920,70 @@ export function createTelegramInboundRouteRuntime<
1881
1920
  assertExecutionCurrent();
1882
1921
  if (handled) return;
1883
1922
  }
1923
+ if (query.data?.startsWith("thread-name:")) {
1924
+ const chatId = query.message?.chat?.id;
1925
+ const dialogMessageId = query.message?.message_id;
1926
+ if (typeof chatId !== "number" || typeof dialogMessageId !== "number") {
1927
+ await deps.answerCallbackQuery(query.id, "⌛ Rename dialog expired.");
1928
+ return;
1929
+ }
1930
+ const target = typeof query.message?.message_thread_id === "number"
1931
+ ? { chatId, threadId: query.message.message_thread_id }
1932
+ : { chatId };
1933
+ const action = query.data.slice("thread-name:".length);
1934
+ if (action !== "reset" && action !== "cancel") {
1935
+ await deps.answerCallbackQuery(query.id, "⌛ Rename dialog expired.");
1936
+ return;
1937
+ }
1938
+ const selected = threadNameDialog.select({
1939
+ scope: getThreadNameDialogScope(),
1940
+ target,
1941
+ dialogMessageId,
1942
+ action,
1943
+ });
1944
+ if (selected.kind === "expired") {
1945
+ await deps.answerCallbackQuery(query.id, "⌛ Rename dialog expired.");
1946
+ return;
1947
+ }
1948
+ if (selected.kind === "cancel") {
1949
+ await deps.editInteractiveMessage?.(
1950
+ chatId,
1951
+ dialogMessageId,
1952
+ "<b>✖ Rename cancelled. Returning to normal agent mode.</b>",
1953
+ "html",
1954
+ { inline_keyboard: [] },
1955
+ );
1956
+ await deps.answerCallbackQuery(query.id);
1957
+ return;
1958
+ }
1959
+ try {
1960
+ const result = await deps.resetCurrentThreadName?.(target);
1961
+ if (!result?.ok) {
1962
+ throw new Error(result?.message ?? "Thread display name reset is unavailable.");
1963
+ }
1964
+ await deps.editInteractiveMessage?.(
1965
+ chatId,
1966
+ dialogMessageId,
1967
+ result.message
1968
+ ? Commands.formatTelegramInformationHeading("✅", result.message)
1969
+ : Commands.formatTelegramAutomaticThreadDisplayNameRestoredHeading(
1970
+ result.threadName ?? "automatic",
1971
+ ),
1972
+ "html",
1973
+ { inline_keyboard: [] },
1974
+ );
1975
+ await deps.answerCallbackQuery(query.id);
1976
+ } catch (error) {
1977
+ threadNameDialog.open({
1978
+ scope: getThreadNameDialogScope(), target, dialogMessageId,
1979
+ });
1980
+ deps.recordRuntimeEvent?.("telegram-command", error, {
1981
+ command: "name", phase: "reset",
1982
+ });
1983
+ await deps.answerCallbackQuery(query.id, "⚠️ Thread name reset failed.");
1984
+ }
1985
+ return;
1986
+ }
1884
1987
  const handledByCompact =
1885
1988
  await Commands.handleTelegramCompactConfirmationCallback(query, {
1886
1989
  ctx,
@@ -2083,10 +2186,54 @@ export function createTelegramInboundRouteRuntime<
2083
2186
  },
2084
2187
  openSettingsMenu: deps.openSettingsMenu,
2085
2188
  getAllowedUserId: deps.configStore.getAllowedUserId,
2086
- setAllowedUserId: deps.configStore.setAllowedUserId,
2189
+ persistAllowedUserId: deps.configStore.persistAllowedUserId,
2087
2190
  setMyCommands: deps.setMyCommands,
2191
+ validateThreadName: deps.validateThreadName,
2192
+ renameCurrentThread: deps.renameCurrentThread,
2193
+ resetCurrentThreadName: deps.resetCurrentThreadName,
2194
+ openThreadNameDialog: async (message) => {
2195
+ const target = Updates.getTelegramMessageTarget(message);
2196
+ if (!deps.sendInteractiveMessage || !target) {
2197
+ await deps.sendTextReply(
2198
+ message.chat.id,
2199
+ message.message_id,
2200
+ Commands.formatTelegramInformationHeading(
2201
+ "🏷️",
2202
+ "Usage: /name Navigator",
2203
+ ),
2204
+ { parseMode: "HTML", target },
2205
+ );
2206
+ return;
2207
+ }
2208
+ const hasManualName = deps.threadStore?.listWorkspaceBindings().some(
2209
+ (binding) => binding.target.chatId === target.chatId &&
2210
+ binding.target.threadId === target.threadId &&
2211
+ typeof binding.manualThreadName === "string",
2212
+ ) ?? false;
2213
+ const instructions = hasManualName
2214
+ ? "<b>🏷️ Send a new Thread name using printable ASCII, reset to automatic, or cancel.</b>"
2215
+ : "<b>🏷️ Send a Thread name using printable ASCII, or cancel.</b>";
2216
+ const buttons = hasManualName
2217
+ ? [
2218
+ { text: "↩️ Reset to automatic", callback_data: "thread-name:reset" },
2219
+ { text: "✖ Cancel rename", callback_data: "thread-name:cancel" },
2220
+ ]
2221
+ : [{ text: "✖ Cancel rename", callback_data: "thread-name:cancel" }];
2222
+ const dialogMessageId = await deps.sendInteractiveMessage(
2223
+ target.chatId,
2224
+ instructions,
2225
+ "html",
2226
+ { inline_keyboard: buttons.map((button) => [button]) },
2227
+ { target },
2228
+ );
2229
+ if (typeof dialogMessageId !== "number") return;
2230
+ threadNameDialog.open({
2231
+ scope: getThreadNameDialogScope(),
2232
+ target,
2233
+ dialogMessageId,
2234
+ });
2235
+ },
2088
2236
  getPromptTemplateCommands,
2089
- persistConfig: deps.configStore.persist,
2090
2237
  sendTextReply: deps.sendTextReply,
2091
2238
  getActiveTurnReply: () => {
2092
2239
  const activeTurn = deps.activeTurnRuntime.get();
@@ -2158,7 +2305,7 @@ export function createTelegramInboundRouteRuntime<
2158
2305
  message.message_id,
2159
2306
  [
2160
2307
  includeGuidance ? formatTelegramUnboundTopicGuidance() : undefined,
2161
- "This thread is not bound to a Pi instance. Open an active Pi thread or run /telegram-connect from a Pi session to bind one.",
2308
+ `This thread is not bound to a Pi instance. Open an active Pi thread or run ${Commands.formatTelegramPiCommandHtml("/telegram-connect")} from a Pi session to bind one.`,
2162
2309
  ]
2163
2310
  .filter((line): line is string => typeof line === "string")
2164
2311
  .join("\n\n"),
@@ -2178,7 +2325,7 @@ export function createTelegramInboundRouteRuntime<
2178
2325
  const replyMarkup = buildTelegramUnboundRerouteChooserMarkup(
2179
2326
  rerouteId,
2180
2327
  activeRecords,
2181
- { canRestore: sourceTarget !== undefined },
2328
+ { canRestore: sourceTarget !== undefined, getDisplayTitle: deps.getDisplayTitle },
2182
2329
  );
2183
2330
  if (deps.sendInteractiveMessage) {
2184
2331
  const chooserId = await deps.sendInteractiveMessage(
@@ -2262,7 +2409,7 @@ export function createTelegramInboundRouteRuntime<
2262
2409
  const replyMarkup = buildTelegramUnboundRerouteChooserMarkup(
2263
2410
  rerouteId,
2264
2411
  activeRecords,
2265
- { canRestore: typeof message.message_thread_id === "number" },
2412
+ { canRestore: typeof message.message_thread_id === "number", getDisplayTitle: deps.getDisplayTitle },
2266
2413
  );
2267
2414
  let chooserId: number | undefined;
2268
2415
  try {
@@ -2311,6 +2458,7 @@ export function createTelegramInboundRouteRuntime<
2311
2458
  throw error;
2312
2459
  }
2313
2460
  rememberRerouteChooser(rerouteId, chooserId);
2461
+ Updates.reportTelegramUpdateCompleted(commandMessage);
2314
2462
  return true;
2315
2463
  };
2316
2464
  const commandOrPrompt = Commands.createTelegramCommandOrPromptRuntime<
@@ -2323,6 +2471,84 @@ export function createTelegramInboundRouteRuntime<
2323
2471
  },
2324
2472
  shouldIgnoreMessages: (messages) =>
2325
2473
  !Media.hasTelegramMessagesPromptContent(messages),
2474
+ consumeThreadNameInput: async (messages) => {
2475
+ const message = messages[0];
2476
+ if (!message || messages.length !== 1) return false;
2477
+ const target = Updates.getTelegramMessageTarget(message);
2478
+ if (!target) return false;
2479
+ const candidate = threadNameDialog.inspect(target);
2480
+ if (!candidate || candidate.scope !== getThreadNameDialogScope() ||
2481
+ candidate.phase !== "input") return false;
2482
+ const name = Media.extractFirstTelegramMessageText(messages).trim();
2483
+ if (/^[A-Z]$/.test(name) && deps.resetCurrentThreadName) {
2484
+ const consumed = threadNameDialog.consumeName({
2485
+ scope: getThreadNameDialogScope(), target, text: name,
2486
+ });
2487
+ if (consumed.kind !== "name") return false;
2488
+ const result = await deps.resetCurrentThreadName(target);
2489
+ if (!result.ok) {
2490
+ threadNameDialog.open({
2491
+ scope: getThreadNameDialogScope(), target,
2492
+ dialogMessageId: candidate.dialogMessageId,
2493
+ });
2494
+ }
2495
+ await deps.sendTextReply(
2496
+ target.chatId,
2497
+ message.message_id,
2498
+ result.ok && !result.message
2499
+ ? Commands.formatTelegramAutomaticThreadDisplayNameRestoredHeading(
2500
+ result.threadName ?? name,
2501
+ )
2502
+ : Commands.formatTelegramInformationHeading(
2503
+ result.ok ? "✅" : "⚠️",
2504
+ result.message ?? "Thread display name reset failed.",
2505
+ ),
2506
+ { parseMode: "HTML", target },
2507
+ );
2508
+ return true;
2509
+ }
2510
+ const validationError = deps.validateThreadName?.(name);
2511
+ if (!name || validationError) {
2512
+ await deps.sendTextReply(
2513
+ target.chatId,
2514
+ message.message_id,
2515
+ validationError
2516
+ ? Commands.formatTelegramInvalidInstanceName(validationError)
2517
+ : Commands.formatTelegramInformationHeading(
2518
+ "⚠️",
2519
+ "Send 1–96 printable ASCII characters.",
2520
+ ),
2521
+ { parseMode: "HTML", target },
2522
+ );
2523
+ return true;
2524
+ }
2525
+ const consumed = threadNameDialog.consumeName({
2526
+ scope: getThreadNameDialogScope(), target, text: name,
2527
+ });
2528
+ if (consumed.kind !== "name") return false;
2529
+ const result = await deps.renameCurrentThread?.(target, consumed.name);
2530
+ if (!result?.ok) {
2531
+ threadNameDialog.open({
2532
+ scope: getThreadNameDialogScope(),
2533
+ target,
2534
+ dialogMessageId: candidate.dialogMessageId,
2535
+ });
2536
+ }
2537
+ await deps.sendTextReply(
2538
+ target.chatId,
2539
+ message.message_id,
2540
+ result?.ok && !result.message
2541
+ ? Commands.formatTelegramThreadDisplayNameSavedHeading(
2542
+ result.threadName ?? consumed.name,
2543
+ )
2544
+ : Commands.formatTelegramInformationHeading(
2545
+ result?.ok ? "✅" : "⚠️",
2546
+ result?.message ?? "Thread display name update failed.",
2547
+ ),
2548
+ { parseMode: "HTML", target },
2549
+ );
2550
+ return true;
2551
+ },
2326
2552
  handleCommand: commandHandler,
2327
2553
  executeExtensionCommand: async (command, message, ctx) => {
2328
2554
  const extensionCommand = Commands.findTelegramExtensionCommand(
@@ -2585,8 +2811,7 @@ export function createTelegramInboundRouteRuntime<
2585
2811
  recordMessageOwnership: deps.recordMessageOwnership,
2586
2812
  handleTelegramTopicLifecycleUpdate,
2587
2813
  foreignOwnedUpdateForwarder: deps.foreignOwnedUpdateForwarder,
2588
- setAllowedUserId: deps.configStore.setAllowedUserId,
2589
- persistConfig: deps.configStore.persist,
2814
+ persistAllowedUserId: deps.configStore.persistAllowedUserId,
2590
2815
  updateStatus: deps.updateStatus,
2591
2816
  removePendingMediaGroupMessages: deps.mediaGroupRuntime.removeMessages,
2592
2817
  flushPendingMediaGroupMessage: deps.mediaGroupRuntime.flushMessage,
@@ -2684,7 +2909,8 @@ export function createTelegramInboundRouteRuntime<
2684
2909
  },
2685
2910
  handleAuthorizedTelegramEditedMessage: editRuntime.updateFromEditedMessage,
2686
2911
  handleAuthorizedTelegramGuestMessage,
2687
- handleUnboundTelegramTopicMessage: async (message, ctx) => {
2912
+ handleUnboundTelegramTopicMessage: (message, ctx) => {
2913
+ const operation = async (): Promise<void> => {
2688
2914
  const assertExecutionCurrent =
2689
2915
  Updates.createTelegramUpdateExecutionFenceGuard(message);
2690
2916
  assertExecutionCurrent();
@@ -2743,7 +2969,7 @@ export function createTelegramInboundRouteRuntime<
2743
2969
  target.chatId,
2744
2970
  message.message_id,
2745
2971
  "Instance " +
2746
- getTelegramThreadRecordLabel(existing) +
2972
+ getTelegramThreadRecordLabel(existing, deps.getDisplayTitle) +
2747
2973
  " is starting. Please wait…",
2748
2974
  { target },
2749
2975
  );
@@ -2754,74 +2980,10 @@ export function createTelegramInboundRouteRuntime<
2754
2980
  target.chatId,
2755
2981
  message.message_id,
2756
2982
  "Instance " +
2757
- getTelegramThreadRecordLabel(existing) +
2758
- " is not connected to the Telegram bus yet. Run /telegram-connect in that Pi instance; keeping this thread.",
2759
- { target },
2760
- );
2761
- return;
2762
- }
2763
- if (
2764
- (existing.status === "stale" || existing.status === "offline") &&
2765
- leaderProfileKey &&
2766
- !hasActiveLeaderTopic(
2767
- deps.threadStore.list(),
2768
- leaderProfileKey,
2769
- instanceId,
2770
- ) &&
2771
- !hasAnyRoutableThread
2772
- ) {
2773
- const priorLeaderRecord =
2774
- deps.threadStore.getByProfileKey(leaderProfileKey);
2775
- const slot =
2776
- deps.threadStore.allocateSlot(
2777
- leaderProfileKey,
2778
- priorLeaderRecord?.slot ?? existing.slot,
2779
- ) ??
2780
- priorLeaderRecord?.slot ??
2781
- existing.slot ??
2782
- "A";
2783
- const threadName =
2784
- priorLeaderRecord?.threadName ??
2785
- existing.threadName ??
2786
- Threads.chooseTelegramThreadName({ slot }) ??
2787
- "Pi";
2788
- deps.threadStore.upsert({
2789
- profileKey: leaderProfileKey,
2790
- owner: {
2791
- kind: "leader",
2792
- cwd:
2793
- typeof (ctx as { cwd?: unknown }).cwd === "string"
2794
- ? (ctx as { cwd?: string }).cwd
2795
- : undefined,
2796
- instanceId,
2797
- },
2798
- target: { chatId: target.chatId, threadId: target.threadId },
2799
- status: "active",
2800
- createdAtMs: priorLeaderRecord?.createdAtMs ?? existing.createdAtMs,
2801
- updatedAtMs: Date.now(),
2802
- threadName,
2803
- instanceId,
2804
- slot,
2805
- });
2806
- await deps.threadStore.persist();
2807
- assertExecutionCurrent();
2808
- deps.setCurrentLeaderIdentity?.({
2809
- target: { chatId: target.chatId, threadId: target.threadId },
2810
- slot,
2811
- threadName,
2812
- });
2813
- deps.recordRuntimeEvent?.(
2814
- "bus",
2815
- "Bus leader reclaimed unbound thread",
2816
- {
2817
- phase: "leader-topic-reclaim",
2818
- chatId: target.chatId,
2819
- threadId: target.threadId,
2820
- slot,
2821
- profileKey: leaderProfileKey,
2822
- },
2983
+ escapeHtml(getTelegramThreadRecordLabel(existing, deps.getDisplayTitle)) +
2984
+ ` is not currently registered with the Telegram bus. This thread is preserved; retry shortly. If it does not recover, run ${Commands.formatTelegramPiCommandHtml("/telegram-connect")} in that Pi instance.`,
2985
+ { parseMode: "HTML", target },
2823
2986
  );
2824
- await textDispatch.handleMessage(message as TMessage, ctx);
2825
2987
  return;
2826
2988
  }
2827
2989
  await deps.sendTextReply(
@@ -2914,12 +3076,28 @@ export function createTelegramInboundRouteRuntime<
2914
3076
  if (!currentLeaderIsStale) throw error;
2915
3077
  }
2916
3078
  if (currentLeaderIsStale) {
3079
+ const slot = deps.threadStore.allocateSlot(leaderProfileKey);
3080
+ if (!slot) {
3081
+ deps.threadStore.markStaleByTarget(
3082
+ currentLeaderRecord.target,
3083
+ "deleted",
3084
+ "Current leader thread is stale during unbound prompt routing.",
3085
+ );
3086
+ await deps.threadStore.persist();
3087
+ assertExecutionCurrent();
3088
+ await deps.sendTextReply(
3089
+ target.chatId,
3090
+ message.message_id,
3091
+ TELEGRAM_SLOT_CAPACITY_MESSAGE,
3092
+ { target },
3093
+ );
3094
+ return;
3095
+ }
2917
3096
  deps.threadStore.markStaleByTarget(
2918
3097
  currentLeaderRecord.target,
2919
3098
  "deleted",
2920
3099
  "Current leader thread is stale during unbound prompt routing.",
2921
3100
  );
2922
- const slot = currentLeaderRecord.slot ?? "A";
2923
3101
  const threadName = getRestoredThreadName(currentLeaderRecord, slot);
2924
3102
  deps.threadStore.upsert({
2925
3103
  ...currentLeaderRecord,
@@ -2972,14 +3150,19 @@ export function createTelegramInboundRouteRuntime<
2972
3150
  deps.threadStore.getByProfileKey(leaderProfileKey);
2973
3151
  const priorLeaderIdentity =
2974
3152
  deps.threadStore.getIdentityByProfileKey(leaderProfileKey);
2975
- const slot =
2976
- deps.threadStore.allocateSlot(
2977
- leaderProfileKey,
2978
- priorLeaderRecord?.slot ?? priorLeaderIdentity?.slot,
2979
- ) ??
2980
- priorLeaderRecord?.slot ??
2981
- priorLeaderIdentity?.slot ??
2982
- "A";
3153
+ const slot = deps.threadStore.allocateSlot(
3154
+ leaderProfileKey,
3155
+ priorLeaderRecord?.slot ?? priorLeaderIdentity?.slot,
3156
+ );
3157
+ if (!slot) {
3158
+ await deps.sendTextReply(
3159
+ target.chatId,
3160
+ message.message_id,
3161
+ TELEGRAM_SLOT_CAPACITY_MESSAGE,
3162
+ { target },
3163
+ );
3164
+ return;
3165
+ }
2983
3166
  const identityThreadName =
2984
3167
  priorLeaderIdentity?.threadName &&
2985
3168
  Threads.isTelegramTopicThreadNameValidForSlot(
@@ -3034,6 +3217,16 @@ export function createTelegramInboundRouteRuntime<
3034
3217
  }
3035
3218
  await sendUnboundRerouteChooser(message as TMessage, ctx);
3036
3219
  return;
3220
+ };
3221
+ if (!deps.runWorkspaceOperation) return operation();
3222
+ return deps.runWorkspaceOperation(
3223
+ {
3224
+ operationId: `workspace-unbound:${message.chat.id}:${message.message_id}`,
3225
+ operationKind: "workspace.route-unbound-thread",
3226
+ scopes: [{ kind: "profile" }],
3227
+ },
3228
+ operation,
3229
+ );
3037
3230
  },
3038
3231
  });
3039
3232
  }