@llblab/pi-kit 0.14.1 → 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 (119) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +3 -3
  3. package/node_modules/@llblab/pi-codex-usage/CHANGELOG.md +5 -0
  4. package/node_modules/@llblab/pi-codex-usage/README.md +6 -0
  5. package/node_modules/@llblab/pi-codex-usage/index.ts +81 -7
  6. package/node_modules/@llblab/pi-codex-usage/package.json +1 -1
  7. package/node_modules/@llblab/pi-state-flow/AGENTS.md +9 -9
  8. package/node_modules/@llblab/pi-state-flow/BACKLOG.md +0 -13
  9. package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +15 -0
  10. package/node_modules/@llblab/pi-state-flow/README.md +7 -6
  11. package/node_modules/@llblab/pi-state-flow/dist/lib/config.d.ts +5 -2
  12. package/node_modules/@llblab/pi-state-flow/dist/lib/config.js +17 -16
  13. package/node_modules/@llblab/pi-state-flow/dist/lib/context.d.ts +8 -0
  14. package/node_modules/@llblab/pi-state-flow/dist/lib/context.js +24 -0
  15. package/node_modules/@llblab/pi-state-flow/dist/lib/continuation.js +4 -3
  16. package/node_modules/@llblab/pi-state-flow/dist/lib/durable.d.ts +1 -0
  17. package/node_modules/@llblab/pi-state-flow/dist/lib/durable.js +8 -3
  18. package/node_modules/@llblab/pi-state-flow/dist/lib/episode.d.ts +2 -0
  19. package/node_modules/@llblab/pi-state-flow/dist/lib/episode.js +4 -0
  20. package/node_modules/@llblab/pi-state-flow/dist/lib/extension.d.ts +5 -0
  21. package/node_modules/@llblab/pi-state-flow/dist/lib/extension.js +93 -35
  22. package/node_modules/@llblab/pi-state-flow/dist/lib/git.js +20 -23
  23. package/node_modules/@llblab/pi-state-flow/dist/lib/history.js +8 -4
  24. package/node_modules/@llblab/pi-state-flow/dist/lib/json.js +29 -3
  25. package/node_modules/@llblab/pi-state-flow/dist/lib/migration.js +34 -18
  26. package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.js +14 -16
  27. package/node_modules/@llblab/pi-state-flow/dist/lib/query.d.ts +27 -1
  28. package/node_modules/@llblab/pi-state-flow/dist/lib/query.js +182 -10
  29. package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.d.ts +2 -0
  30. package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.js +34 -5
  31. package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.d.ts +5 -5
  32. package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.js +20 -24
  33. package/node_modules/@llblab/pi-state-flow/dist/lib/state.d.ts +6 -3
  34. package/node_modules/@llblab/pi-state-flow/dist/lib/state.js +5 -3
  35. package/node_modules/@llblab/pi-state-flow/dist/lib/status.js +1 -1
  36. package/node_modules/@llblab/pi-state-flow/dist/lib/storage.d.ts +2 -0
  37. package/node_modules/@llblab/pi-state-flow/dist/lib/storage.js +55 -20
  38. package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.d.ts +8 -6
  39. package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.js +10 -3
  40. package/node_modules/@llblab/pi-state-flow/dist/lib/transition.js +7 -3
  41. package/node_modules/@llblab/pi-state-flow/dist/package.json +1 -1
  42. package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-memory/SKILL.md +11 -3
  43. package/node_modules/@llblab/pi-state-flow/docs/README.md +1 -0
  44. package/node_modules/@llblab/pi-state-flow/docs/architecture.md +5 -3
  45. package/node_modules/@llblab/pi-state-flow/docs/filesystem-recovery.md +3 -2
  46. package/node_modules/@llblab/pi-state-flow/docs/lazy-state.md +504 -0
  47. package/node_modules/@llblab/pi-state-flow/docs/usage.md +11 -8
  48. package/node_modules/@llblab/pi-state-flow/lib/config.ts +18 -15
  49. package/node_modules/@llblab/pi-state-flow/lib/context.ts +24 -0
  50. package/node_modules/@llblab/pi-state-flow/lib/continuation.ts +4 -3
  51. package/node_modules/@llblab/pi-state-flow/lib/durable.ts +8 -4
  52. package/node_modules/@llblab/pi-state-flow/lib/episode.ts +5 -0
  53. package/node_modules/@llblab/pi-state-flow/lib/extension.ts +86 -34
  54. package/node_modules/@llblab/pi-state-flow/lib/git.ts +23 -22
  55. package/node_modules/@llblab/pi-state-flow/lib/history.ts +8 -4
  56. package/node_modules/@llblab/pi-state-flow/lib/json.ts +31 -3
  57. package/node_modules/@llblab/pi-state-flow/lib/migration.ts +33 -16
  58. package/node_modules/@llblab/pi-state-flow/lib/protocol.ts +14 -16
  59. package/node_modules/@llblab/pi-state-flow/lib/query.ts +173 -9
  60. package/node_modules/@llblab/pi-state-flow/lib/runtime.ts +32 -4
  61. package/node_modules/@llblab/pi-state-flow/lib/snapshot.ts +22 -25
  62. package/node_modules/@llblab/pi-state-flow/lib/state.ts +11 -6
  63. package/node_modules/@llblab/pi-state-flow/lib/status.ts +1 -1
  64. package/node_modules/@llblab/pi-state-flow/lib/storage.ts +46 -18
  65. package/node_modules/@llblab/pi-state-flow/lib/telegram.ts +19 -6
  66. package/node_modules/@llblab/pi-state-flow/lib/transition.ts +7 -3
  67. package/node_modules/@llblab/pi-state-flow/package.json +1 -1
  68. package/node_modules/@llblab/pi-state-flow/skills/state-flow-memory/SKILL.md +11 -3
  69. package/node_modules/@llblab/pi-telegram/AGENTS.md +1 -1
  70. package/node_modules/@llblab/pi-telegram/BACKLOG.md +1 -0
  71. package/node_modules/@llblab/pi-telegram/CHANGELOG.md +12 -0
  72. package/node_modules/@llblab/pi-telegram/README.md +1 -0
  73. package/node_modules/@llblab/pi-telegram/dist/lib/activity.d.ts +2 -0
  74. package/node_modules/@llblab/pi-telegram/dist/lib/activity.js +12 -0
  75. package/node_modules/@llblab/pi-telegram/dist/lib/bindings.d.ts +3 -2
  76. package/node_modules/@llblab/pi-telegram/dist/lib/bindings.js +7 -2
  77. package/node_modules/@llblab/pi-telegram/dist/lib/commands.d.ts +134 -2
  78. package/node_modules/@llblab/pi-telegram/dist/lib/commands.js +297 -16
  79. package/node_modules/@llblab/pi-telegram/dist/lib/delivery.d.ts +2 -0
  80. package/node_modules/@llblab/pi-telegram/dist/lib/delivery.js +11 -0
  81. package/node_modules/@llblab/pi-telegram/dist/lib/extension.js +68 -8
  82. package/node_modules/@llblab/pi-telegram/dist/lib/menu-settings.d.ts +4 -3
  83. package/node_modules/@llblab/pi-telegram/dist/lib/menu-settings.js +17 -13
  84. package/node_modules/@llblab/pi-telegram/dist/lib/pi.d.ts +2 -1
  85. package/node_modules/@llblab/pi-telegram/dist/lib/pi.js +1 -0
  86. package/node_modules/@llblab/pi-telegram/dist/lib/queue.d.ts +2 -0
  87. package/node_modules/@llblab/pi-telegram/dist/lib/queue.js +5 -1
  88. package/node_modules/@llblab/pi-telegram/dist/lib/replies.d.ts +1 -0
  89. package/node_modules/@llblab/pi-telegram/dist/lib/replies.js +1 -0
  90. package/node_modules/@llblab/pi-telegram/dist/lib/routing.d.ts +1 -0
  91. package/node_modules/@llblab/pi-telegram/dist/lib/routing.js +59 -7
  92. package/node_modules/@llblab/pi-telegram/dist/lib/thread-display.d.ts +23 -0
  93. package/node_modules/@llblab/pi-telegram/dist/lib/thread-display.js +20 -0
  94. package/node_modules/@llblab/pi-telegram/dist/lib/threads.d.ts +18 -0
  95. package/node_modules/@llblab/pi-telegram/dist/lib/threads.js +152 -6
  96. package/node_modules/@llblab/pi-telegram/dist/lib/updates.d.ts +1 -0
  97. package/node_modules/@llblab/pi-telegram/dist/lib/updates.js +5 -3
  98. package/node_modules/@llblab/pi-telegram/dist/package.json +1 -1
  99. package/node_modules/@llblab/pi-telegram/docs/architecture.md +3 -3
  100. package/node_modules/@llblab/pi-telegram/docs/callback-namespaces.md +1 -1
  101. package/node_modules/@llblab/pi-telegram/docs/public-api.md +4 -3
  102. package/node_modules/@llblab/pi-telegram/docs/sections.md +2 -2
  103. package/node_modules/@llblab/pi-telegram/docs/ui-style.md +2 -1
  104. package/node_modules/@llblab/pi-telegram/docs/updates.md +1 -1
  105. package/node_modules/@llblab/pi-telegram/lib/activity.ts +14 -0
  106. package/node_modules/@llblab/pi-telegram/lib/bindings.ts +10 -2
  107. package/node_modules/@llblab/pi-telegram/lib/commands.ts +466 -21
  108. package/node_modules/@llblab/pi-telegram/lib/delivery.ts +15 -0
  109. package/node_modules/@llblab/pi-telegram/lib/extension.ts +77 -9
  110. package/node_modules/@llblab/pi-telegram/lib/menu-settings.ts +25 -10
  111. package/node_modules/@llblab/pi-telegram/lib/pi.ts +3 -0
  112. package/node_modules/@llblab/pi-telegram/lib/queue.ts +7 -1
  113. package/node_modules/@llblab/pi-telegram/lib/replies.ts +2 -0
  114. package/node_modules/@llblab/pi-telegram/lib/routing.ts +90 -19
  115. package/node_modules/@llblab/pi-telegram/lib/thread-display.ts +30 -0
  116. package/node_modules/@llblab/pi-telegram/lib/threads.ts +199 -6
  117. package/node_modules/@llblab/pi-telegram/lib/updates.ts +5 -2
  118. package/node_modules/@llblab/pi-telegram/package.json +1 -1
  119. package/package.json +4 -4
@@ -1,11 +1,13 @@
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 { type TelegramConfigStore } from "./config.ts";
7
+ import type * as Pi from "./pi.ts";
7
8
  import type { ExtensionAPI, ExtensionCommandContext } from "./pi.ts";
8
9
  import type { TelegramBridgeStatusLineOptions } from "./status.ts";
10
+ import type { TelegramSessionReplacementIntent } from "./threads.ts";
9
11
  import { type PendingTelegramControlItem, type TelegramQueueAdmissionReceipt } from "./queue.ts";
10
12
  export interface ParsedTelegramCommand {
11
13
  name: string;
@@ -60,6 +62,7 @@ export declare const TELEGRAM_COMMAND_EMOJI: {
60
62
  readonly abort: "⏹️";
61
63
  readonly stop: "🟥";
62
64
  readonly name: "🏷️";
65
+ readonly new: "🆕";
63
66
  };
64
67
  export type TelegramCommandEmojiName = keyof typeof TELEGRAM_COMMAND_EMOJI;
65
68
  export declare function getTelegramCommandEmoji(command: TelegramCommandEmojiName): string;
@@ -144,7 +147,7 @@ export declare function createTelegramThreadDisplayNameRenameBinding(): {
144
147
  };
145
148
  export declare function parseTelegramRequestedThreadName(args: string): string | undefined;
146
149
  export declare function registerTelegramBridgeCommands(pi: ExtensionAPI, deps: TelegramBridgeCommandRegistrationDeps): void;
147
- export declare const TELEGRAM_RESERVED_COMMAND_NAMES: readonly ["stop", "name", "abort", "next", "continue", "status", "queue", "compact", "model", "thinking", "settings", "help", "start"];
150
+ export declare const TELEGRAM_RESERVED_COMMAND_NAMES: readonly ["stop", "name", "new", "abort", "next", "continue", "status", "queue", "compact", "model", "thinking", "settings", "help", "start"];
148
151
  export type TelegramReservedCommandName = (typeof TELEGRAM_RESERVED_COMMAND_NAMES)[number];
149
152
  export declare function isTelegramReservedCommandName(commandName: string | undefined): commandName is TelegramReservedCommandName;
150
153
  export type TelegramCommandAction = {
@@ -156,6 +159,9 @@ export type TelegramCommandAction = {
156
159
  } | {
157
160
  kind: "name";
158
161
  executionMode: "immediate";
162
+ } | {
163
+ kind: "new";
164
+ executionMode: "immediate";
159
165
  } | {
160
166
  kind: "abort";
161
167
  executionMode: "immediate";
@@ -192,6 +198,7 @@ export type TelegramCommandExecutionMode = "ignored" | "immediate";
192
198
  export interface TelegramCommandActionDeps<TMessage, TContext> {
193
199
  handleStop: (message: TMessage, ctx: TContext) => Promise<void>;
194
200
  handleName: (message: TMessage, ctx: TContext, name: string) => Promise<void>;
201
+ handleNew: (message: TMessage, ctx: TContext) => Promise<void>;
195
202
  handleAbort: (message: TMessage, ctx: TContext) => Promise<void>;
196
203
  handleNext: (message: TMessage, ctx: TContext) => Promise<void>;
197
204
  handleContinue: (message: TMessage, ctx: TContext) => Promise<void>;
@@ -264,6 +271,13 @@ export interface TelegramCompactConfirmationCallbackQuery {
264
271
  message_thread_id?: number;
265
272
  };
266
273
  }
274
+ export interface TelegramNewConfirmationCallbackDeps<TContext> {
275
+ ctx: TContext;
276
+ answerCallbackQuery: (callbackQueryId: string, text?: string) => Promise<void>;
277
+ editInteractiveMessage: (chatId: number, messageId: number, text: string, mode: "markdown" | "html" | "plain", replyMarkup: TelegramCompactConfirmationReplyMarkup) => Promise<void>;
278
+ deleteMessage: (chatId: number, messageId: number) => Promise<void>;
279
+ runNew: (ctx: TContext) => Promise<void>;
280
+ }
267
281
  export interface TelegramCompactConfirmationCallbackDeps<TContext> {
268
282
  ctx: TContext;
269
283
  answerCallbackQuery: (callbackQueryId: string, text?: string) => Promise<void>;
@@ -390,6 +404,7 @@ export interface TelegramCommandRuntimeDeps<TMessage extends TelegramCommandRunt
390
404
  }) => void;
391
405
  stopTypingLoop?: () => void;
392
406
  enqueueContinueTurn: (message: TMessage, ctx: TContext) => Promise<void>;
407
+ requestNewSession?: (message: TMessage) => void;
393
408
  compact: (ctx: TContext, callbacks: {
394
409
  onComplete: () => void;
395
410
  onError: (error: unknown) => void;
@@ -434,6 +449,10 @@ export declare const TELEGRAM_COMMAND_ACTIONS: {
434
449
  readonly kind: "name";
435
450
  readonly executionMode: "immediate";
436
451
  };
452
+ readonly new: {
453
+ readonly kind: "new";
454
+ readonly executionMode: "immediate";
455
+ };
437
456
  readonly abort: {
438
457
  readonly kind: "abort";
439
458
  readonly executionMode: "immediate";
@@ -515,10 +534,27 @@ export declare function handleTelegramNextCommand(deps: {
515
534
  export declare function handleTelegramContinueCommand<TMessage, TContext>(message: TMessage, ctx: TContext, deps: {
516
535
  enqueueContinueTurn: (message: TMessage, ctx: TContext) => Promise<void>;
517
536
  }): Promise<void>;
537
+ export declare function buildTelegramNewConfirmationReplyMarkup(): TelegramCompactConfirmationReplyMarkup;
538
+ export declare function getTelegramNewConfirmationHtml(): string;
539
+ export declare function openTelegramNewConfirmation(target: TelegramCommandMessageTarget, deps: TelegramCompactConfirmationDeps): Promise<void>;
540
+ export declare function handleTelegramNewConfirmationCallback<TContext>(query: TelegramCompactConfirmationCallbackQuery, deps: TelegramNewConfirmationCallbackDeps<TContext>): Promise<boolean>;
518
541
  export declare function buildTelegramCompactConfirmationReplyMarkup(): TelegramCompactConfirmationReplyMarkup;
519
542
  export declare function getTelegramCompactConfirmationHtml(): string;
520
543
  export declare function openTelegramCompactConfirmation(target: TelegramCommandMessageTarget, deps: TelegramCompactConfirmationDeps): Promise<void>;
521
544
  export declare function handleTelegramCompactConfirmationCallback<TContext>(query: TelegramCompactConfirmationCallbackQuery, deps: TelegramCompactConfirmationCallbackDeps<TContext>): Promise<boolean>;
545
+ export interface TelegramNewCommandDeps extends TelegramRuntimeEventRecorderPort {
546
+ isIdle: () => boolean;
547
+ hasPendingMessages: () => boolean;
548
+ hasActiveTelegramTurn: () => boolean;
549
+ hasDispatchPending: () => boolean;
550
+ hasQueuedTelegramItems: () => boolean;
551
+ isCompactionInProgress: () => boolean;
552
+ requestNewSession?: () => void;
553
+ sendTextReply: (text: string, options?: {
554
+ parseMode?: "HTML";
555
+ }) => Promise<void>;
556
+ }
557
+ export declare function handleTelegramNewCommand(deps: TelegramNewCommandDeps): Promise<void>;
522
558
  export declare function handleTelegramCompactCommand(deps: TelegramCompactCommandDeps): Promise<void>;
523
559
  export declare function handleTelegramStatusCommand<TContext>(deps: {
524
560
  ctx: TContext;
@@ -538,4 +574,100 @@ export declare function createTelegramCommandHandler<TMessage extends TelegramCo
538
574
  export declare function createTelegramCommandOrPromptRuntime<TMessage, TContext>(deps: TelegramCommandOrPromptRuntimeDeps<TMessage, TContext>): {
539
575
  dispatchMessages: (messages: TMessage[], ctx: TContext) => Promise<void>;
540
576
  };
577
+ export declare const TELEGRAM_SESSION_ACTION_COMMAND_NAME = "telegram-session-action";
578
+ export declare const TELEGRAM_SESSION_ACTION_COMMAND_DESCRIPTION = "(internal) replace the current Pi session after Telegram settlement";
579
+ export declare function delayTelegramSessionAction(delayMs: number): Promise<void>;
580
+ export interface TelegramSessionActionRuntimeDeps {
581
+ registerCommand: Pi.ExtensionAPI["registerCommand"];
582
+ sendUserMessage: Pi.ExtensionAPI["sendUserMessage"];
583
+ notifyResult: (target: {
584
+ chatId: number;
585
+ threadId?: number;
586
+ messageId: number;
587
+ }, result: "success" | "cancelled" | "failure") => Promise<void>;
588
+ prepareReplacement?: (ctx: Pi.ExtensionCommandContext, updateId: number, target: {
589
+ chatId: number;
590
+ threadId?: number;
591
+ messageId: number;
592
+ }) => Promise<void>;
593
+ recordRuntimeEvent?: (category: string, error: unknown) => void;
594
+ }
595
+ export interface TelegramSessionReplacementSettlementDeps {
596
+ getIntent: () => Promise<TelegramSessionReplacementIntent | undefined>;
597
+ hasSuccessorContinuity: (intent: TelegramSessionReplacementIntent) => boolean;
598
+ editSuccess: (intent: TelegramSessionReplacementIntent) => Promise<{
599
+ ok: boolean;
600
+ retryable?: boolean;
601
+ message?: string;
602
+ }>;
603
+ clearIntent: (intent: TelegramSessionReplacementIntent) => Promise<boolean>;
604
+ profileName: string | undefined;
605
+ cwd: string;
606
+ sessionId: string;
607
+ now?: () => number;
608
+ sleep?: (delayMs: number) => Promise<void>;
609
+ isCurrent?: () => boolean;
610
+ }
611
+ export declare function settleTelegramSessionReplacement(deps: TelegramSessionReplacementSettlementDeps): Promise<"none" | "settled" | "expired" | "failed" | "stale">;
612
+ export declare function createTelegramSessionReplacementSettlementRuntime<TContext>(deps: {
613
+ resolve: (ctx: TContext) => TelegramSessionReplacementSettlementDeps | undefined;
614
+ onResult?: (result: "none" | "settled" | "expired" | "failed" | "stale") => void;
615
+ onError?: (error: unknown) => void;
616
+ }): {
617
+ onSessionStart: (ctx: TContext) => void;
618
+ };
619
+ export interface TelegramSessionActionAssemblyDeps {
620
+ registerCommand: Pi.ExtensionAPI["registerCommand"];
621
+ sendUserMessage: Pi.ExtensionAPI["sendUserMessage"];
622
+ store: {
623
+ load: () => Promise<void>;
624
+ refresh?: () => Promise<void>;
625
+ getWorkspaceBindingByTarget: (target: {
626
+ chatId: number;
627
+ threadId?: number;
628
+ }, sessionId?: string) => {
629
+ cwd: string;
630
+ sessionId?: string;
631
+ slot?: string;
632
+ threadName?: string;
633
+ manualThreadName?: string;
634
+ target: {
635
+ chatId: number;
636
+ threadId: number;
637
+ };
638
+ } | undefined;
639
+ getSessionReplacementIntent: () => TelegramSessionReplacementIntent | undefined;
640
+ commitSessionReplacementIntent: (intent: TelegramSessionReplacementIntent, isCurrent: () => boolean) => Promise<boolean>;
641
+ removeSessionReplacementIntent: (intent: TelegramSessionReplacementIntent, isCurrent: () => boolean) => Promise<boolean>;
642
+ };
643
+ getProfileName: () => string | undefined;
644
+ ownsPersistence: () => boolean;
645
+ sendResult: (target: {
646
+ chatId: number;
647
+ threadId?: number;
648
+ }, html: string) => Promise<{
649
+ ok: boolean;
650
+ retryable?: boolean;
651
+ }>;
652
+ handoffTtlMs: number;
653
+ now?: () => number;
654
+ recordRuntimeEvent?: (category: string, error: unknown) => void;
655
+ }
656
+ export declare function createTelegramSessionActionAssembly(deps: TelegramSessionActionAssemblyDeps): {
657
+ action: TelegramSessionActionRuntime;
658
+ settlement: {
659
+ onSessionStart: (ctx: Pi.ExtensionContext) => void;
660
+ };
661
+ };
662
+ export interface TelegramSessionActionRuntime {
663
+ register: () => void;
664
+ scheduleAfterUpdate: (updateId: number, target: {
665
+ chatId: number;
666
+ threadId?: number;
667
+ messageId: number;
668
+ }) => boolean;
669
+ onUpdateCompleted: (updateId: number) => void;
670
+ hasPending: () => boolean;
671
+ }
672
+ export declare function createTelegramSessionActionRuntime(deps: TelegramSessionActionRuntimeDeps): TelegramSessionActionRuntime;
541
673
  export {};
@@ -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)