@llblab/pi-kit 0.15.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +2 -2
  3. package/node_modules/@llblab/pi-state-flow/AGENTS.md +9 -9
  4. package/node_modules/@llblab/pi-state-flow/BACKLOG.md +0 -13
  5. package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +15 -0
  6. package/node_modules/@llblab/pi-state-flow/README.md +7 -6
  7. package/node_modules/@llblab/pi-state-flow/dist/lib/config.d.ts +5 -2
  8. package/node_modules/@llblab/pi-state-flow/dist/lib/config.js +17 -16
  9. package/node_modules/@llblab/pi-state-flow/dist/lib/context.d.ts +8 -0
  10. package/node_modules/@llblab/pi-state-flow/dist/lib/context.js +24 -0
  11. package/node_modules/@llblab/pi-state-flow/dist/lib/continuation.js +4 -3
  12. package/node_modules/@llblab/pi-state-flow/dist/lib/durable.d.ts +1 -0
  13. package/node_modules/@llblab/pi-state-flow/dist/lib/durable.js +8 -3
  14. package/node_modules/@llblab/pi-state-flow/dist/lib/episode.d.ts +2 -0
  15. package/node_modules/@llblab/pi-state-flow/dist/lib/episode.js +4 -0
  16. package/node_modules/@llblab/pi-state-flow/dist/lib/extension.d.ts +5 -0
  17. package/node_modules/@llblab/pi-state-flow/dist/lib/extension.js +93 -35
  18. package/node_modules/@llblab/pi-state-flow/dist/lib/git.js +20 -23
  19. package/node_modules/@llblab/pi-state-flow/dist/lib/history.js +8 -4
  20. package/node_modules/@llblab/pi-state-flow/dist/lib/json.js +29 -3
  21. package/node_modules/@llblab/pi-state-flow/dist/lib/migration.js +34 -18
  22. package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.js +14 -16
  23. package/node_modules/@llblab/pi-state-flow/dist/lib/query.d.ts +27 -1
  24. package/node_modules/@llblab/pi-state-flow/dist/lib/query.js +182 -10
  25. package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.d.ts +2 -0
  26. package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.js +34 -5
  27. package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.d.ts +5 -5
  28. package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.js +20 -24
  29. package/node_modules/@llblab/pi-state-flow/dist/lib/state.d.ts +6 -3
  30. package/node_modules/@llblab/pi-state-flow/dist/lib/state.js +5 -3
  31. package/node_modules/@llblab/pi-state-flow/dist/lib/status.js +1 -1
  32. package/node_modules/@llblab/pi-state-flow/dist/lib/storage.d.ts +2 -0
  33. package/node_modules/@llblab/pi-state-flow/dist/lib/storage.js +55 -20
  34. package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.d.ts +8 -6
  35. package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.js +10 -3
  36. package/node_modules/@llblab/pi-state-flow/dist/lib/transition.js +7 -3
  37. package/node_modules/@llblab/pi-state-flow/dist/package.json +1 -1
  38. package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-memory/SKILL.md +11 -3
  39. package/node_modules/@llblab/pi-state-flow/docs/README.md +1 -0
  40. package/node_modules/@llblab/pi-state-flow/docs/architecture.md +5 -3
  41. package/node_modules/@llblab/pi-state-flow/docs/filesystem-recovery.md +3 -2
  42. package/node_modules/@llblab/pi-state-flow/docs/lazy-state.md +504 -0
  43. package/node_modules/@llblab/pi-state-flow/docs/usage.md +11 -8
  44. package/node_modules/@llblab/pi-state-flow/lib/config.ts +18 -15
  45. package/node_modules/@llblab/pi-state-flow/lib/context.ts +24 -0
  46. package/node_modules/@llblab/pi-state-flow/lib/continuation.ts +4 -3
  47. package/node_modules/@llblab/pi-state-flow/lib/durable.ts +8 -4
  48. package/node_modules/@llblab/pi-state-flow/lib/episode.ts +5 -0
  49. package/node_modules/@llblab/pi-state-flow/lib/extension.ts +86 -34
  50. package/node_modules/@llblab/pi-state-flow/lib/git.ts +23 -22
  51. package/node_modules/@llblab/pi-state-flow/lib/history.ts +8 -4
  52. package/node_modules/@llblab/pi-state-flow/lib/json.ts +31 -3
  53. package/node_modules/@llblab/pi-state-flow/lib/migration.ts +33 -16
  54. package/node_modules/@llblab/pi-state-flow/lib/protocol.ts +14 -16
  55. package/node_modules/@llblab/pi-state-flow/lib/query.ts +173 -9
  56. package/node_modules/@llblab/pi-state-flow/lib/runtime.ts +32 -4
  57. package/node_modules/@llblab/pi-state-flow/lib/snapshot.ts +22 -25
  58. package/node_modules/@llblab/pi-state-flow/lib/state.ts +11 -6
  59. package/node_modules/@llblab/pi-state-flow/lib/status.ts +1 -1
  60. package/node_modules/@llblab/pi-state-flow/lib/storage.ts +46 -18
  61. package/node_modules/@llblab/pi-state-flow/lib/telegram.ts +19 -6
  62. package/node_modules/@llblab/pi-state-flow/lib/transition.ts +7 -3
  63. package/node_modules/@llblab/pi-state-flow/package.json +1 -1
  64. package/node_modules/@llblab/pi-state-flow/skills/state-flow-memory/SKILL.md +11 -3
  65. package/node_modules/@llblab/pi-telegram/AGENTS.md +1 -1
  66. package/node_modules/@llblab/pi-telegram/BACKLOG.md +1 -0
  67. package/node_modules/@llblab/pi-telegram/CHANGELOG.md +7 -0
  68. package/node_modules/@llblab/pi-telegram/README.md +1 -0
  69. package/node_modules/@llblab/pi-telegram/dist/lib/bindings.d.ts +2 -1
  70. package/node_modules/@llblab/pi-telegram/dist/lib/bindings.js +2 -1
  71. package/node_modules/@llblab/pi-telegram/dist/lib/commands.d.ts +134 -2
  72. package/node_modules/@llblab/pi-telegram/dist/lib/commands.js +297 -16
  73. package/node_modules/@llblab/pi-telegram/dist/lib/delivery.d.ts +2 -0
  74. package/node_modules/@llblab/pi-telegram/dist/lib/delivery.js +11 -0
  75. package/node_modules/@llblab/pi-telegram/dist/lib/extension.js +68 -8
  76. package/node_modules/@llblab/pi-telegram/dist/lib/menu-settings.d.ts +4 -3
  77. package/node_modules/@llblab/pi-telegram/dist/lib/menu-settings.js +17 -13
  78. package/node_modules/@llblab/pi-telegram/dist/lib/pi.d.ts +2 -1
  79. package/node_modules/@llblab/pi-telegram/dist/lib/pi.js +1 -0
  80. package/node_modules/@llblab/pi-telegram/dist/lib/routing.d.ts +1 -0
  81. package/node_modules/@llblab/pi-telegram/dist/lib/routing.js +55 -5
  82. package/node_modules/@llblab/pi-telegram/dist/lib/thread-display.d.ts +23 -0
  83. package/node_modules/@llblab/pi-telegram/dist/lib/thread-display.js +20 -0
  84. package/node_modules/@llblab/pi-telegram/dist/lib/threads.d.ts +18 -0
  85. package/node_modules/@llblab/pi-telegram/dist/lib/threads.js +152 -6
  86. package/node_modules/@llblab/pi-telegram/dist/lib/updates.d.ts +1 -0
  87. package/node_modules/@llblab/pi-telegram/dist/lib/updates.js +5 -3
  88. package/node_modules/@llblab/pi-telegram/dist/package.json +1 -1
  89. package/node_modules/@llblab/pi-telegram/docs/architecture.md +3 -3
  90. package/node_modules/@llblab/pi-telegram/docs/callback-namespaces.md +1 -1
  91. package/node_modules/@llblab/pi-telegram/docs/public-api.md +4 -3
  92. package/node_modules/@llblab/pi-telegram/docs/sections.md +2 -2
  93. package/node_modules/@llblab/pi-telegram/docs/ui-style.md +2 -1
  94. package/node_modules/@llblab/pi-telegram/docs/updates.md +1 -1
  95. package/node_modules/@llblab/pi-telegram/lib/bindings.ts +3 -0
  96. package/node_modules/@llblab/pi-telegram/lib/commands.ts +466 -21
  97. package/node_modules/@llblab/pi-telegram/lib/delivery.ts +15 -0
  98. package/node_modules/@llblab/pi-telegram/lib/extension.ts +77 -9
  99. package/node_modules/@llblab/pi-telegram/lib/menu-settings.ts +25 -10
  100. package/node_modules/@llblab/pi-telegram/lib/pi.ts +3 -0
  101. package/node_modules/@llblab/pi-telegram/lib/routing.ts +84 -17
  102. package/node_modules/@llblab/pi-telegram/lib/thread-display.ts +30 -0
  103. package/node_modules/@llblab/pi-telegram/lib/threads.ts +199 -6
  104. package/node_modules/@llblab/pi-telegram/lib/updates.ts +5 -2
  105. package/node_modules/@llblab/pi-telegram/package.json +1 -1
  106. package/package.json +3 -3
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Telegram command routing helpers
3
3
  * Zones: telegram controls, pi agent commands, queue controls
4
- * Owns Telegram slash-command normalization, bot command metadata, and pi-side command registration behind runtime ports
4
+ * Owns Telegram slash-command normalization, bot command metadata, pi-side command registration, and command-initiated session replacement orchestration behind runtime ports
5
5
  */
6
6
  import { pairTelegramUserIfNeeded, TELEGRAM_DEFAULT_PROFILE_NAME, } from "./config.js";
7
7
  import { escapeHtml } from "./rendering.js";
@@ -89,6 +89,7 @@ export const TELEGRAM_COMMAND_EMOJI = {
89
89
  abort: "⏹️",
90
90
  stop: "🟥",
91
91
  name: "🏷️",
92
+ new: "🆕",
92
93
  };
93
94
  export function getTelegramCommandEmoji(command) {
94
95
  return TELEGRAM_COMMAND_EMOJI[command];
@@ -133,22 +134,22 @@ export const TELEGRAM_BUILTIN_BOT_COMMANDS = [
133
134
  command: "start",
134
135
  description: formatTelegramBotCommandDescription("start", "Open menu / Pair bridge"),
135
136
  },
136
- {
137
- command: "name",
138
- description: formatTelegramBotCommandDescription("name", "Rename this thread"),
139
- },
140
137
  {
141
138
  command: "compact",
142
139
  description: formatTelegramBotCommandDescription("compact", "Compact current session"),
143
140
  },
144
141
  {
145
- command: "next",
146
- description: formatTelegramBotCommandDescription("next", "Force next turn"),
142
+ command: "new",
143
+ description: formatTelegramBotCommandDescription("new", "Start a new session"),
147
144
  },
148
145
  {
149
146
  command: "continue",
150
147
  description: formatTelegramBotCommandDescription("continue", "Queue continue prompt"),
151
148
  },
149
+ {
150
+ command: "next",
151
+ description: formatTelegramBotCommandDescription("next", "Force next turn"),
152
+ },
152
153
  {
153
154
  command: "abort",
154
155
  description: formatTelegramBotCommandDescription("abort", "Abort Pi"),
@@ -179,15 +180,15 @@ export async function registerTelegramBotCommands(deps) {
179
180
  await deps.setMyCommands(TELEGRAM_BOT_COMMANDS);
180
181
  return;
181
182
  }
182
- const compactCommandIndex = TELEGRAM_BOT_COMMANDS.findIndex((command) => command.command === "compact");
183
- if (compactCommandIndex === -1) {
183
+ const nextCommandIndex = TELEGRAM_BOT_COMMANDS.findIndex((command) => command.command === "next");
184
+ if (nextCommandIndex === -1) {
184
185
  await deps.setMyCommands([...TELEGRAM_BOT_COMMANDS, ...extensionCommands]);
185
186
  return;
186
187
  }
187
188
  await deps.setMyCommands([
188
- ...TELEGRAM_BOT_COMMANDS.slice(0, compactCommandIndex + 1),
189
+ ...TELEGRAM_BOT_COMMANDS.slice(0, nextCommandIndex + 1),
189
190
  ...extensionCommands,
190
- ...TELEGRAM_BOT_COMMANDS.slice(compactCommandIndex + 1),
191
+ ...TELEGRAM_BOT_COMMANDS.slice(nextCommandIndex + 1),
191
192
  ]);
192
193
  }
193
194
  export function createTelegramBotCommandRegistrar(deps) {
@@ -412,6 +413,7 @@ export function registerTelegramBridgeCommands(pi, deps) {
412
413
  export const TELEGRAM_RESERVED_COMMAND_NAMES = [
413
414
  "stop",
414
415
  "name",
416
+ "new",
415
417
  "abort",
416
418
  "next",
417
419
  "continue",
@@ -529,10 +531,10 @@ export const TELEGRAM_APP_MENU_INTRO_HTML = [
529
531
  "<b>Pi Telegram</b>",
530
532
  "",
531
533
  `${formatTelegramCommandEmojiPrefix("start")}/start — Open menu / Pair bridge`,
532
- `${formatTelegramCommandEmojiPrefix("name")}/name — Rename this thread`,
533
534
  `${formatTelegramCommandEmojiPrefix("compact")}/compact — Compact current session`,
534
- `${formatTelegramCommandEmojiPrefix("next")}/next — Force next turn`,
535
+ `${formatTelegramCommandEmojiPrefix("new")}/new — Start a new session`,
535
536
  `${formatTelegramCommandEmojiPrefix("continue")}/continue — Queue continue prompt`,
537
+ `${formatTelegramCommandEmojiPrefix("next")}/next — Force next turn`,
536
538
  `${formatTelegramCommandEmojiPrefix("abort")}/abort — Abort Pi`,
537
539
  `${formatTelegramCommandEmojiPrefix("stop")}/stop — Abort Pi & Clear queue`,
538
540
  ].join("\n");
@@ -567,11 +569,11 @@ function buildTelegramAppMenuIntroHtml() {
567
569
  "<b>Pi Telegram</b>",
568
570
  "",
569
571
  `${formatTelegramCommandEmojiPrefix("start")}/start — Open menu / Pair bridge`,
570
- `${formatTelegramCommandEmojiPrefix("name")}/name — Rename this thread`,
571
572
  `${formatTelegramCommandEmojiPrefix("compact")}/compact — Compact current session`,
572
- ...extensionLines,
573
- `${formatTelegramCommandEmojiPrefix("next")}/next — Force next turn`,
573
+ `${formatTelegramCommandEmojiPrefix("new")}/new — Start a new session`,
574
574
  `${formatTelegramCommandEmojiPrefix("continue")}/continue — Queue continue prompt`,
575
+ `${formatTelegramCommandEmojiPrefix("next")}/next — Force next turn`,
576
+ ...extensionLines,
575
577
  `${formatTelegramCommandEmojiPrefix("abort")}/abort — Abort Pi`,
576
578
  `${formatTelegramCommandEmojiPrefix("stop")}/stop — Abort Pi & Clear queue`,
577
579
  ].join("\n");
@@ -623,6 +625,7 @@ export function parseTelegramCommand(text) {
623
625
  export const TELEGRAM_COMMAND_ACTIONS = {
624
626
  stop: { kind: "stop", executionMode: "immediate" },
625
627
  name: { kind: "name", executionMode: "immediate" },
628
+ new: { kind: "new", executionMode: "immediate" },
626
629
  abort: { kind: "abort", executionMode: "immediate" },
627
630
  next: { kind: "next", executionMode: "immediate" },
628
631
  continue: { kind: "continue", executionMode: "immediate" },
@@ -709,6 +712,43 @@ function dispatchNextQueuedTelegramTurnAfterCompact(deps) {
709
712
  }
710
713
  deps.dispatchNextQueuedTelegramTurn();
711
714
  }
715
+ export function buildTelegramNewConfirmationReplyMarkup() {
716
+ return {
717
+ inline_keyboard: [
718
+ [
719
+ { text: "🆕 Yes, start new", callback_data: "new:confirm" },
720
+ { text: "❌ No", callback_data: "new:cancel" },
721
+ ],
722
+ ],
723
+ };
724
+ }
725
+ export function getTelegramNewConfirmationHtml() {
726
+ return "<b>Start a new session?</b>";
727
+ }
728
+ export async function openTelegramNewConfirmation(target, deps) {
729
+ await deps.sendInteractiveMessage(target.chatId, getTelegramNewConfirmationHtml(), "html", buildTelegramNewConfirmationReplyMarkup(), target.threadId !== undefined
730
+ ? { target: { chatId: target.chatId, threadId: target.threadId } }
731
+ : undefined);
732
+ }
733
+ export async function handleTelegramNewConfirmationCallback(query, deps) {
734
+ if (query.data !== "new:confirm" && query.data !== "new:cancel")
735
+ return false;
736
+ const chatId = query.message?.chat?.id;
737
+ const messageId = query.message?.message_id;
738
+ if (typeof chatId !== "number" || typeof messageId !== "number") {
739
+ await deps.answerCallbackQuery(query.id, "⌛ Interactive message expired.");
740
+ return true;
741
+ }
742
+ if (query.data === "new:cancel") {
743
+ await deps.editInteractiveMessage(chatId, messageId, "<b>🚫 New session cancelled.</b>", "html", { inline_keyboard: [] });
744
+ await deps.answerCallbackQuery(query.id);
745
+ return true;
746
+ }
747
+ await deps.answerCallbackQuery(query.id);
748
+ await deps.deleteMessage(chatId, messageId);
749
+ await deps.runNew(deps.ctx);
750
+ return true;
751
+ }
712
752
  export function buildTelegramCompactConfirmationReplyMarkup() {
713
753
  return {
714
754
  inline_keyboard: [
@@ -749,6 +789,22 @@ export async function handleTelegramCompactConfirmationCallback(query, deps) {
749
789
  await deps.runCompact(deps.ctx, chatId, messageId, typeof threadId === "number" ? { chatId, threadId } : { chatId });
750
790
  return true;
751
791
  }
792
+ export async function handleTelegramNewCommand(deps) {
793
+ if (!deps.isIdle() ||
794
+ deps.hasPendingMessages() ||
795
+ deps.hasActiveTelegramTurn() ||
796
+ deps.hasDispatchPending() ||
797
+ deps.hasQueuedTelegramItems() ||
798
+ deps.isCompactionInProgress()) {
799
+ await deps.sendTextReply(formatTelegramInformationHeading("⏳", "Cannot start a new session while Pi or the Telegram queue is busy. Wait for queued turns to finish or send /abort first."), { parseMode: "HTML" });
800
+ return;
801
+ }
802
+ if (!deps.requestNewSession) {
803
+ await deps.sendTextReply(formatTelegramInformationHeading("🚫", "Session replacement is unavailable in this Pi runtime."), { parseMode: "HTML" });
804
+ return;
805
+ }
806
+ deps.requestNewSession();
807
+ }
752
808
  export async function handleTelegramCompactCommand(deps) {
753
809
  if (!deps.isIdle() ||
754
810
  deps.hasPendingMessages() ||
@@ -830,6 +886,9 @@ export async function executeTelegramCommandAction(action, message, ctx, deps, c
830
886
  case "name":
831
887
  await deps.handleName(message, ctx, commandArgs);
832
888
  return true;
889
+ case "new":
890
+ await deps.handleNew(message, ctx);
891
+ return true;
833
892
  case "abort":
834
893
  await deps.handleAbort(message, ctx);
835
894
  return true;
@@ -900,6 +959,7 @@ export function createTelegramCommandHandlerTargetRuntime(deps) {
900
959
  stopTypingLoop: deps.stopTypingLoop,
901
960
  enqueueContinueTurn: deps.enqueueContinueTurn,
902
961
  compact: deps.compact,
962
+ requestNewSession: deps.requestNewSession,
903
963
  sendInteractiveMessage: deps.sendInteractiveMessage,
904
964
  enqueueControlItem: commandTargetRuntime.enqueueControlItem,
905
965
  showStatus: commandTargetRuntime.showStatus,
@@ -1075,6 +1135,25 @@ async function handleTelegramCommandRuntime(commandName, message, ctx, deps, com
1075
1135
  handleQueue: async (nextMessage, commandCtx) => {
1076
1136
  scheduleTelegramCommandEffect(commandCtx, "queue", "menu-render", deps, () => deps.openQueueMenu(nextMessage, commandCtx), assertExecutionCurrentFor(nextMessage));
1077
1137
  },
1138
+ handleNew: async (nextMessage, commandCtx) => {
1139
+ if (deps.sendInteractiveMessage) {
1140
+ await openTelegramNewConfirmation(getTelegramCommandMessageTarget(nextMessage), { sendInteractiveMessage: deps.sendInteractiveMessage });
1141
+ return;
1142
+ }
1143
+ await handleTelegramNewCommand({
1144
+ isIdle: () => deps.isIdle(commandCtx),
1145
+ hasPendingMessages: () => deps.hasPendingMessages(commandCtx),
1146
+ hasActiveTelegramTurn: deps.hasActiveTelegramTurn,
1147
+ hasDispatchPending: deps.hasDispatchPending,
1148
+ hasQueuedTelegramItems: deps.hasQueuedTelegramItems,
1149
+ isCompactionInProgress: deps.isCompactionInProgress,
1150
+ requestNewSession: deps.requestNewSession
1151
+ ? () => deps.requestNewSession(nextMessage)
1152
+ : undefined,
1153
+ sendTextReply: sendReplyFor(nextMessage),
1154
+ recordRuntimeEvent: deps.recordRuntimeEvent,
1155
+ });
1156
+ },
1078
1157
  handleCompact: async (nextMessage, commandCtx) => {
1079
1158
  if (deps.sendInteractiveMessage) {
1080
1159
  await openTelegramCompactConfirmation(getTelegramCommandMessageTarget(nextMessage), { sendInteractiveMessage: deps.sendInteractiveMessage });
@@ -1153,3 +1232,205 @@ async function handleTelegramCommandRuntime(commandName, message, ctx, deps, com
1153
1232
  },
1154
1233
  }, commandArgs);
1155
1234
  }
1235
+ export const TELEGRAM_SESSION_ACTION_COMMAND_NAME = "telegram-session-action";
1236
+ export const TELEGRAM_SESSION_ACTION_COMMAND_DESCRIPTION = "(internal) replace the current Pi session after Telegram settlement";
1237
+ export function delayTelegramSessionAction(delayMs) {
1238
+ return new Promise((resolve) => setTimeout(resolve, delayMs));
1239
+ }
1240
+ export async function settleTelegramSessionReplacement(deps) {
1241
+ const now = deps.now ?? Date.now;
1242
+ const sleep = deps.sleep ?? ((delayMs) => new Promise((resolve) => setTimeout(resolve, delayMs)));
1243
+ while (deps.isCurrent?.() !== false) {
1244
+ const intent = await deps.getIntent();
1245
+ if (!intent || intent.sourceSessionId === deps.sessionId)
1246
+ return "none";
1247
+ if (intent.profileName !== deps.profileName || intent.cwd !== deps.cwd)
1248
+ return "stale";
1249
+ if (now() >= intent.expiresAtMs)
1250
+ return "expired";
1251
+ if (!deps.hasSuccessorContinuity(intent)) {
1252
+ await sleep(100);
1253
+ continue;
1254
+ }
1255
+ if (!await deps.clearIntent(intent))
1256
+ return "failed";
1257
+ do {
1258
+ const delivered = await deps.editSuccess(intent);
1259
+ if (delivered.ok)
1260
+ return "settled";
1261
+ if (!delivered.retryable)
1262
+ return "failed";
1263
+ await sleep(100);
1264
+ } while (deps.isCurrent?.() !== false && now() < intent.expiresAtMs);
1265
+ return "failed";
1266
+ }
1267
+ return "stale";
1268
+ }
1269
+ export function createTelegramSessionReplacementSettlementRuntime(deps) {
1270
+ let generation = 0;
1271
+ return {
1272
+ onSessionStart(ctx) {
1273
+ const currentGeneration = ++generation;
1274
+ const resolved = deps.resolve(ctx);
1275
+ if (!resolved)
1276
+ return;
1277
+ void settleTelegramSessionReplacement({
1278
+ ...resolved,
1279
+ isCurrent: () => currentGeneration === generation &&
1280
+ resolved.isCurrent?.() !== false,
1281
+ }).then(deps.onResult, deps.onError);
1282
+ },
1283
+ };
1284
+ }
1285
+ export function createTelegramSessionActionAssembly(deps) {
1286
+ const now = deps.now ?? Date.now;
1287
+ const report = (error) => deps.recordRuntimeEvent?.("new-session", error);
1288
+ const sendTerminalResult = async (target, result) => {
1289
+ const text = result === "success" ? "<b>🆕 New session started.</b>"
1290
+ : result === "cancelled" ? "<b>🚫 New session cancelled.</b>"
1291
+ : "<b>⚠️ New session failed.</b>";
1292
+ const deadline = now() + 10_000;
1293
+ do {
1294
+ const delivery = await deps.sendResult(target, text);
1295
+ if (delivery.ok)
1296
+ return;
1297
+ if (!delivery.retryable)
1298
+ break;
1299
+ await delayTelegramSessionAction(100);
1300
+ } while (now() < deadline);
1301
+ report(new Error("Telegram new-session result delivery failed."));
1302
+ };
1303
+ const action = createTelegramSessionActionRuntime({
1304
+ registerCommand: deps.registerCommand,
1305
+ sendUserMessage: deps.sendUserMessage,
1306
+ notifyResult(target, result) { return sendTerminalResult(target, result); },
1307
+ async prepareReplacement(ctx, updateId, target) {
1308
+ await deps.store.load();
1309
+ const sessionId = ctx.sessionManager.getSessionId();
1310
+ const binding = typeof target.threadId === "number"
1311
+ ? deps.store.getWorkspaceBindingByTarget(target)
1312
+ : undefined;
1313
+ if (typeof target.threadId === "number" &&
1314
+ (!binding || binding.cwd !== ctx.cwd || binding.sessionId !== sessionId)) {
1315
+ throw new Error("Telegram session replacement binding is unavailable.");
1316
+ }
1317
+ const createdAtMs = now();
1318
+ if (!await deps.store.commitSessionReplacementIntent({
1319
+ continuity: binding ? "workspace-thread" : "classic-chat",
1320
+ cwd: binding?.cwd ?? ctx.cwd,
1321
+ profileName: deps.getProfileName() ?? "default",
1322
+ sourceSessionId: sessionId,
1323
+ sourceUpdateId: updateId,
1324
+ target: binding ? { ...binding.target } : { chatId: target.chatId },
1325
+ messageId: target.messageId,
1326
+ ...(binding?.slot ? { slot: binding.slot } : {}),
1327
+ ...(binding?.manualThreadName ?? binding?.threadName
1328
+ ? { threadName: binding.manualThreadName ?? binding.threadName } : {}),
1329
+ createdAtMs,
1330
+ expiresAtMs: createdAtMs + deps.handoffTtlMs,
1331
+ }, deps.ownsPersistence)) {
1332
+ throw new Error("Telegram session replacement intent was not persisted.");
1333
+ }
1334
+ },
1335
+ recordRuntimeEvent: deps.recordRuntimeEvent,
1336
+ });
1337
+ const settlement = createTelegramSessionReplacementSettlementRuntime({
1338
+ resolve(ctx) {
1339
+ const sessionId = ctx.sessionManager?.getSessionId?.();
1340
+ if (!sessionId)
1341
+ return undefined;
1342
+ return {
1343
+ async getIntent() { await deps.store.refresh?.(); return deps.store.getSessionReplacementIntent(); },
1344
+ hasSuccessorContinuity(intent) {
1345
+ return intent.continuity === "classic-chat" ||
1346
+ deps.store.getWorkspaceBindingByTarget(intent.target, sessionId)?.cwd === intent.cwd;
1347
+ },
1348
+ editSuccess(intent) { return deps.sendResult(intent.target, "<b>🆕 New session started.</b>"); },
1349
+ clearIntent(intent) { return deps.store.removeSessionReplacementIntent(intent, deps.ownsPersistence); },
1350
+ profileName: deps.getProfileName() ?? "default",
1351
+ cwd: ctx.cwd,
1352
+ sessionId,
1353
+ };
1354
+ },
1355
+ onResult(result) {
1356
+ if (result === "expired" || result === "failed") {
1357
+ report(new Error(`Telegram session replacement successor settlement ${result}.`));
1358
+ }
1359
+ },
1360
+ onError: report,
1361
+ });
1362
+ return { action, settlement };
1363
+ }
1364
+ export function createTelegramSessionActionRuntime(deps) {
1365
+ let pendingUpdateId;
1366
+ let pendingTarget;
1367
+ let commandPending = false;
1368
+ let commandUpdateId;
1369
+ let registered = false;
1370
+ const reportFailure = (error) => {
1371
+ try {
1372
+ deps.recordRuntimeEvent?.("new-session", error);
1373
+ }
1374
+ catch {
1375
+ // Diagnostics cannot make a completed durable update retryable.
1376
+ }
1377
+ };
1378
+ return {
1379
+ register() {
1380
+ if (registered)
1381
+ return;
1382
+ registered = true;
1383
+ deps.registerCommand(TELEGRAM_SESSION_ACTION_COMMAND_NAME, {
1384
+ description: TELEGRAM_SESSION_ACTION_COMMAND_DESCRIPTION,
1385
+ handler: async (_args, ctx) => {
1386
+ if (!commandPending)
1387
+ return;
1388
+ commandPending = false;
1389
+ const target = pendingTarget;
1390
+ const updateId = commandUpdateId;
1391
+ pendingTarget = undefined;
1392
+ commandUpdateId = undefined;
1393
+ if (!target || updateId === undefined)
1394
+ return;
1395
+ try {
1396
+ await deps.prepareReplacement?.(ctx, updateId, target);
1397
+ const result = await ctx.newSession();
1398
+ if (result.cancelled)
1399
+ await deps.notifyResult(target, "cancelled");
1400
+ }
1401
+ catch (error) {
1402
+ reportFailure(error);
1403
+ await deps.notifyResult(target, "failure");
1404
+ }
1405
+ },
1406
+ });
1407
+ },
1408
+ scheduleAfterUpdate(updateId, target) {
1409
+ if (pendingUpdateId !== undefined || commandPending)
1410
+ return false;
1411
+ pendingUpdateId = updateId;
1412
+ pendingTarget = { ...target };
1413
+ return true;
1414
+ },
1415
+ onUpdateCompleted(updateId) {
1416
+ if (pendingUpdateId !== updateId)
1417
+ return;
1418
+ pendingUpdateId = undefined;
1419
+ commandUpdateId = updateId;
1420
+ commandPending = true;
1421
+ void Promise.resolve()
1422
+ .then(() => deps.sendUserMessage(`/${TELEGRAM_SESSION_ACTION_COMMAND_NAME}`, {
1423
+ expandPromptTemplates: true,
1424
+ }))
1425
+ .catch((error) => {
1426
+ commandPending = false;
1427
+ commandUpdateId = undefined;
1428
+ pendingTarget = undefined;
1429
+ reportFailure(error);
1430
+ });
1431
+ },
1432
+ hasPending() {
1433
+ return pendingUpdateId !== undefined || commandPending;
1434
+ },
1435
+ };
1436
+ }
@@ -156,6 +156,8 @@ export declare function clearTelegramDeliveryRuntime(): void;
156
156
  /** @internal */
157
157
  export declare function isTelegramDeliveryHandleCurrent(handle: TelegramDeliveryHandle): boolean;
158
158
  export declare function sendTelegramView(view: TelegramDeliveryView, options: SendTelegramViewOptions): Promise<TelegramDeliveryResult<TelegramDeliveryHandle>>;
159
+ /** @internal Edit an exact Telegram message through the currently bound runtime generation. */
160
+ export declare function editTelegramTargetView(target: TelegramDeliveryTarget, messageId: number, view: TelegramDeliveryView): Promise<TelegramDeliveryResult<TelegramDeliveryHandle>>;
159
161
  export declare function editTelegramView(handle: TelegramDeliveryHandle, view: TelegramDeliveryView): Promise<TelegramDeliveryResult<TelegramDeliveryHandle>>;
160
162
  export declare function deleteTelegramView(handle: TelegramDeliveryHandle): Promise<TelegramDeliveryResult<void>>;
161
163
  export declare function sendTelegramChatAction(action: TelegramDeliveryChatAction, options: {
@@ -518,6 +518,17 @@ export async function sendTelegramView(view, options) {
518
518
  return invalid;
519
519
  return runDeliveryOperation((runtime) => runtime.sendView(view, options));
520
520
  }
521
+ /** @internal Edit an exact Telegram message through the currently bound runtime generation. */
522
+ export async function editTelegramTargetView(target, messageId, view) {
523
+ const invalid = validateView(view);
524
+ if (invalid)
525
+ return invalid;
526
+ return runDeliveryOperation((runtime) => runtime.editView({
527
+ target: { ...target },
528
+ messageIds: [messageId],
529
+ generation: runtime.generation,
530
+ }, view));
531
+ }
521
532
  export async function editTelegramView(handle, view) {
522
533
  const invalid = validateView(view);
523
534
  if (invalid)
@@ -65,7 +65,7 @@ const telegramBusProtocolIdentity = Bus.createTelegramCurrentBusProtocolIdentity
65
65
  export default function (pi) {
66
66
  Skills.registerTelegramSkillDiscovery(pi);
67
67
  const piRuntime = Pi.createExtensionApiRuntimePorts(pi);
68
- const { getActiveTools, getCommands, getThinkingLevel, sendUserMessage, setActiveTools, setModel, setThinkingLevel, } = piRuntime;
68
+ const { getActiveTools, getCommands, getThinkingLevel, sendUserMessage, registerCommand, setActiveTools, setModel, setThinkingLevel, } = piRuntime;
69
69
  const bridgeRuntime = Runtime.createTelegramBridgeRuntime();
70
70
  const runtimeDiagnostics = Logging.createTelegramRuntimeDiagnosticsRuntime();
71
71
  const runtimeEvents = runtimeDiagnostics.events;
@@ -185,6 +185,23 @@ export default function (pi) {
185
185
  canReset: lockRuntime.owns,
186
186
  commitReset: lockRuntime.commitIfOwned,
187
187
  });
188
+ const sessionActionAssembly = Commands.createTelegramSessionActionAssembly({
189
+ registerCommand,
190
+ sendUserMessage,
191
+ store: threadStore,
192
+ getProfileName: configStore.getActiveProfileName,
193
+ ownsPersistence: lockRuntime.owns,
194
+ async sendResult(target, html) {
195
+ const delivery = await Delivery.sendTelegramView({ text: html, parseMode: "html", replyMarkup: { inline_keyboard: [] } }, { scope: { kind: "target", target } });
196
+ return delivery.ok ? { ok: true } : { ok: false,
197
+ retryable: delivery.reason === "runtime-unavailable" ||
198
+ delivery.reason === "target-unavailable" ||
199
+ delivery.reason === "transport-retryable" };
200
+ },
201
+ handoffTtlMs: Threads.TELEGRAM_LEADER_SESSION_HANDOFF_TTL_MS,
202
+ recordRuntimeEvent,
203
+ });
204
+ const sessionActionsRuntime = sessionActionAssembly.action;
188
205
  const lockOwnershipGuard = Locks.createTelegramLockOwnershipGuard(lockRuntime);
189
206
  const getCurrentLeaderEpoch = lockRuntime.getOwnedLeaderEpoch;
190
207
  const telegramSessionContextStore = Lifecycle.createTelegramSessionContextStore({
@@ -673,6 +690,23 @@ export default function (pi) {
673
690
  updateStatusMessage: menuActions.updateStatusMessage,
674
691
  updateStatus,
675
692
  });
693
+ const threadDisplaySettingsRuntime = ThreadDisplay.createTelegramThreadDisplaySettingsRuntime({
694
+ getTarget() {
695
+ return telegramBusFollowerRegistrationState.getTarget() ??
696
+ telegramBusLeaderState.getTarget();
697
+ },
698
+ getBinding(target) { return threadStore.getWorkspaceBindingByTarget(target); },
699
+ apply(mode) {
700
+ return ThreadDisplay.applyTelegramThreadDisplaySetting(mode, {
701
+ getProfileKey: configStore.getActiveProfileName,
702
+ ownsLeader() { return getCurrentLeaderEpoch() !== undefined; },
703
+ getLeaderSetter() { return telegramBusLeaderRuntime.setThreadDisplayMode; },
704
+ getFollowerSetter() { return telegramBusFollowerRegistration.setThreadDisplayMode; },
705
+ reloadConfig: configStore.load,
706
+ });
707
+ },
708
+ reset(target) { return telegramThreadDisplayNameResetBinding.reset(target); },
709
+ });
676
710
  const settingsMenuRuntime = MenuSettings.createTelegramSettingsMenuRuntime({
677
711
  reloadConfig: configStore.load,
678
712
  getModelMenuState: getQueueMenuState,
@@ -687,15 +721,10 @@ export default function (pi) {
687
721
  return threadStore.getBotState().threadMode === "enabled"
688
722
  ? Config.resolveTelegramThreadDisplayMode(configStore.get()) : undefined;
689
723
  },
724
+ isThreadDisplayCustom: threadDisplaySettingsRuntime.isCustom,
690
725
  async setThreadDisplayMode(mode) {
691
726
  try {
692
- await ThreadDisplay.applyTelegramThreadDisplaySetting(mode, {
693
- getProfileKey: configStore.getActiveProfileName,
694
- ownsLeader() { return getCurrentLeaderEpoch() !== undefined; },
695
- getLeaderSetter() { return telegramBusLeaderRuntime.setThreadDisplayMode; },
696
- getFollowerSetter() { return telegramBusFollowerRegistration.setThreadDisplayMode; },
697
- reloadConfig: configStore.load,
698
- });
727
+ await threadDisplaySettingsRuntime.setMode(mode);
699
728
  }
700
729
  catch (error) {
701
730
  recordRuntimeEvent("bus", error, { phase: "thread-display-setting" });
@@ -767,6 +796,32 @@ export default function (pi) {
767
796
  foreignOwnedUpdateForwarder,
768
797
  replaceFollowerThreadTarget: restoreFollowerThreadTarget,
769
798
  bridgeRuntime,
799
+ requestNewSession(source) {
800
+ const updateId = Updates.getTelegramUpdateExecutionFence(source)?.updateId;
801
+ if (updateId === undefined) {
802
+ throw new Error("Telegram session replacement requires durable update authority.");
803
+ }
804
+ const callbackMessage = source && typeof source === "object" && "message" in source
805
+ ? source.message
806
+ : source;
807
+ const messageTarget = callbackMessage;
808
+ const target = typeof messageTarget?.chat?.id === "number" &&
809
+ typeof messageTarget.message_id === "number"
810
+ ? {
811
+ chatId: messageTarget.chat.id,
812
+ messageId: messageTarget.message_id,
813
+ ...(typeof messageTarget.message_thread_id === "number"
814
+ ? { threadId: messageTarget.message_thread_id }
815
+ : {}),
816
+ }
817
+ : undefined;
818
+ if (!target) {
819
+ throw new Error("Telegram session replacement target is unavailable.");
820
+ }
821
+ if (!sessionActionsRuntime.scheduleAfterUpdate(updateId, target)) {
822
+ throw new Error("A Telegram session replacement is already pending.");
823
+ }
824
+ },
770
825
  activeTurnRuntime,
771
826
  mediaGroupRuntime,
772
827
  textGroupRuntime,
@@ -856,6 +911,7 @@ export default function (pi) {
856
911
  isContextCurrent: telegramSessionContextStore.isCurrent,
857
912
  dispatchNext: dispatchNextQueuedTelegramTurn,
858
913
  requestQueueHandoffReconciliation: queueHandoffReconciliationBinding.request,
914
+ afterUpdateCompleted: sessionActionsRuntime.onUpdateCompleted,
859
915
  },
860
916
  worker: {
861
917
  defaultHandle: inboundRouteRuntime.handleUpdate,
@@ -1366,6 +1422,7 @@ export default function (pi) {
1366
1422
  }
1367
1423
  },
1368
1424
  }.reset);
1425
+ sessionActionsRuntime.register();
1369
1426
  Bindings.registerTelegramCommandsAndTools({
1370
1427
  pi,
1371
1428
  agentDir: Paths.resolveAgentDir(),
@@ -1566,6 +1623,9 @@ export default function (pi) {
1566
1623
  disconnectOnQuit: cleanupTelegramThreadForSessionRestart,
1567
1624
  shutdownGenerativeAppLiveSurfaces: generativeAppLiveSurfaceBinding.shutdown,
1568
1625
  resolveAutomaticThreadCleanupEnabled: configControls.resolveAutomaticThreadCleanupEnabled,
1626
+ onSessionStarted(_event, ctx) {
1627
+ sessionActionAssembly.settlement.onSessionStart(ctx);
1628
+ },
1569
1629
  buttonActionStore,
1570
1630
  callMultipart,
1571
1631
  sendChatAction,
@@ -12,6 +12,7 @@ import type { TelegramVoiceReplyMode } from "./voice.ts";
12
12
  export type TelegramSettingsMenuReplyMarkup = TelegramInlineKeyboardMarkup;
13
13
  export interface TelegramSettingsStateDeps {
14
14
  getThreadDisplayMode?: () => TelegramThreadDisplayMode | undefined;
15
+ isThreadDisplayCustom?: () => boolean;
15
16
  areDraftPreviewsEnabled: () => boolean;
16
17
  getAssistantRenderingMode: () => TelegramAssistantRenderingMode;
17
18
  getActivityVerbosity: () => TelegramActivityVerbosity;
@@ -86,7 +87,7 @@ export declare const TIME_INJECTION_MODE_SETTINGS_TITLE = "<b>\uD83D\uDD52 Time
86
87
  export declare const VOICE_REPLY_MODE_SETTINGS_TITLE = "<b>\uD83D\uDC44 Voice reply mode:</b>";
87
88
  export declare const THREAD_DISPLAY_SETTINGS_TITLE = "<b>\uD83E\uDDF5 Thread display:</b>";
88
89
  export declare function buildTelegramSettingsMenuText(): string;
89
- export declare function buildThreadDisplaySettingsText(mode: TelegramThreadDisplayMode): string;
90
+ export declare function buildThreadDisplaySettingsText(mode: TelegramThreadDisplayMode, custom?: boolean): string;
90
91
  export declare function buildAutomaticThreadCleanupSettingsText(enabled: boolean): string;
91
92
  export declare function buildInactiveThreadReviewText(count: number): string;
92
93
  export declare function buildInactiveThreadReviewReplyMarkup(operationId?: string, canCleanInactiveThreads?: boolean): TelegramSettingsMenuReplyMarkup;
@@ -95,9 +96,9 @@ export declare function buildAssistantRenderingSettingsText(mode: TelegramAssist
95
96
  export declare function buildActivityVerbositySettingsText(verbosity: TelegramActivityVerbosity): string;
96
97
  export declare function buildVoiceReplyModeSettingsText(mode: TelegramVoiceReplyMode, configured?: boolean): string;
97
98
  export declare function buildTimeInjectionModeSettingsText(mode: TelegramTimeMode): string;
98
- export declare function buildTelegramSettingsMenuReplyMarkup(draftPreviewsEnabled: boolean, assistantRenderingModeOrVoiceReplyMode: TelegramAssistantRenderingMode | TelegramVoiceReplyMode, voiceReplyModeOrTimeInjectionMode: TelegramVoiceReplyMode | TelegramTimeMode, timeInjectionModeOrSectionRegistry?: TelegramTimeMode | TelegramSectionRegistry, sectionRegistryOrVoiceReplyModeConfigured?: TelegramSectionRegistry | boolean, voiceReplyModeConfigured?: boolean, automaticThreadCleanupEnabled?: boolean, activityVerbosity?: TelegramActivityVerbosity, threadDisplayMode?: TelegramThreadDisplayMode): TelegramSettingsMenuReplyMarkup;
99
+ export declare function buildTelegramSettingsMenuReplyMarkup(draftPreviewsEnabled: boolean, assistantRenderingModeOrVoiceReplyMode: TelegramAssistantRenderingMode | TelegramVoiceReplyMode, voiceReplyModeOrTimeInjectionMode: TelegramVoiceReplyMode | TelegramTimeMode, timeInjectionModeOrSectionRegistry?: TelegramTimeMode | TelegramSectionRegistry, sectionRegistryOrVoiceReplyModeConfigured?: TelegramSectionRegistry | boolean, voiceReplyModeConfigured?: boolean, automaticThreadCleanupEnabled?: boolean, activityVerbosity?: TelegramActivityVerbosity, threadDisplayMode?: TelegramThreadDisplayMode, threadDisplayCustom?: boolean): TelegramSettingsMenuReplyMarkup;
99
100
  export declare function openTelegramSettingsMenu<TModel extends MenuModel = MenuModel>(deps: TelegramSettingsMenuOpenDeps<TModel>, sectionRegistry?: TelegramSectionRegistry): Promise<void>;
100
- export declare function buildThreadDisplaySettingsReplyMarkup(mode: TelegramThreadDisplayMode): TelegramSettingsMenuReplyMarkup;
101
+ export declare function buildThreadDisplaySettingsReplyMarkup(mode: TelegramThreadDisplayMode, custom?: boolean): TelegramSettingsMenuReplyMarkup;
101
102
  export declare function buildAutomaticThreadCleanupSettingsReplyMarkup(enabled: boolean, canReviewInactiveThreads?: boolean): TelegramSettingsMenuReplyMarkup;
102
103
  export declare function buildDraftPreviewsSettingsReplyMarkup(enabled: boolean): TelegramSettingsMenuReplyMarkup;
103
104
  export declare function buildAssistantRenderingSettingsReplyMarkup(mode: TelegramAssistantRenderingMode): TelegramSettingsMenuReplyMarkup;