@llblab/pi-kit 0.6.0 → 0.7.1

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 (129) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +4 -4
  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-state-flow/AGENTS.md +33 -12
  35. package/node_modules/@llblab/pi-state-flow/BACKLOG.md +125 -2
  36. package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +87 -45
  37. package/node_modules/@llblab/pi-state-flow/README.md +203 -107
  38. package/node_modules/@llblab/pi-state-flow/docs/README.md +4 -0
  39. package/node_modules/@llblab/pi-state-flow/docs/architecture.md +188 -0
  40. package/node_modules/@llblab/pi-state-flow/docs/temporal-acceptance.md +42 -0
  41. package/node_modules/@llblab/pi-state-flow/index.ts +164 -2
  42. package/node_modules/@llblab/pi-state-flow/lib/acquisition.ts +138 -0
  43. package/node_modules/@llblab/pi-state-flow/lib/artifact.ts +273 -0
  44. package/node_modules/@llblab/pi-state-flow/lib/config.ts +48 -0
  45. package/node_modules/@llblab/pi-state-flow/lib/context.ts +18 -5
  46. package/node_modules/@llblab/pi-state-flow/lib/continuation.ts +268 -0
  47. package/node_modules/@llblab/pi-state-flow/lib/discovery.ts +117 -0
  48. package/node_modules/@llblab/pi-state-flow/lib/durable.ts +562 -0
  49. package/node_modules/@llblab/pi-state-flow/lib/episode.ts +24 -12
  50. package/node_modules/@llblab/pi-state-flow/lib/extension.ts +606 -70
  51. package/node_modules/@llblab/pi-state-flow/lib/git.ts +666 -0
  52. package/node_modules/@llblab/pi-state-flow/lib/history.ts +95 -0
  53. package/node_modules/@llblab/pi-state-flow/lib/json.ts +24 -0
  54. package/node_modules/@llblab/pi-state-flow/lib/maintenance.ts +141 -0
  55. package/node_modules/@llblab/pi-state-flow/lib/memory.ts +52 -0
  56. package/node_modules/@llblab/pi-state-flow/lib/migration.ts +88 -0
  57. package/node_modules/@llblab/pi-state-flow/lib/publication.ts +296 -0
  58. package/node_modules/@llblab/pi-state-flow/lib/recovery.ts +23 -7
  59. package/node_modules/@llblab/pi-state-flow/lib/rehydration.ts +79 -0
  60. package/node_modules/@llblab/pi-state-flow/lib/runtime.ts +264 -0
  61. package/node_modules/@llblab/pi-state-flow/lib/session.ts +6 -0
  62. package/node_modules/@llblab/pi-state-flow/lib/skills.ts +99 -7
  63. package/node_modules/@llblab/pi-state-flow/lib/snapshot.ts +305 -48
  64. package/node_modules/@llblab/pi-state-flow/lib/state.ts +74 -7
  65. package/node_modules/@llblab/pi-state-flow/lib/status.ts +125 -6
  66. package/node_modules/@llblab/pi-state-flow/lib/storage.ts +196 -0
  67. package/node_modules/@llblab/pi-state-flow/lib/temporal.ts +233 -0
  68. package/node_modules/@llblab/pi-state-flow/lib/terminal.ts +70 -24
  69. package/node_modules/@llblab/pi-state-flow/lib/transition.ts +254 -29
  70. package/node_modules/@llblab/pi-state-flow/package.json +8 -2
  71. package/node_modules/@llblab/pi-state-flow/skills/state-flow-memory/SKILL.md +128 -0
  72. package/node_modules/@llblab/pi-telegram/AGENTS.md +14 -9
  73. package/node_modules/@llblab/pi-telegram/BACKLOG.md +23 -5
  74. package/node_modules/@llblab/pi-telegram/CHANGELOG.md +18 -0
  75. package/node_modules/@llblab/pi-telegram/README.md +13 -9
  76. package/node_modules/@llblab/pi-telegram/docs/README.md +1 -0
  77. package/node_modules/@llblab/pi-telegram/docs/architecture.md +227 -23
  78. package/node_modules/@llblab/pi-telegram/docs/generative-apps.md +1 -1
  79. package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +70 -19
  80. package/node_modules/@llblab/pi-telegram/docs/outbound.md +6 -6
  81. package/node_modules/@llblab/pi-telegram/docs/public-api.md +13 -6
  82. package/node_modules/@llblab/pi-telegram/docs/ui-style.md +3 -1
  83. package/node_modules/@llblab/pi-telegram/index.ts +4 -1418
  84. package/node_modules/@llblab/pi-telegram/lib/agent-messages.ts +6 -3
  85. package/node_modules/@llblab/pi-telegram/lib/bindings.ts +46 -1
  86. package/node_modules/@llblab/pi-telegram/lib/bus-api.ts +32 -19
  87. package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +600 -135
  88. package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +962 -55
  89. package/node_modules/@llblab/pi-telegram/lib/bus.ts +355 -26
  90. package/node_modules/@llblab/pi-telegram/lib/channel-posts.ts +718 -0
  91. package/node_modules/@llblab/pi-telegram/lib/commands.ts +237 -11
  92. package/node_modules/@llblab/pi-telegram/lib/config.ts +242 -26
  93. package/node_modules/@llblab/pi-telegram/lib/extension.ts +1851 -0
  94. package/node_modules/@llblab/pi-telegram/lib/generative-apps.ts +20 -2
  95. package/node_modules/@llblab/pi-telegram/lib/journal.ts +2184 -126
  96. package/node_modules/@llblab/pi-telegram/lib/locks.ts +44 -2
  97. package/node_modules/@llblab/pi-telegram/lib/menu-settings.ts +152 -13
  98. package/node_modules/@llblab/pi-telegram/lib/outbound-attachments.ts +97 -10
  99. package/node_modules/@llblab/pi-telegram/lib/paths.ts +29 -0
  100. package/node_modules/@llblab/pi-telegram/lib/polling.ts +85 -17
  101. package/node_modules/@llblab/pi-telegram/lib/preview.ts +17 -0
  102. package/node_modules/@llblab/pi-telegram/lib/prompts.ts +6 -2
  103. package/node_modules/@llblab/pi-telegram/lib/queue.ts +118 -26
  104. package/node_modules/@llblab/pi-telegram/lib/rendering.ts +4 -1
  105. package/node_modules/@llblab/pi-telegram/lib/replies.ts +21 -2
  106. package/node_modules/@llblab/pi-telegram/lib/routing.ts +344 -112
  107. package/node_modules/@llblab/pi-telegram/lib/setup.ts +44 -4
  108. package/node_modules/@llblab/pi-telegram/lib/status.ts +51 -4
  109. package/node_modules/@llblab/pi-telegram/lib/sync.ts +308 -39
  110. package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +353 -22
  111. package/node_modules/@llblab/pi-telegram/lib/thread-cleanup-manager.ts +664 -0
  112. package/node_modules/@llblab/pi-telegram/lib/thread-display.ts +226 -0
  113. package/node_modules/@llblab/pi-telegram/lib/thread-naming.ts +118 -0
  114. package/node_modules/@llblab/pi-telegram/lib/threads.ts +1686 -129
  115. package/node_modules/@llblab/pi-telegram/lib/turns.ts +7 -0
  116. package/node_modules/@llblab/pi-telegram/lib/updates.ts +1319 -97
  117. package/node_modules/@llblab/pi-telegram/lib/workspace-admission.ts +1643 -0
  118. package/node_modules/@llblab/pi-telegram/lib/workspace-retirement.ts +968 -0
  119. package/node_modules/@llblab/pi-telegram/lib/workspace-slots.ts +84 -0
  120. package/node_modules/@llblab/pi-telegram/package.json +1 -1
  121. package/node_modules/@llblab/pi-telegram/screenshot.png +0 -0
  122. package/node_modules/@llblab/pi-telegram/scripts/measure-bus.mjs +83 -0
  123. package/node_modules/@llblab/pi-telegram/scripts/measure-workspace.mjs +101 -0
  124. package/node_modules/@llblab/{skills → pi-telegram/skills}/show-me/SKILL.md +28 -6
  125. package/node_modules/@llblab/pi-telegram/skills/show-me/references/telegram-surfaces.md +43 -0
  126. package/node_modules/@llblab/pi-telegram/skills/telegram-bridge/references/delivery-and-threads.md +1 -1
  127. package/node_modules/@llblab/skills/package.json +2 -3
  128. package/package.json +6 -5
  129. /package/node_modules/@llblab/pi-telegram/lib/{logs.ts → logging.ts} +0 -0
@@ -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;
@@ -654,6 +659,12 @@ export interface TelegramInboundRouteRuntimeDeps<
654
659
  ) => Promise<number | undefined>;
655
660
  deleteMessage?: (chatId: number, messageId: number) => Promise<void>;
656
661
  answerGuestQuery: (guestQueryId: string, text?: string) => Promise<void>;
662
+ /** Answers the guest query immediately and returns its inline message id. */
663
+ answerGuestQueryForInlineMessage?: (
664
+ guestQueryId: string,
665
+ text: string,
666
+ options?: { parseMode?: "HTML" },
667
+ ) => Promise<string | undefined>;
657
668
  sendTextReply: (
658
669
  chatId: number,
659
670
  replyToMessageId: number,
@@ -661,6 +672,9 @@ export interface TelegramInboundRouteRuntimeDeps<
661
672
  options?: { parseMode?: "HTML"; target?: Queue.TelegramQueueTarget },
662
673
  ) => Promise<number | undefined>;
663
674
  setMyCommands: Commands.TelegramBotCommandRegistrationDeps["setMyCommands"];
675
+ validateThreadName?: (threadName: string) => string | undefined;
676
+ renameCurrentThread?: Commands.TelegramThreadDisplayNameRenamePort;
677
+ resetCurrentThreadName?: Commands.TelegramThreadDisplayNameResetPort;
664
678
  getCommands: () => Parameters<
665
679
  typeof PromptTemplates.getTelegramPromptTemplateCommands
666
680
  >[0];
@@ -785,7 +799,8 @@ export function createTelegramInboundRouteRuntime<
785
799
  const chatId = message.chat.id;
786
800
  const threadId = message.message_thread_id;
787
801
  if (!threadId) return undefined;
788
- const localLabel = deps.getLocalThreadLabelForTarget?.({ chatId, threadId });
802
+ const localLabel = deps.getDisplayTitle?.({ chatId, threadId }) ??
803
+ deps.getLocalThreadLabelForTarget?.({ chatId, threadId });
789
804
  if (localLabel) return localLabel;
790
805
  if (!deps.threadStore) return undefined;
791
806
  const records = deps.threadStore.list();
@@ -964,6 +979,8 @@ export function createTelegramInboundRouteRuntime<
964
979
  timer: ReturnType<typeof setTimeout>;
965
980
  }
966
981
  >();
982
+ const threadNameDialog = ThreadNaming.createTelegramThreadNameDialogRuntime();
983
+ const getThreadNameDialogScope = () => deps.getCurrentInstanceId?.() ?? "local";
967
984
  const menuCallbackHandler = Menu.createTelegramMenuCallbackHandlerForContext<
968
985
  TCallbackQuery,
969
986
  TContext,
@@ -1328,6 +1345,7 @@ export function createTelegramInboundRouteRuntime<
1328
1345
  const replyMarkup = buildTelegramUnboundRerouteRestoreChooserMarkup(
1329
1346
  parsed.rerouteId,
1330
1347
  activeRecords,
1348
+ deps.getDisplayTitle,
1331
1349
  );
1332
1350
  if (deps.editInteractiveMessage) {
1333
1351
  await deps.editInteractiveMessage(
@@ -1352,7 +1370,7 @@ export function createTelegramInboundRouteRuntime<
1352
1370
  await deps.answerCallbackQuery(query.id, "Choose instance to restore.");
1353
1371
  return true;
1354
1372
  };
1355
- const executeUnboundRerouteCallback = async (
1373
+ const executeUnboundRerouteCallbackOperation = async (
1356
1374
  query: TCallbackQuery,
1357
1375
  ctx: TContext,
1358
1376
  ): Promise<boolean> => {
@@ -1471,6 +1489,13 @@ export function createTelegramInboundRouteRuntime<
1471
1489
  currentInstanceId,
1472
1490
  );
1473
1491
  if (parsed.useNewSlot && !isCurrentLeaderRecord) {
1492
+ if (!record.slot || !/^[A-Z]$/u.test(record.slot)) {
1493
+ await deps.answerCallbackQuery(
1494
+ query.id,
1495
+ "Follower thread slot authority is unavailable.",
1496
+ );
1497
+ return true;
1498
+ }
1474
1499
  if (
1475
1500
  !sourceTarget ||
1476
1501
  !deps.replaceFollowerThreadTarget ||
@@ -1497,7 +1522,7 @@ export function createTelegramInboundRouteRuntime<
1497
1522
  return true;
1498
1523
  }
1499
1524
  const nowMs = Date.now();
1500
- const slot = record.slot ?? "?";
1525
+ const slot = record.slot;
1501
1526
  const threadName = getRestoredThreadName(record, slot);
1502
1527
  assertExecutionCurrent();
1503
1528
  deps.threadStore.markStaleByTarget(
@@ -1640,18 +1665,21 @@ export function createTelegramInboundRouteRuntime<
1640
1665
  (record.target.chatId !== sourceTarget.chatId ||
1641
1666
  record.target.threadId !== sourceTarget.threadId)
1642
1667
  ) {
1668
+ const slot = deps.threadStore.allocateSlot(
1669
+ leaderProfileKey ?? record.profileKey,
1670
+ getNextTelegramSlotPreference(record.slot),
1671
+ undefined,
1672
+ { excludeCurrentRecord: true },
1673
+ );
1674
+ if (!slot) {
1675
+ await deps.answerCallbackQuery(query.id, TELEGRAM_SLOT_CAPACITY_MESSAGE);
1676
+ return true;
1677
+ }
1643
1678
  deps.threadStore.markStaleByTarget(
1644
1679
  record.target,
1645
1680
  "deleted",
1646
1681
  "Current leader thread was replaced by a new-slot reroute source.",
1647
1682
  );
1648
- const slot =
1649
- deps.threadStore.allocateSlot(
1650
- leaderProfileKey ?? record.profileKey,
1651
- getNextTelegramSlotPreference(record.slot),
1652
- ) ??
1653
- record.slot ??
1654
- "?";
1655
1683
  const nowMs = Date.now();
1656
1684
  const threadName = getRestoredThreadName(record, slot);
1657
1685
  deps.threadStore.upsert({
@@ -1759,6 +1787,23 @@ export function createTelegramInboundRouteRuntime<
1759
1787
  );
1760
1788
  return true;
1761
1789
  };
1790
+ const executeUnboundRerouteCallback = (
1791
+ query: TCallbackQuery,
1792
+ ctx: TContext,
1793
+ ): Promise<boolean> => {
1794
+ if (!parseTelegramUnboundRerouteCallbackData(query.data) ||
1795
+ !deps.runWorkspaceOperation) {
1796
+ return executeUnboundRerouteCallbackOperation(query, ctx);
1797
+ }
1798
+ return deps.runWorkspaceOperation(
1799
+ {
1800
+ operationId: `workspace-reroute:${query.id}`,
1801
+ operationKind: "workspace.route-unbound-thread",
1802
+ scopes: [{ kind: "profile" }],
1803
+ },
1804
+ () => executeUnboundRerouteCallbackOperation(query, ctx),
1805
+ );
1806
+ };
1762
1807
  const handleUnboundRerouteCallback = async (
1763
1808
  query: TCallbackQuery,
1764
1809
  ctx: TContext,
@@ -1881,6 +1926,70 @@ export function createTelegramInboundRouteRuntime<
1881
1926
  assertExecutionCurrent();
1882
1927
  if (handled) return;
1883
1928
  }
1929
+ if (query.data?.startsWith("thread-name:")) {
1930
+ const chatId = query.message?.chat?.id;
1931
+ const dialogMessageId = query.message?.message_id;
1932
+ if (typeof chatId !== "number" || typeof dialogMessageId !== "number") {
1933
+ await deps.answerCallbackQuery(query.id, "⌛ Rename dialog expired.");
1934
+ return;
1935
+ }
1936
+ const target = typeof query.message?.message_thread_id === "number"
1937
+ ? { chatId, threadId: query.message.message_thread_id }
1938
+ : { chatId };
1939
+ const action = query.data.slice("thread-name:".length);
1940
+ if (action !== "reset" && action !== "cancel") {
1941
+ await deps.answerCallbackQuery(query.id, "⌛ Rename dialog expired.");
1942
+ return;
1943
+ }
1944
+ const selected = threadNameDialog.select({
1945
+ scope: getThreadNameDialogScope(),
1946
+ target,
1947
+ dialogMessageId,
1948
+ action,
1949
+ });
1950
+ if (selected.kind === "expired") {
1951
+ await deps.answerCallbackQuery(query.id, "⌛ Rename dialog expired.");
1952
+ return;
1953
+ }
1954
+ if (selected.kind === "cancel") {
1955
+ await deps.editInteractiveMessage?.(
1956
+ chatId,
1957
+ dialogMessageId,
1958
+ "<b>✖ Rename cancelled. Returning to normal agent mode.</b>",
1959
+ "html",
1960
+ { inline_keyboard: [] },
1961
+ );
1962
+ await deps.answerCallbackQuery(query.id);
1963
+ return;
1964
+ }
1965
+ try {
1966
+ const result = await deps.resetCurrentThreadName?.(target);
1967
+ if (!result?.ok) {
1968
+ throw new Error(result?.message ?? "Thread display name reset is unavailable.");
1969
+ }
1970
+ await deps.editInteractiveMessage?.(
1971
+ chatId,
1972
+ dialogMessageId,
1973
+ result.message
1974
+ ? Commands.formatTelegramInformationHeading("✅", result.message)
1975
+ : Commands.formatTelegramAutomaticThreadDisplayNameRestoredHeading(
1976
+ result.threadName ?? "automatic",
1977
+ ),
1978
+ "html",
1979
+ { inline_keyboard: [] },
1980
+ );
1981
+ await deps.answerCallbackQuery(query.id);
1982
+ } catch (error) {
1983
+ threadNameDialog.open({
1984
+ scope: getThreadNameDialogScope(), target, dialogMessageId,
1985
+ });
1986
+ deps.recordRuntimeEvent?.("telegram-command", error, {
1987
+ command: "name", phase: "reset",
1988
+ });
1989
+ await deps.answerCallbackQuery(query.id, "⚠️ Thread name reset failed.");
1990
+ }
1991
+ return;
1992
+ }
1884
1993
  const handledByCompact =
1885
1994
  await Commands.handleTelegramCompactConfirmationCallback(query, {
1886
1995
  ctx,
@@ -2083,10 +2192,54 @@ export function createTelegramInboundRouteRuntime<
2083
2192
  },
2084
2193
  openSettingsMenu: deps.openSettingsMenu,
2085
2194
  getAllowedUserId: deps.configStore.getAllowedUserId,
2086
- setAllowedUserId: deps.configStore.setAllowedUserId,
2195
+ persistAllowedUserId: deps.configStore.persistAllowedUserId,
2087
2196
  setMyCommands: deps.setMyCommands,
2197
+ validateThreadName: deps.validateThreadName,
2198
+ renameCurrentThread: deps.renameCurrentThread,
2199
+ resetCurrentThreadName: deps.resetCurrentThreadName,
2200
+ openThreadNameDialog: async (message) => {
2201
+ const target = Updates.getTelegramMessageTarget(message);
2202
+ if (!deps.sendInteractiveMessage || !target) {
2203
+ await deps.sendTextReply(
2204
+ message.chat.id,
2205
+ message.message_id,
2206
+ Commands.formatTelegramInformationHeading(
2207
+ "🏷️",
2208
+ "Usage: /name Navigator",
2209
+ ),
2210
+ { parseMode: "HTML", target },
2211
+ );
2212
+ return;
2213
+ }
2214
+ const hasManualName = deps.threadStore?.listWorkspaceBindings().some(
2215
+ (binding) => binding.target.chatId === target.chatId &&
2216
+ binding.target.threadId === target.threadId &&
2217
+ typeof binding.manualThreadName === "string",
2218
+ ) ?? false;
2219
+ const instructions = hasManualName
2220
+ ? "<b>🏷️ Send a new Thread name using printable ASCII, reset to automatic, or cancel.</b>"
2221
+ : "<b>🏷️ Send a Thread name using printable ASCII, or cancel.</b>";
2222
+ const buttons = hasManualName
2223
+ ? [
2224
+ { text: "↩️ Reset to automatic", callback_data: "thread-name:reset" },
2225
+ { text: "✖ Cancel rename", callback_data: "thread-name:cancel" },
2226
+ ]
2227
+ : [{ text: "✖ Cancel rename", callback_data: "thread-name:cancel" }];
2228
+ const dialogMessageId = await deps.sendInteractiveMessage(
2229
+ target.chatId,
2230
+ instructions,
2231
+ "html",
2232
+ { inline_keyboard: buttons.map((button) => [button]) },
2233
+ { target },
2234
+ );
2235
+ if (typeof dialogMessageId !== "number") return;
2236
+ threadNameDialog.open({
2237
+ scope: getThreadNameDialogScope(),
2238
+ target,
2239
+ dialogMessageId,
2240
+ });
2241
+ },
2088
2242
  getPromptTemplateCommands,
2089
- persistConfig: deps.configStore.persist,
2090
2243
  sendTextReply: deps.sendTextReply,
2091
2244
  getActiveTurnReply: () => {
2092
2245
  const activeTurn = deps.activeTurnRuntime.get();
@@ -2158,7 +2311,7 @@ export function createTelegramInboundRouteRuntime<
2158
2311
  message.message_id,
2159
2312
  [
2160
2313
  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.",
2314
+ `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
2315
  ]
2163
2316
  .filter((line): line is string => typeof line === "string")
2164
2317
  .join("\n\n"),
@@ -2178,7 +2331,7 @@ export function createTelegramInboundRouteRuntime<
2178
2331
  const replyMarkup = buildTelegramUnboundRerouteChooserMarkup(
2179
2332
  rerouteId,
2180
2333
  activeRecords,
2181
- { canRestore: sourceTarget !== undefined },
2334
+ { canRestore: sourceTarget !== undefined, getDisplayTitle: deps.getDisplayTitle },
2182
2335
  );
2183
2336
  if (deps.sendInteractiveMessage) {
2184
2337
  const chooserId = await deps.sendInteractiveMessage(
@@ -2262,7 +2415,7 @@ export function createTelegramInboundRouteRuntime<
2262
2415
  const replyMarkup = buildTelegramUnboundRerouteChooserMarkup(
2263
2416
  rerouteId,
2264
2417
  activeRecords,
2265
- { canRestore: typeof message.message_thread_id === "number" },
2418
+ { canRestore: typeof message.message_thread_id === "number", getDisplayTitle: deps.getDisplayTitle },
2266
2419
  );
2267
2420
  let chooserId: number | undefined;
2268
2421
  try {
@@ -2311,6 +2464,7 @@ export function createTelegramInboundRouteRuntime<
2311
2464
  throw error;
2312
2465
  }
2313
2466
  rememberRerouteChooser(rerouteId, chooserId);
2467
+ Updates.reportTelegramUpdateCompleted(commandMessage);
2314
2468
  return true;
2315
2469
  };
2316
2470
  const commandOrPrompt = Commands.createTelegramCommandOrPromptRuntime<
@@ -2323,6 +2477,84 @@ export function createTelegramInboundRouteRuntime<
2323
2477
  },
2324
2478
  shouldIgnoreMessages: (messages) =>
2325
2479
  !Media.hasTelegramMessagesPromptContent(messages),
2480
+ consumeThreadNameInput: async (messages) => {
2481
+ const message = messages[0];
2482
+ if (!message || messages.length !== 1) return false;
2483
+ const target = Updates.getTelegramMessageTarget(message);
2484
+ if (!target) return false;
2485
+ const candidate = threadNameDialog.inspect(target);
2486
+ if (!candidate || candidate.scope !== getThreadNameDialogScope() ||
2487
+ candidate.phase !== "input") return false;
2488
+ const name = Media.extractFirstTelegramMessageText(messages).trim();
2489
+ if (/^[A-Z]$/.test(name) && deps.resetCurrentThreadName) {
2490
+ const consumed = threadNameDialog.consumeName({
2491
+ scope: getThreadNameDialogScope(), target, text: name,
2492
+ });
2493
+ if (consumed.kind !== "name") return false;
2494
+ const result = await deps.resetCurrentThreadName(target);
2495
+ if (!result.ok) {
2496
+ threadNameDialog.open({
2497
+ scope: getThreadNameDialogScope(), target,
2498
+ dialogMessageId: candidate.dialogMessageId,
2499
+ });
2500
+ }
2501
+ await deps.sendTextReply(
2502
+ target.chatId,
2503
+ message.message_id,
2504
+ result.ok && !result.message
2505
+ ? Commands.formatTelegramAutomaticThreadDisplayNameRestoredHeading(
2506
+ result.threadName ?? name,
2507
+ )
2508
+ : Commands.formatTelegramInformationHeading(
2509
+ result.ok ? "✅" : "⚠️",
2510
+ result.message ?? "Thread display name reset failed.",
2511
+ ),
2512
+ { parseMode: "HTML", target },
2513
+ );
2514
+ return true;
2515
+ }
2516
+ const validationError = deps.validateThreadName?.(name);
2517
+ if (!name || validationError) {
2518
+ await deps.sendTextReply(
2519
+ target.chatId,
2520
+ message.message_id,
2521
+ validationError
2522
+ ? Commands.formatTelegramInvalidInstanceName(validationError)
2523
+ : Commands.formatTelegramInformationHeading(
2524
+ "⚠️",
2525
+ "Send 1–96 printable ASCII characters.",
2526
+ ),
2527
+ { parseMode: "HTML", target },
2528
+ );
2529
+ return true;
2530
+ }
2531
+ const consumed = threadNameDialog.consumeName({
2532
+ scope: getThreadNameDialogScope(), target, text: name,
2533
+ });
2534
+ if (consumed.kind !== "name") return false;
2535
+ const result = await deps.renameCurrentThread?.(target, consumed.name);
2536
+ if (!result?.ok) {
2537
+ threadNameDialog.open({
2538
+ scope: getThreadNameDialogScope(),
2539
+ target,
2540
+ dialogMessageId: candidate.dialogMessageId,
2541
+ });
2542
+ }
2543
+ await deps.sendTextReply(
2544
+ target.chatId,
2545
+ message.message_id,
2546
+ result?.ok && !result.message
2547
+ ? Commands.formatTelegramThreadDisplayNameSavedHeading(
2548
+ result.threadName ?? consumed.name,
2549
+ )
2550
+ : Commands.formatTelegramInformationHeading(
2551
+ result?.ok ? "✅" : "⚠️",
2552
+ result?.message ?? "Thread display name update failed.",
2553
+ ),
2554
+ { parseMode: "HTML", target },
2555
+ );
2556
+ return true;
2557
+ },
2326
2558
  handleCommand: commandHandler,
2327
2559
  executeExtensionCommand: async (command, message, ctx) => {
2328
2560
  const extensionCommand = Commands.findTelegramExtensionCommand(
@@ -2429,6 +2661,9 @@ export function createTelegramInboundRouteRuntime<
2429
2661
  await deps.threadStore.load();
2430
2662
  assertExecutionCurrent();
2431
2663
  };
2664
+ // Answer the guest query immediately so the agent-end edit can replace the
2665
+ // early ACK once the turn settles. See BACKLOG.md for live acceptance.
2666
+ const TELEGRAM_GUEST_ACK_HTML = "<b>⚙️ Received. Working on it…</b>";
2432
2667
  const handleAuthorizedTelegramGuestMessage = async (
2433
2668
  guestMessage: Updates.TelegramGuestMessage & { from: TelegramUser },
2434
2669
  ctx: TContext,
@@ -2436,6 +2671,31 @@ export function createTelegramInboundRouteRuntime<
2436
2671
  const assertExecutionCurrent =
2437
2672
  Updates.createTelegramUpdateExecutionFenceGuard(guestMessage);
2438
2673
  assertExecutionCurrent();
2674
+ let guestInlineMessageId: string | undefined;
2675
+ if (deps.answerGuestQueryForInlineMessage) {
2676
+ try {
2677
+ guestInlineMessageId = await deps.answerGuestQueryForInlineMessage(
2678
+ guestMessage.guest_query_id,
2679
+ TELEGRAM_GUEST_ACK_HTML,
2680
+ { parseMode: "HTML" },
2681
+ );
2682
+ deps.recordRuntimeEvent?.(
2683
+ "guest",
2684
+ new Error("Guest ACK experiment answered the guest query"),
2685
+ {
2686
+ phase: "guest-ack-sent",
2687
+ guestQueryId: guestMessage.guest_query_id,
2688
+ hasInlineMessageId: !!guestInlineMessageId,
2689
+ },
2690
+ );
2691
+ } catch (error) {
2692
+ deps.recordRuntimeEvent?.("guest", error, {
2693
+ phase: "guest-ack-failed",
2694
+ guestQueryId: guestMessage.guest_query_id,
2695
+ });
2696
+ }
2697
+ assertExecutionCurrent();
2698
+ }
2439
2699
  const text = guestMessage.text ?? "";
2440
2700
  const gm = guestMessage as unknown as Record<string, unknown>;
2441
2701
  // Build telegram prefix with guest context
@@ -2526,6 +2786,10 @@ export function createTelegramInboundRouteRuntime<
2526
2786
  promptFiles: processed.promptFiles,
2527
2787
  handlerOutputs: processed.handlerOutputs,
2528
2788
  sourceContext,
2789
+ // Guest Mode allows exactly one reply within Telegram's limited response
2790
+ // window; the note travels with the turn text so the agent sees it at
2791
+ // execution time without a guest-specific system prompt variant.
2792
+ guestTurn: true,
2529
2793
  });
2530
2794
  const order = deps.bridgeRuntime.queue.allocateItemOrder();
2531
2795
  const content: Queue.TelegramPromptContent[] = [
@@ -2552,6 +2816,7 @@ export function createTelegramInboundRouteRuntime<
2552
2816
  chatId: 0,
2553
2817
  replyToMessageId: 0,
2554
2818
  guestQueryId: guestMessage.guest_query_id,
2819
+ ...(guestInlineMessageId ? { guestInlineMessageId } : {}),
2555
2820
  sourceMessageIds: [],
2556
2821
  queueOrder: order,
2557
2822
  queueLane: "default",
@@ -2585,8 +2850,7 @@ export function createTelegramInboundRouteRuntime<
2585
2850
  recordMessageOwnership: deps.recordMessageOwnership,
2586
2851
  handleTelegramTopicLifecycleUpdate,
2587
2852
  foreignOwnedUpdateForwarder: deps.foreignOwnedUpdateForwarder,
2588
- setAllowedUserId: deps.configStore.setAllowedUserId,
2589
- persistConfig: deps.configStore.persist,
2853
+ persistAllowedUserId: deps.configStore.persistAllowedUserId,
2590
2854
  updateStatus: deps.updateStatus,
2591
2855
  removePendingMediaGroupMessages: deps.mediaGroupRuntime.removeMessages,
2592
2856
  flushPendingMediaGroupMessage: deps.mediaGroupRuntime.flushMessage,
@@ -2684,7 +2948,8 @@ export function createTelegramInboundRouteRuntime<
2684
2948
  },
2685
2949
  handleAuthorizedTelegramEditedMessage: editRuntime.updateFromEditedMessage,
2686
2950
  handleAuthorizedTelegramGuestMessage,
2687
- handleUnboundTelegramTopicMessage: async (message, ctx) => {
2951
+ handleUnboundTelegramTopicMessage: (message, ctx) => {
2952
+ const operation = async (): Promise<void> => {
2688
2953
  const assertExecutionCurrent =
2689
2954
  Updates.createTelegramUpdateExecutionFenceGuard(message);
2690
2955
  assertExecutionCurrent();
@@ -2743,7 +3008,7 @@ export function createTelegramInboundRouteRuntime<
2743
3008
  target.chatId,
2744
3009
  message.message_id,
2745
3010
  "Instance " +
2746
- getTelegramThreadRecordLabel(existing) +
3011
+ getTelegramThreadRecordLabel(existing, deps.getDisplayTitle) +
2747
3012
  " is starting. Please wait…",
2748
3013
  { target },
2749
3014
  );
@@ -2754,76 +3019,12 @@ export function createTelegramInboundRouteRuntime<
2754
3019
  target.chatId,
2755
3020
  message.message_id,
2756
3021
  "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 },
3022
+ escapeHtml(getTelegramThreadRecordLabel(existing, deps.getDisplayTitle)) +
3023
+ ` 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.`,
3024
+ { parseMode: "HTML", target },
2760
3025
  );
2761
3026
  return;
2762
3027
  }
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
- },
2823
- );
2824
- await textDispatch.handleMessage(message as TMessage, ctx);
2825
- return;
2826
- }
2827
3028
  await deps.sendTextReply(
2828
3029
  target.chatId,
2829
3030
  message.message_id,
@@ -2914,12 +3115,28 @@ export function createTelegramInboundRouteRuntime<
2914
3115
  if (!currentLeaderIsStale) throw error;
2915
3116
  }
2916
3117
  if (currentLeaderIsStale) {
3118
+ const slot = deps.threadStore.allocateSlot(leaderProfileKey);
3119
+ if (!slot) {
3120
+ deps.threadStore.markStaleByTarget(
3121
+ currentLeaderRecord.target,
3122
+ "deleted",
3123
+ "Current leader thread is stale during unbound prompt routing.",
3124
+ );
3125
+ await deps.threadStore.persist();
3126
+ assertExecutionCurrent();
3127
+ await deps.sendTextReply(
3128
+ target.chatId,
3129
+ message.message_id,
3130
+ TELEGRAM_SLOT_CAPACITY_MESSAGE,
3131
+ { target },
3132
+ );
3133
+ return;
3134
+ }
2917
3135
  deps.threadStore.markStaleByTarget(
2918
3136
  currentLeaderRecord.target,
2919
3137
  "deleted",
2920
3138
  "Current leader thread is stale during unbound prompt routing.",
2921
3139
  );
2922
- const slot = currentLeaderRecord.slot ?? "A";
2923
3140
  const threadName = getRestoredThreadName(currentLeaderRecord, slot);
2924
3141
  deps.threadStore.upsert({
2925
3142
  ...currentLeaderRecord,
@@ -2972,14 +3189,19 @@ export function createTelegramInboundRouteRuntime<
2972
3189
  deps.threadStore.getByProfileKey(leaderProfileKey);
2973
3190
  const priorLeaderIdentity =
2974
3191
  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";
3192
+ const slot = deps.threadStore.allocateSlot(
3193
+ leaderProfileKey,
3194
+ priorLeaderRecord?.slot ?? priorLeaderIdentity?.slot,
3195
+ );
3196
+ if (!slot) {
3197
+ await deps.sendTextReply(
3198
+ target.chatId,
3199
+ message.message_id,
3200
+ TELEGRAM_SLOT_CAPACITY_MESSAGE,
3201
+ { target },
3202
+ );
3203
+ return;
3204
+ }
2983
3205
  const identityThreadName =
2984
3206
  priorLeaderIdentity?.threadName &&
2985
3207
  Threads.isTelegramTopicThreadNameValidForSlot(
@@ -3034,6 +3256,16 @@ export function createTelegramInboundRouteRuntime<
3034
3256
  }
3035
3257
  await sendUnboundRerouteChooser(message as TMessage, ctx);
3036
3258
  return;
3259
+ };
3260
+ if (!deps.runWorkspaceOperation) return operation();
3261
+ return deps.runWorkspaceOperation(
3262
+ {
3263
+ operationId: `workspace-unbound:${message.chat.id}:${message.message_id}`,
3264
+ operationKind: "workspace.route-unbound-thread",
3265
+ scopes: [{ kind: "profile" }],
3266
+ },
3267
+ operation,
3268
+ );
3037
3269
  },
3038
3270
  });
3039
3271
  }