@llblab/pi-kit 0.15.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +2 -2
  3. package/node_modules/@llblab/pi-state-flow/AGENTS.md +12 -12
  4. package/node_modules/@llblab/pi-state-flow/BACKLOG.md +2 -13
  5. package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +31 -0
  6. package/node_modules/@llblab/pi-state-flow/README.md +11 -7
  7. package/node_modules/@llblab/pi-state-flow/dist/lib/config.d.ts +5 -2
  8. package/node_modules/@llblab/pi-state-flow/dist/lib/config.js +17 -16
  9. package/node_modules/@llblab/pi-state-flow/dist/lib/context.d.ts +8 -0
  10. package/node_modules/@llblab/pi-state-flow/dist/lib/context.js +24 -0
  11. package/node_modules/@llblab/pi-state-flow/dist/lib/continuation.js +4 -3
  12. package/node_modules/@llblab/pi-state-flow/dist/lib/durable.d.ts +2 -1
  13. package/node_modules/@llblab/pi-state-flow/dist/lib/durable.js +15 -3
  14. package/node_modules/@llblab/pi-state-flow/dist/lib/episode.d.ts +2 -0
  15. package/node_modules/@llblab/pi-state-flow/dist/lib/episode.js +4 -0
  16. package/node_modules/@llblab/pi-state-flow/dist/lib/extension.d.ts +7 -4
  17. package/node_modules/@llblab/pi-state-flow/dist/lib/extension.js +124 -274
  18. package/node_modules/@llblab/pi-state-flow/dist/lib/git.js +20 -23
  19. package/node_modules/@llblab/pi-state-flow/dist/lib/history.js +8 -4
  20. package/node_modules/@llblab/pi-state-flow/dist/lib/json.js +29 -3
  21. package/node_modules/@llblab/pi-state-flow/dist/lib/logging.d.ts +18 -0
  22. package/node_modules/@llblab/pi-state-flow/dist/lib/logging.js +44 -1
  23. package/node_modules/@llblab/pi-state-flow/dist/lib/migration.d.ts +2 -2
  24. package/node_modules/@llblab/pi-state-flow/dist/lib/migration.js +55 -21
  25. package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.d.ts +7 -0
  26. package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.js +65 -17
  27. package/node_modules/@llblab/pi-state-flow/dist/lib/publication.d.ts +17 -0
  28. package/node_modules/@llblab/pi-state-flow/dist/lib/publication.js +102 -0
  29. package/node_modules/@llblab/pi-state-flow/dist/lib/query.d.ts +43 -1
  30. package/node_modules/@llblab/pi-state-flow/dist/lib/query.js +268 -10
  31. package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.d.ts +2 -0
  32. package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.js +34 -5
  33. package/node_modules/@llblab/pi-state-flow/dist/lib/session.d.ts +17 -0
  34. package/node_modules/@llblab/pi-state-flow/dist/lib/session.js +38 -0
  35. package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.d.ts +5 -5
  36. package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.js +20 -24
  37. package/node_modules/@llblab/pi-state-flow/dist/lib/state.d.ts +11 -3
  38. package/node_modules/@llblab/pi-state-flow/dist/lib/state.js +14 -4
  39. package/node_modules/@llblab/pi-state-flow/dist/lib/status.js +1 -1
  40. package/node_modules/@llblab/pi-state-flow/dist/lib/storage.d.ts +2 -0
  41. package/node_modules/@llblab/pi-state-flow/dist/lib/storage.js +55 -20
  42. package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.d.ts +9 -6
  43. package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.js +10 -3
  44. package/node_modules/@llblab/pi-state-flow/dist/lib/transition.js +8 -3
  45. package/node_modules/@llblab/pi-state-flow/dist/package.json +1 -1
  46. package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-guide/SKILL.md +79 -0
  47. package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-memory/SKILL.md +23 -121
  48. package/node_modules/@llblab/pi-state-flow/docs/README.md +1 -0
  49. package/node_modules/@llblab/pi-state-flow/docs/architecture.md +27 -20
  50. package/node_modules/@llblab/pi-state-flow/docs/compatibility.md +1 -1
  51. package/node_modules/@llblab/pi-state-flow/docs/filesystem-recovery.md +3 -2
  52. package/node_modules/@llblab/pi-state-flow/docs/lazy-state.md +513 -0
  53. package/node_modules/@llblab/pi-state-flow/docs/usage.md +13 -10
  54. package/node_modules/@llblab/pi-state-flow/lib/config.ts +18 -15
  55. package/node_modules/@llblab/pi-state-flow/lib/context.ts +24 -0
  56. package/node_modules/@llblab/pi-state-flow/lib/continuation.ts +4 -3
  57. package/node_modules/@llblab/pi-state-flow/lib/durable.ts +14 -5
  58. package/node_modules/@llblab/pi-state-flow/lib/episode.ts +5 -0
  59. package/node_modules/@llblab/pi-state-flow/lib/extension.ts +119 -262
  60. package/node_modules/@llblab/pi-state-flow/lib/git.ts +23 -22
  61. package/node_modules/@llblab/pi-state-flow/lib/history.ts +8 -4
  62. package/node_modules/@llblab/pi-state-flow/lib/json.ts +31 -3
  63. package/node_modules/@llblab/pi-state-flow/lib/logging.ts +53 -1
  64. package/node_modules/@llblab/pi-state-flow/lib/migration.ts +51 -20
  65. package/node_modules/@llblab/pi-state-flow/lib/protocol.ts +61 -17
  66. package/node_modules/@llblab/pi-state-flow/lib/publication.ts +96 -0
  67. package/node_modules/@llblab/pi-state-flow/lib/query.ts +261 -9
  68. package/node_modules/@llblab/pi-state-flow/lib/runtime.ts +32 -4
  69. package/node_modules/@llblab/pi-state-flow/lib/session.ts +45 -0
  70. package/node_modules/@llblab/pi-state-flow/lib/snapshot.ts +22 -25
  71. package/node_modules/@llblab/pi-state-flow/lib/state.ts +22 -6
  72. package/node_modules/@llblab/pi-state-flow/lib/status.ts +1 -1
  73. package/node_modules/@llblab/pi-state-flow/lib/storage.ts +46 -18
  74. package/node_modules/@llblab/pi-state-flow/lib/telegram.ts +20 -6
  75. package/node_modules/@llblab/pi-state-flow/lib/transition.ts +8 -3
  76. package/node_modules/@llblab/pi-state-flow/package.json +1 -1
  77. package/node_modules/@llblab/pi-state-flow/skills/state-flow-guide/SKILL.md +79 -0
  78. package/node_modules/@llblab/pi-state-flow/skills/state-flow-memory/SKILL.md +23 -121
  79. package/node_modules/@llblab/pi-telegram/AGENTS.md +1 -1
  80. package/node_modules/@llblab/pi-telegram/BACKLOG.md +1 -0
  81. package/node_modules/@llblab/pi-telegram/CHANGELOG.md +7 -0
  82. package/node_modules/@llblab/pi-telegram/README.md +1 -0
  83. package/node_modules/@llblab/pi-telegram/dist/lib/bindings.d.ts +2 -1
  84. package/node_modules/@llblab/pi-telegram/dist/lib/bindings.js +2 -1
  85. package/node_modules/@llblab/pi-telegram/dist/lib/commands.d.ts +134 -2
  86. package/node_modules/@llblab/pi-telegram/dist/lib/commands.js +297 -16
  87. package/node_modules/@llblab/pi-telegram/dist/lib/delivery.d.ts +2 -0
  88. package/node_modules/@llblab/pi-telegram/dist/lib/delivery.js +11 -0
  89. package/node_modules/@llblab/pi-telegram/dist/lib/extension.js +68 -8
  90. package/node_modules/@llblab/pi-telegram/dist/lib/menu-settings.d.ts +4 -3
  91. package/node_modules/@llblab/pi-telegram/dist/lib/menu-settings.js +17 -13
  92. package/node_modules/@llblab/pi-telegram/dist/lib/pi.d.ts +2 -1
  93. package/node_modules/@llblab/pi-telegram/dist/lib/pi.js +1 -0
  94. package/node_modules/@llblab/pi-telegram/dist/lib/routing.d.ts +1 -0
  95. package/node_modules/@llblab/pi-telegram/dist/lib/routing.js +55 -5
  96. package/node_modules/@llblab/pi-telegram/dist/lib/thread-display.d.ts +23 -0
  97. package/node_modules/@llblab/pi-telegram/dist/lib/thread-display.js +20 -0
  98. package/node_modules/@llblab/pi-telegram/dist/lib/threads.d.ts +18 -0
  99. package/node_modules/@llblab/pi-telegram/dist/lib/threads.js +152 -6
  100. package/node_modules/@llblab/pi-telegram/dist/lib/updates.d.ts +1 -0
  101. package/node_modules/@llblab/pi-telegram/dist/lib/updates.js +5 -3
  102. package/node_modules/@llblab/pi-telegram/dist/package.json +1 -1
  103. package/node_modules/@llblab/pi-telegram/docs/architecture.md +3 -3
  104. package/node_modules/@llblab/pi-telegram/docs/callback-namespaces.md +1 -1
  105. package/node_modules/@llblab/pi-telegram/docs/public-api.md +4 -3
  106. package/node_modules/@llblab/pi-telegram/docs/sections.md +2 -2
  107. package/node_modules/@llblab/pi-telegram/docs/ui-style.md +2 -1
  108. package/node_modules/@llblab/pi-telegram/docs/updates.md +1 -1
  109. package/node_modules/@llblab/pi-telegram/lib/bindings.ts +3 -0
  110. package/node_modules/@llblab/pi-telegram/lib/commands.ts +466 -21
  111. package/node_modules/@llblab/pi-telegram/lib/delivery.ts +15 -0
  112. package/node_modules/@llblab/pi-telegram/lib/extension.ts +77 -9
  113. package/node_modules/@llblab/pi-telegram/lib/menu-settings.ts +25 -10
  114. package/node_modules/@llblab/pi-telegram/lib/pi.ts +3 -0
  115. package/node_modules/@llblab/pi-telegram/lib/routing.ts +84 -17
  116. package/node_modules/@llblab/pi-telegram/lib/thread-display.ts +30 -0
  117. package/node_modules/@llblab/pi-telegram/lib/threads.ts +199 -6
  118. package/node_modules/@llblab/pi-telegram/lib/updates.ts +5 -2
  119. package/node_modules/@llblab/pi-telegram/package.json +1 -1
  120. package/package.json +3 -3
@@ -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;
@@ -25,12 +25,14 @@ function getVoiceReplyModeSetting(mode, configured) {
25
25
  export function buildTelegramSettingsMenuText() {
26
26
  return SETTINGS_MENU_TITLE;
27
27
  }
28
- export function buildThreadDisplaySettingsText(mode) {
28
+ export function buildThreadDisplaySettingsText(mode, custom = false) {
29
29
  return [
30
- `${THREAD_DISPLAY_SETTINGS_TITLE} <code>${threadDisplayModeLabel(mode)}</code>`,
30
+ `${THREAD_DISPLAY_SETTINGS_TITLE} <code>${custom ? "custom" : threadDisplayModeLabel(mode)}</code>`,
31
31
  "",
32
32
  "Choose how this bot profile labels Telegram tabs and Pi terminal status. Each slot is unique across this bot profile.",
33
33
  "",
34
+ "Set a manual name for the current Thread with /name <code>Name</code>.",
35
+ "",
34
36
  "<code>-</code> <code>letters</code> (default): show the unique slot, such as <b><i>A</i></b> or <b><i>B</i></b>.",
35
37
  "<code>-</code> <code>names</code>: show the generated dictionary name for the slot, such as <b><i>Anchor</i></b> or <b><i>Briar</i></b>.",
36
38
  "<code>-</code> <code>directory-title</code>: humanize the distinguishing directory path, such as <b><i>Api Tools</i></b> or <b><i>Frontend / Extensions A</i></b>.",
@@ -122,7 +124,7 @@ export function buildTimeInjectionModeSettingsText(mode) {
122
124
  "<code>-</code> <code>interval</code> (default): add time at most once per chat interval (1 hour unless configured).",
123
125
  ].join("\n");
124
126
  }
125
- export function buildTelegramSettingsMenuReplyMarkup(draftPreviewsEnabled, assistantRenderingModeOrVoiceReplyMode, voiceReplyModeOrTimeInjectionMode, timeInjectionModeOrSectionRegistry, sectionRegistryOrVoiceReplyModeConfigured, voiceReplyModeConfigured = true, automaticThreadCleanupEnabled = true, activityVerbosity = "quiet", threadDisplayMode) {
127
+ export function buildTelegramSettingsMenuReplyMarkup(draftPreviewsEnabled, assistantRenderingModeOrVoiceReplyMode, voiceReplyModeOrTimeInjectionMode, timeInjectionModeOrSectionRegistry, sectionRegistryOrVoiceReplyModeConfigured, voiceReplyModeConfigured = true, automaticThreadCleanupEnabled = true, activityVerbosity = "quiet", threadDisplayMode, threadDisplayCustom = false) {
126
128
  const hasRenderingMode = assistantRenderingModeOrVoiceReplyMode === "rich" ||
127
129
  assistantRenderingModeOrVoiceReplyMode === "html";
128
130
  const assistantRenderingMode = hasRenderingMode ? assistantRenderingModeOrVoiceReplyMode : "rich";
@@ -164,14 +166,13 @@ export function buildTelegramSettingsMenuReplyMarkup(draftPreviewsEnabled, assis
164
166
  text: `🕒 Time injection: ${getTelegramSettingsStateValueLabel(timeInjectionMode)}`,
165
167
  callback_data: "settings:open:time-injection",
166
168
  },
167
- {
168
- text: `🧹 Thread cleanup: ${automaticThreadCleanupEnabled ? "on" : "off"}`,
169
- callback_data: "settings:open:automatic-thread-cleanup",
170
- },
171
169
  ];
172
170
  if (threadDisplayMode)
173
171
  settingsButtons.push({
174
- text: `🧵 Thread display: ${threadDisplayMode}`,
172
+ text: `🧹 Thread cleanup: ${automaticThreadCleanupEnabled ? "on" : "off"}`,
173
+ callback_data: "settings:open:automatic-thread-cleanup",
174
+ }, {
175
+ text: `🧵 Thread display: ${threadDisplayCustom ? "custom" : threadDisplayMode}`,
175
176
  callback_data: "settings:open:thread-display",
176
177
  });
177
178
  if (sectionRegistry) {
@@ -186,7 +187,7 @@ export function buildTelegramSettingsMenuReplyMarkup(draftPreviewsEnabled, assis
186
187
  }
187
188
  export async function openTelegramSettingsMenu(deps, sectionRegistry) {
188
189
  const state = await deps.getModelMenuState();
189
- const messageId = await deps.sendSettingsMenu(state, buildTelegramSettingsMenuText(), buildTelegramSettingsMenuReplyMarkup(deps.areDraftPreviewsEnabled(), deps.getAssistantRenderingMode(), deps.getVoiceReplyMode(), deps.getTimeInjectionMode(), sectionRegistry, deps.isVoiceReplyModeConfigured(), deps.isAutomaticThreadCleanupEnabled(), deps.getActivityVerbosity(), deps.getThreadDisplayMode?.()));
190
+ const messageId = await deps.sendSettingsMenu(state, buildTelegramSettingsMenuText(), buildTelegramSettingsMenuReplyMarkup(deps.areDraftPreviewsEnabled(), deps.getAssistantRenderingMode(), deps.getVoiceReplyMode(), deps.getTimeInjectionMode(), sectionRegistry, deps.isVoiceReplyModeConfigured(), deps.isAutomaticThreadCleanupEnabled(), deps.getActivityVerbosity(), deps.getThreadDisplayMode?.(), deps.isThreadDisplayCustom?.() ?? false));
190
191
  if (messageId === undefined)
191
192
  return;
192
193
  state.messageId = messageId;
@@ -202,11 +203,11 @@ function threadDisplayModeLabel(mode) {
202
203
  default: return "letters";
203
204
  }
204
205
  }
205
- export function buildThreadDisplaySettingsReplyMarkup(mode) {
206
+ export function buildThreadDisplaySettingsReplyMarkup(mode, custom = false) {
206
207
  return { inline_keyboard: [
207
208
  [{ text: "⬆️ Back", callback_data: "settings:list" }],
208
209
  ...["letters", "names", "directory-title", "directory-snake"].map((value) => [{
209
- text: `${mode === value ? "🟢 " : ""}${threadDisplayModeLabel(value)}`,
210
+ text: `${!custom && mode === value ? "🟢 " : ""}${threadDisplayModeLabel(value)}`,
210
211
  callback_data: `settings:set:thread-display:${value}`,
211
212
  }]),
212
213
  ] };
@@ -307,7 +308,7 @@ export function buildVoiceReplyModeSettingsReplyMarkup(mode, configured = true)
307
308
  };
308
309
  }
309
310
  export async function updateTelegramSettingsMenuMessage(deps, sectionRegistry) {
310
- await deps.updateSettingsMessage(buildTelegramSettingsMenuText(), buildTelegramSettingsMenuReplyMarkup(deps.areDraftPreviewsEnabled(), deps.getAssistantRenderingMode(), deps.getVoiceReplyMode(), deps.getTimeInjectionMode(), sectionRegistry, deps.isVoiceReplyModeConfigured(), deps.isAutomaticThreadCleanupEnabled(), deps.getActivityVerbosity(), deps.getThreadDisplayMode?.()));
311
+ await deps.updateSettingsMessage(buildTelegramSettingsMenuText(), buildTelegramSettingsMenuReplyMarkup(deps.areDraftPreviewsEnabled(), deps.getAssistantRenderingMode(), deps.getVoiceReplyMode(), deps.getTimeInjectionMode(), sectionRegistry, deps.isVoiceReplyModeConfigured(), deps.isAutomaticThreadCleanupEnabled(), deps.getActivityVerbosity(), deps.getThreadDisplayMode?.(), deps.isThreadDisplayCustom?.() ?? false));
311
312
  }
312
313
  export async function updateAutomaticThreadCleanupSettingsMessage(deps) {
313
314
  const enabled = deps.isAutomaticThreadCleanupEnabled();
@@ -362,7 +363,7 @@ export async function handleTelegramSettingsMenuCallbackAction(callbackQueryId,
362
363
  await deps.answerCallbackQuery(callbackQueryId, "Threaded Mode is no longer available.");
363
364
  return true;
364
365
  }
365
- await deps.updateSettingsMessage(buildThreadDisplaySettingsText(mode), buildThreadDisplaySettingsReplyMarkup(mode));
366
+ await deps.updateSettingsMessage(buildThreadDisplaySettingsText(mode, deps.isThreadDisplayCustom?.() ?? false), buildThreadDisplaySettingsReplyMarkup(mode, deps.isThreadDisplayCustom?.() ?? false));
366
367
  await deps.answerCallbackQuery(callbackQueryId);
367
368
  return true;
368
369
  }
@@ -533,6 +534,7 @@ export function createTelegramSettingsMenuRuntime(deps, sectionRegistry) {
533
534
  getTimeInjectionMode: deps.getTimeInjectionMode,
534
535
  isAutomaticThreadCleanupEnabled: deps.isAutomaticThreadCleanupEnabled,
535
536
  getThreadDisplayMode: deps.getThreadDisplayMode,
537
+ isThreadDisplayCustom: deps.isThreadDisplayCustom,
536
538
  sendSettingsMenu: (state, text, replyMarkup) => deps.sendInteractiveMessage(state.chatId, text, "html", replyMarkup),
537
539
  storeModelMenuState: deps.storeModelMenuState,
538
540
  }, sectionRegistry);
@@ -548,6 +550,7 @@ export function createTelegramSettingsMenuRuntime(deps, sectionRegistry) {
548
550
  getTimeInjectionMode: deps.getTimeInjectionMode,
549
551
  isAutomaticThreadCleanupEnabled: deps.isAutomaticThreadCleanupEnabled,
550
552
  getThreadDisplayMode: deps.getThreadDisplayMode,
553
+ isThreadDisplayCustom: deps.isThreadDisplayCustom,
551
554
  updateSettingsMessage: (text, replyMarkup) => deps.editInteractiveMessage(state.chatId, state.messageId, text, "html", replyMarkup),
552
555
  }, sectionRegistry);
553
556
  },
@@ -577,6 +580,7 @@ export function createTelegramSettingsMenuRuntime(deps, sectionRegistry) {
577
580
  getTimeInjectionMode: deps.getTimeInjectionMode,
578
581
  isAutomaticThreadCleanupEnabled: deps.isAutomaticThreadCleanupEnabled,
579
582
  getThreadDisplayMode: deps.getThreadDisplayMode,
583
+ isThreadDisplayCustom: deps.isThreadDisplayCustom,
580
584
  setThreadDisplayMode: deps.setThreadDisplayMode,
581
585
  setDraftPreviewsEnabled: deps.setDraftPreviewsEnabled,
582
586
  setAssistantRenderingMode: deps.setAssistantRenderingMode,
@@ -56,8 +56,9 @@ export interface PiExtensionApiRuntimePorts {
56
56
  getActiveTools: ExtensionAPI["getActiveTools"];
57
57
  setActiveTools: ExtensionAPI["setActiveTools"];
58
58
  setModel: ExtensionAPI["setModel"];
59
+ registerCommand: ExtensionAPI["registerCommand"];
59
60
  }
60
- export declare function createExtensionApiRuntimePorts(api: Pick<ExtensionAPI, "sendUserMessage" | "exec" | "getCommands" | "getThinkingLevel" | "setThinkingLevel" | "getActiveTools" | "setActiveTools" | "setModel">): PiExtensionApiRuntimePorts;
61
+ export declare function createExtensionApiRuntimePorts(api: Pick<ExtensionAPI, "sendUserMessage" | "exec" | "getCommands" | "getThinkingLevel" | "setThinkingLevel" | "getActiveTools" | "setActiveTools" | "setModel" | "registerCommand">): PiExtensionApiRuntimePorts;
61
62
  export declare function normalizeSettingsManager(manager: unknown): PiSettingsManager;
62
63
  export declare function createSettingsManager(cwd: string): Promise<PiSettingsManager>;
63
64
  export declare function createScopedModelPatternPersister(deps: {
@@ -44,6 +44,7 @@ export function createExtensionApiRuntimePorts(api) {
44
44
  getActiveTools: () => api.getActiveTools(),
45
45
  setActiveTools: (names) => api.setActiveTools(names),
46
46
  setModel: (model) => api.setModel(model),
47
+ registerCommand: (name, options) => api.registerCommand(name, options),
47
48
  };
48
49
  }
49
50
  function readEnabledModels(value) {
@@ -164,6 +164,7 @@ export interface TelegramInboundRouteRuntimeDeps<TMessage extends TelegramRouted
164
164
  sendUserMessage?: (message: string, options?: Queue.TelegramPromptDeliveryOptions) => void;
165
165
  isIdle: (ctx: TContext) => boolean;
166
166
  hasPendingMessages: (ctx: TContext) => boolean;
167
+ requestNewSession?: (source: unknown) => void;
167
168
  compact: (ctx: TContext, callbacks: {
168
169
  onComplete: () => void;
169
170
  onError: (error: unknown) => void;
@@ -322,6 +322,7 @@ const TELEGRAM_OWNED_CALLBACK_PREFIXES = [
322
322
  "compact:",
323
323
  "menu:",
324
324
  "model:",
325
+ "new:",
325
326
  "queue:",
326
327
  "section:",
327
328
  "settings:",
@@ -1216,7 +1217,7 @@ export function createTelegramInboundRouteRuntime(deps) {
1216
1217
  return;
1217
1218
  }
1218
1219
  if (selected.kind === "cancel") {
1219
- await deps.editInteractiveMessage?.(chatId, dialogMessageId, "<b>✖ Rename cancelled. Returning to normal agent mode.</b>", "html", { inline_keyboard: [] });
1220
+ await deps.editInteractiveMessage?.(chatId, dialogMessageId, "<b>✖ Rename cancelled.</b>", "html", { inline_keyboard: [] });
1220
1221
  await deps.answerCallbackQuery(query.id);
1221
1222
  return;
1222
1223
  }
@@ -1241,6 +1242,36 @@ export function createTelegramInboundRouteRuntime(deps) {
1241
1242
  }
1242
1243
  return;
1243
1244
  }
1245
+ const handledByNew = await Commands.handleTelegramNewConfirmationCallback(query, {
1246
+ ctx,
1247
+ answerCallbackQuery: deps.answerCallbackQuery,
1248
+ editInteractiveMessage: deps.editInteractiveMessage ?? (async () => { }),
1249
+ deleteMessage: deps.deleteMessage ?? (async () => { }),
1250
+ runNew: async (newCtx) => {
1251
+ await Commands.handleTelegramNewCommand({
1252
+ isIdle: () => deps.isIdle(newCtx),
1253
+ hasPendingMessages: () => deps.hasPendingMessages(newCtx),
1254
+ hasActiveTelegramTurn: deps.activeTurnRuntime.has,
1255
+ hasDispatchPending: deps.bridgeRuntime.lifecycle.hasDispatchPending,
1256
+ hasQueuedTelegramItems: deps.telegramQueueStore.hasQueuedItems,
1257
+ isCompactionInProgress: deps.bridgeRuntime.lifecycle.isCompactionInProgress,
1258
+ requestNewSession: deps.requestNewSession
1259
+ ? () => deps.requestNewSession(query)
1260
+ : undefined,
1261
+ sendTextReply: async (text) => {
1262
+ const chatId = query.message?.chat?.id;
1263
+ const messageId = query.message?.message_id;
1264
+ if (typeof chatId !== "number" || typeof messageId !== "number")
1265
+ return;
1266
+ await deps.editInteractiveMessage?.(chatId, messageId, text, "html", { inline_keyboard: [] });
1267
+ },
1268
+ recordRuntimeEvent: deps.recordRuntimeEvent,
1269
+ });
1270
+ },
1271
+ });
1272
+ assertExecutionCurrent();
1273
+ if (handledByNew)
1274
+ return;
1244
1275
  const handledByCompact = await Commands.handleTelegramCompactConfirmationCallback(query, {
1245
1276
  ctx,
1246
1277
  answerCallbackQuery: deps.answerCallbackQuery,
@@ -1392,6 +1423,7 @@ export function createTelegramInboundRouteRuntime(deps) {
1392
1423
  stopTypingLoop: deps.stopTypingLoop,
1393
1424
  enqueueContinueTurn,
1394
1425
  compact: deps.compact,
1426
+ requestNewSession: deps.requestNewSession,
1395
1427
  allocateItemOrder: deps.bridgeRuntime.queue.allocateItemOrder,
1396
1428
  allocateControlOrder: deps.bridgeRuntime.queue.allocateControlOrder,
1397
1429
  appendControlItem: deps.queueMutationRuntime.append,
@@ -1652,9 +1684,18 @@ export function createTelegramInboundRouteRuntime(deps) {
1652
1684
  dialogMessageId: candidate.dialogMessageId,
1653
1685
  });
1654
1686
  }
1655
- await deps.sendTextReply(target.chatId, message.message_id, result.ok && !result.message
1687
+ const replyText = result.ok && !result.message
1656
1688
  ? Commands.formatTelegramAutomaticThreadDisplayNameRestoredHeading(result.threadName ?? name)
1657
- : Commands.formatTelegramInformationHeading(result.ok ? "✅" : "⚠️", result.message ?? "Thread display name reset failed."), { parseMode: "HTML", target });
1689
+ : Commands.formatTelegramInformationHeading(result.ok ? "✅" : "⚠️", result.message ?? "Thread display name reset failed.");
1690
+ if (result.ok) {
1691
+ Updates.assertTelegramUpdateExecutionCurrent(message);
1692
+ Updates.reportTelegramUpdateCompleted(message);
1693
+ void deps.sendTextReply(target.chatId, message.message_id, replyText, { parseMode: "HTML", target }).catch((error) => deps.recordRuntimeEvent?.("telegram-command", error, {
1694
+ command: "name", phase: "reset-result",
1695
+ }));
1696
+ return true;
1697
+ }
1698
+ await deps.sendTextReply(target.chatId, message.message_id, replyText, { parseMode: "HTML", target });
1658
1699
  return true;
1659
1700
  }
1660
1701
  const validationError = deps.validateThreadName?.(name);
@@ -1677,9 +1718,18 @@ export function createTelegramInboundRouteRuntime(deps) {
1677
1718
  dialogMessageId: candidate.dialogMessageId,
1678
1719
  });
1679
1720
  }
1680
- await deps.sendTextReply(target.chatId, message.message_id, result?.ok && !result.message
1721
+ const replyText = result?.ok && !result.message
1681
1722
  ? Commands.formatTelegramThreadDisplayNameSavedHeading(result.threadName ?? consumed.name)
1682
- : Commands.formatTelegramInformationHeading(result?.ok ? "✅" : "⚠️", result?.message ?? "Thread display name update failed."), { parseMode: "HTML", target });
1723
+ : Commands.formatTelegramInformationHeading(result?.ok ? "✅" : "⚠️", result?.message ?? "Thread display name update failed.");
1724
+ if (result?.ok) {
1725
+ Updates.assertTelegramUpdateExecutionCurrent(message);
1726
+ Updates.reportTelegramUpdateCompleted(message);
1727
+ void deps.sendTextReply(target.chatId, message.message_id, replyText, { parseMode: "HTML", target }).catch((error) => deps.recordRuntimeEvent?.("telegram-command", error, {
1728
+ command: "name", phase: "rename-result",
1729
+ }));
1730
+ return true;
1731
+ }
1732
+ await deps.sendTextReply(target.chatId, message.message_id, replyText, { parseMode: "HTML", target });
1683
1733
  return true;
1684
1734
  },
1685
1735
  handleCommand: commandHandler,
@@ -38,6 +38,29 @@ export interface TelegramThreadDisplayReconcilerDeps {
38
38
  captureLiveBindingKeys(bindings: readonly TelegramWorkspaceThreadBinding[]): ReadonlySet<string>;
39
39
  callApi<TResponse>(method: string, body: Record<string, unknown>, options?: TelegramApiCallOptions): Promise<TResponse>;
40
40
  }
41
+ export declare function createTelegramThreadDisplaySettingsRuntime(deps: {
42
+ getTarget(): {
43
+ chatId: number;
44
+ threadId?: number;
45
+ } | undefined;
46
+ getBinding(target: {
47
+ chatId: number;
48
+ threadId?: number;
49
+ }): {
50
+ manualThreadName?: string;
51
+ } | undefined;
52
+ apply(mode: TelegramThreadDisplayMode): Promise<void>;
53
+ reset(target: {
54
+ chatId: number;
55
+ threadId?: number;
56
+ }): Promise<{
57
+ ok: boolean;
58
+ message?: string;
59
+ }>;
60
+ }): {
61
+ isCustom(): boolean;
62
+ setMode(mode: TelegramThreadDisplayMode): Promise<void>;
63
+ };
41
64
  /** Caller owns triggering and reporting; no timer or background retry is created. */
42
65
  export declare function createTelegramThreadDisplayReconciler(deps: TelegramThreadDisplayReconcilerDeps): {
43
66
  reconcile(): Promise<{
@@ -192,6 +192,26 @@ export async function applyTelegramThreadDisplaySetting(mode, deps) {
192
192
  if (deps.getProfileKey() !== profile)
193
193
  throw new Error("Telegram Thread display setting changed profile.");
194
194
  }
195
+ export function createTelegramThreadDisplaySettingsRuntime(deps) {
196
+ return {
197
+ isCustom() {
198
+ const target = deps.getTarget();
199
+ return !!target && typeof deps.getBinding(target)?.manualThreadName === "string";
200
+ },
201
+ async setMode(mode) {
202
+ const target = deps.getTarget();
203
+ const hadManualName = !!target &&
204
+ typeof deps.getBinding(target)?.manualThreadName === "string";
205
+ await deps.apply(mode);
206
+ if (!hadManualName || !target)
207
+ return;
208
+ const reset = await deps.reset(target);
209
+ if (!reset.ok) {
210
+ throw new Error(reset.message ?? "Telegram Thread display override reset failed.");
211
+ }
212
+ },
213
+ };
214
+ }
195
215
  /** Caller owns triggering and reporting; no timer or background retry is created. */
196
216
  export function createTelegramThreadDisplayReconciler(deps) {
197
217
  let tail = Promise.resolve();
@@ -175,6 +175,19 @@ export interface TelegramBotStateSnapshot {
175
175
  lastSlot?: string;
176
176
  lastReconcileAction?: string;
177
177
  }
178
+ export interface TelegramSessionReplacementIntent {
179
+ continuity: "workspace-thread" | "classic-chat";
180
+ cwd: string;
181
+ profileName: string;
182
+ sourceSessionId: string;
183
+ sourceUpdateId: number;
184
+ target: TelegramTarget;
185
+ messageId: number;
186
+ slot?: string;
187
+ threadName?: string;
188
+ createdAtMs: number;
189
+ expiresAtMs: number;
190
+ }
178
191
  export interface TelegramTopicTargetFile {
179
192
  version: 1;
180
193
  source: "snapshot";
@@ -187,6 +200,7 @@ export interface TelegramTopicTargetFile {
187
200
  identities?: TelegramThreadIdentityRecord[];
188
201
  workspaceBindings?: TelegramWorkspaceThreadBinding[];
189
202
  workspaceRetirements?: TelegramWorkspaceRetirementIntent[];
203
+ sessionReplacement?: TelegramSessionReplacementIntent;
190
204
  reservations?: TelegramThreadReservation[];
191
205
  pendingProvisions?: TelegramThreadPendingProvision[];
192
206
  pendingCleanups?: TelegramThreadCleanupIntent[];
@@ -227,6 +241,10 @@ export interface TelegramTopicTargetStore {
227
241
  getIdentityByProfileKey: (profileKey: string) => TelegramThreadIdentityRecord | undefined;
228
242
  forgetIdentityByProfileKey: (profileKey: string) => boolean;
229
243
  listWorkspaceBindings: () => TelegramWorkspaceThreadBinding[];
244
+ getWorkspaceBindingByTarget: (target: TelegramTarget, sessionId?: string) => TelegramWorkspaceThreadBinding | undefined;
245
+ getSessionReplacementIntent: () => TelegramSessionReplacementIntent | undefined;
246
+ commitSessionReplacementIntent: (intent: TelegramSessionReplacementIntent, isCurrent: () => boolean) => Promise<boolean>;
247
+ removeSessionReplacementIntent: (expected: TelegramSessionReplacementIntent, isCurrent: () => boolean) => Promise<boolean>;
230
248
  listWorkspaceRetirementIntents: () => TelegramWorkspaceRetirementIntent[];
231
249
  commitWorkspaceJournalEvidence: (expected: TelegramWorkspaceThreadBinding, journalBindingKeys: readonly string[], complete: boolean) => TelegramWorkspaceThreadBinding | undefined;
232
250
  upsertWorkspaceRetirementIntent: (intent: TelegramWorkspaceRetirementIntent) => boolean;