@llblab/pi-kit 0.1.12 → 0.2.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 (73) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +2 -2
  3. package/node_modules/@llblab/pi-actors/AGENTS.md +5 -3
  4. package/node_modules/@llblab/pi-actors/BACKLOG.md +1 -1
  5. package/node_modules/@llblab/pi-actors/CHANGELOG.md +15 -0
  6. package/node_modules/@llblab/pi-actors/README.md +5 -3
  7. package/node_modules/@llblab/pi-actors/dist/index.js +4 -1
  8. package/node_modules/@llblab/pi-actors/dist/lib/async-runs.d.ts +2 -2
  9. package/node_modules/@llblab/pi-actors/dist/lib/async-runs.js +42 -19
  10. package/node_modules/@llblab/pi-actors/dist/lib/extension-runtime.d.ts +2 -1
  11. package/node_modules/@llblab/pi-actors/dist/lib/extension-runtime.js +7 -2
  12. package/node_modules/@llblab/pi-actors/dist/lib/limits.d.ts +9 -0
  13. package/node_modules/@llblab/pi-actors/dist/lib/limits.js +9 -0
  14. package/node_modules/@llblab/pi-actors/dist/lib/observability.d.ts +10 -11
  15. package/node_modules/@llblab/pi-actors/dist/lib/observability.js +81 -56
  16. package/node_modules/@llblab/pi-actors/dist/lib/pi.d.ts +31 -0
  17. package/node_modules/@llblab/pi-actors/dist/lib/pi.js +180 -0
  18. package/node_modules/@llblab/pi-actors/dist/lib/run-delivery.d.ts +115 -0
  19. package/node_modules/@llblab/pi-actors/dist/lib/run-delivery.js +623 -0
  20. package/node_modules/@llblab/pi-actors/dist/lib/run-ui-runtime.d.ts +3 -0
  21. package/node_modules/@llblab/pi-actors/dist/lib/run-ui-runtime.js +341 -13
  22. package/node_modules/@llblab/pi-actors/dist/lib/runs-trace.d.ts +1 -1
  23. package/node_modules/@llblab/pi-actors/dist/lib/runs-trace.js +5 -3
  24. package/node_modules/@llblab/pi-actors/dist/lib/session-evidence.d.ts +16 -0
  25. package/node_modules/@llblab/pi-actors/dist/lib/session-evidence.js +143 -0
  26. package/node_modules/@llblab/pi-actors/dist/lib/temp.js +1 -1
  27. package/node_modules/@llblab/pi-actors/dist/lib/tools-inspect.js +3 -1
  28. package/node_modules/@llblab/pi-actors/dist/scripts/async-runner.mjs +5 -19
  29. package/node_modules/@llblab/pi-actors/dist/skills/actors/SKILL.md +2 -2
  30. package/node_modules/@llblab/pi-actors/dist/skills/actors/references/runs.md +1 -1
  31. package/node_modules/@llblab/pi-actors/dist/skills/swarm/SKILL.md +1 -1
  32. package/node_modules/@llblab/pi-actors/docs/README.md +1 -0
  33. package/node_modules/@llblab/pi-actors/docs/async-runs.md +8 -4
  34. package/node_modules/@llblab/pi-actors/docs/coordinator-delivery.md +207 -0
  35. package/node_modules/@llblab/pi-actors/index.ts +4 -1
  36. package/node_modules/@llblab/pi-actors/lib/async-runs.ts +42 -21
  37. package/node_modules/@llblab/pi-actors/lib/extension-runtime.ts +8 -3
  38. package/node_modules/@llblab/pi-actors/lib/limits.ts +9 -0
  39. package/node_modules/@llblab/pi-actors/lib/observability.ts +97 -78
  40. package/node_modules/@llblab/pi-actors/lib/pi.ts +210 -0
  41. package/node_modules/@llblab/pi-actors/lib/run-delivery.ts +800 -0
  42. package/node_modules/@llblab/pi-actors/lib/run-ui-runtime.ts +370 -18
  43. package/node_modules/@llblab/pi-actors/lib/runs-trace.ts +6 -4
  44. package/node_modules/@llblab/pi-actors/lib/session-evidence.ts +153 -0
  45. package/node_modules/@llblab/pi-actors/lib/temp.ts +1 -1
  46. package/node_modules/@llblab/pi-actors/lib/tools-inspect.ts +4 -1
  47. package/node_modules/@llblab/pi-actors/package.json +3 -3
  48. package/node_modules/@llblab/pi-actors/scripts/async-runner.mjs +5 -19
  49. package/node_modules/@llblab/pi-actors/skills/actors/SKILL.md +2 -2
  50. package/node_modules/@llblab/pi-actors/skills/actors/references/runs.md +1 -1
  51. package/node_modules/@llblab/pi-actors/skills/swarm/SKILL.md +1 -1
  52. package/node_modules/@llblab/pi-telegram/AGENTS.md +4 -4
  53. package/node_modules/@llblab/pi-telegram/CHANGELOG.md +19 -0
  54. package/node_modules/@llblab/pi-telegram/README.md +1 -1
  55. package/node_modules/@llblab/pi-telegram/docs/activity.md +12 -5
  56. package/node_modules/@llblab/pi-telegram/docs/architecture.md +7 -6
  57. package/node_modules/@llblab/pi-telegram/docs/outbound.md +2 -2
  58. package/node_modules/@llblab/pi-telegram/docs/public-api.md +3 -4
  59. package/node_modules/@llblab/pi-telegram/docs/ui-style.md +4 -5
  60. package/node_modules/@llblab/pi-telegram/docs/voice.md +12 -2
  61. package/node_modules/@llblab/pi-telegram/index.ts +0 -1
  62. package/node_modules/@llblab/pi-telegram/lib/activity.ts +32 -5
  63. package/node_modules/@llblab/pi-telegram/lib/bindings.ts +59 -12
  64. package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +1 -1
  65. package/node_modules/@llblab/pi-telegram/lib/config.ts +25 -25
  66. package/node_modules/@llblab/pi-telegram/lib/inbound.ts +1 -1
  67. package/node_modules/@llblab/pi-telegram/lib/lifecycle.ts +50 -0
  68. package/node_modules/@llblab/pi-telegram/lib/menu-settings.ts +10 -73
  69. package/node_modules/@llblab/pi-telegram/lib/pi.ts +15 -0
  70. package/node_modules/@llblab/pi-telegram/lib/status.ts +11 -1
  71. package/node_modules/@llblab/pi-telegram/package.json +4 -4
  72. package/node_modules/@llblab/pi-telegram/skills/generated-control-surface/SKILL.md +5 -3
  73. package/package.json +3 -3
@@ -337,7 +337,6 @@ export interface TelegramAssistantOutputBindingRuntime<TTransportStamp> {
337
337
  export function createTelegramAssistantOutputBindingRuntime<
338
338
  TTransportStamp,
339
339
  >(deps: {
340
- isEnabled: () => boolean;
341
340
  authority: {
342
341
  getPreferredTarget: () =>
343
342
  | OutboundAttachments.TelegramQueuedOutboundAttachmentTurnView["target"]
@@ -364,7 +363,6 @@ export function createTelegramAssistantOutputBindingRuntime<
364
363
  deps.sender,
365
364
  );
366
365
  const runtime = Activity.createTelegramAssistantOutputRuntime({
367
- isEnabled: deps.isEnabled,
368
366
  ...authority,
369
367
  async send(event, authority, isAuthorityActive) {
370
368
  await deps.waitForActivityIdle?.();
@@ -1012,8 +1010,9 @@ export function registerTelegramLifecycleRuntimeHooks({
1012
1010
  const agentStartWithDedupReset = Lifecycle.createAgentStartDedupHook(
1013
1011
  agentLifecycleHooks.onAgentStart,
1014
1012
  );
1013
+ let uiPromptActive = false;
1015
1014
  const startAgentActivityTypingLoop = (ctx: Pi.ExtensionContext): boolean => {
1016
- if (!canSendAgentActivity(ctx)) return false;
1015
+ if (uiPromptActive || !canSendAgentActivity(ctx)) return false;
1017
1016
  const turn = activeTurnRuntime.get();
1018
1017
  const target = turn?.target ?? proactivePushTargetGetter();
1019
1018
  promptDispatchRuntime.startTypingLoop(ctx, turn?.chatId ?? target?.chatId, {
@@ -1022,12 +1021,15 @@ export function registerTelegramLifecycleRuntimeHooks({
1022
1021
  return true;
1023
1022
  };
1024
1023
  const startActiveTurnTypingLoop = (ctx: Pi.ExtensionContext): void => {
1024
+ if (uiPromptActive) return;
1025
1025
  const turn = activeTurnRuntime.get();
1026
1026
  promptDispatchRuntime.startTypingLoop(ctx, turn?.chatId, {
1027
1027
  target: turn?.target,
1028
1028
  });
1029
1029
  };
1030
1030
  let observedAutomaticCompaction = false;
1031
+ let agentWorkActive = false;
1032
+ let terminalAssistantMessagePendingDelivery = false;
1031
1033
  const deferredAutomaticCompactionNotices: string[] = [];
1032
1034
  const sendCompactionNotice = async (text: string): Promise<void> => {
1033
1035
  const turn = activeTurnRuntime.get();
@@ -1094,6 +1096,9 @@ export function registerTelegramLifecycleRuntimeHooks({
1094
1096
  activityVerbosityRuntime?.reset();
1095
1097
  assistantOutputRuntime.stop();
1096
1098
  observedAutomaticCompaction = false;
1099
+ agentWorkActive = false;
1100
+ terminalAssistantMessagePendingDelivery = false;
1101
+ uiPromptActive = false;
1097
1102
  deferredAutomaticCompactionNotices.length = 0;
1098
1103
  compactionObserver.onSessionShutdown();
1099
1104
  if (event.reason === "quit" && disconnectOnQuit) {
@@ -1116,7 +1121,7 @@ export function registerTelegramLifecycleRuntimeHooks({
1116
1121
  activityRuntime.onCompactionStart(Pi.getSessionCompactionReason(event));
1117
1122
  compactionObserver.onSessionBeforeCompact(event, ctx);
1118
1123
  if (shouldNotify) {
1119
- if (activeTurnRuntime.has()) {
1124
+ if (terminalAssistantMessagePendingDelivery) {
1120
1125
  deferredAutomaticCompactionNotices.push(
1121
1126
  Commands.TELEGRAM_COMPACTION_STARTED_MARKDOWN,
1122
1127
  );
@@ -1135,10 +1140,7 @@ export function registerTelegramLifecycleRuntimeHooks({
1135
1140
  compactionObserver.onSessionCompact(event, ctx);
1136
1141
  if (observedAutomaticCompaction) {
1137
1142
  observedAutomaticCompaction = false;
1138
- if (
1139
- activeTurnRuntime.has() ||
1140
- deferredAutomaticCompactionNotices.length > 0
1141
- ) {
1143
+ if (deferredAutomaticCompactionNotices.length > 0) {
1142
1144
  deferredAutomaticCompactionNotices.push(
1143
1145
  Commands.TELEGRAM_COMPACTION_COMPLETED_MARKDOWN,
1144
1146
  );
@@ -1149,8 +1151,27 @@ export function registerTelegramLifecycleRuntimeHooks({
1149
1151
  }
1150
1152
  }
1151
1153
  },
1154
+ async onSessionCompactFailed(event, ctx) {
1155
+ if (!isSessionContextActive(ctx)) return;
1156
+ const shouldNotify = observedAutomaticCompaction;
1157
+ const deferredNotices = deferredAutomaticCompactionNotices.splice(0);
1158
+ const shouldDefer =
1159
+ deferredNotices.length > 0 || terminalAssistantMessagePendingDelivery;
1160
+ compactionObserver.onSessionCompactFailed(event, ctx);
1161
+ if (!shouldNotify) return;
1162
+ const notice = event.aborted
1163
+ ? "**⚠️ Compaction cancelled.**"
1164
+ : "**⚠️ Compaction failed.**";
1165
+ if (shouldDefer) {
1166
+ deferredAutomaticCompactionNotices.push(...deferredNotices, notice);
1167
+ } else {
1168
+ await sendCompactionNotice(notice);
1169
+ }
1170
+ },
1152
1171
  async onAgentStart(event, ctx) {
1153
1172
  if (!isSessionContextActive(ctx)) return;
1173
+ agentWorkActive = true;
1174
+ terminalAssistantMessagePendingDelivery = false;
1154
1175
  await agentStartWithDedupReset(event, ctx);
1155
1176
  activityRuntime.onAgentStart(activeTurnRuntime.get()?.target);
1156
1177
  startAgentActivityTypingLoop(ctx);
@@ -1195,19 +1216,45 @@ export function registerTelegramLifecycleRuntimeHooks({
1195
1216
  }
1196
1217
  await messageActivityHooks.onMessageUpdate(event, ctx);
1197
1218
  },
1219
+ onMessageEnd(event, ctx) {
1220
+ if (!isSessionContextActive(ctx)) return;
1221
+ terminalAssistantMessagePendingDelivery =
1222
+ event.message.role === "assistant" &&
1223
+ event.message.stopReason !== "toolUse" &&
1224
+ event.message.stopReason !== "error" &&
1225
+ event.message.stopReason !== "aborted";
1226
+ },
1227
+ onUiPromptStart(event, ctx) {
1228
+ if (!isSessionContextActive(ctx)) return;
1229
+ uiPromptActive = true;
1230
+ typing.stop();
1231
+ activityRuntime.onUiPromptStart(event.kind, event.title);
1232
+ updateStatus(ctx);
1233
+ },
1234
+ onUiPromptEnd(_event, ctx) {
1235
+ if (!isSessionContextActive(ctx)) return;
1236
+ uiPromptActive = false;
1237
+ activityRuntime.onUiPromptEnd();
1238
+ if (agentWorkActive || lifecycle.isCompactionInProgress()) {
1239
+ startAgentActivityTypingLoop(ctx);
1240
+ }
1241
+ updateStatus(ctx);
1242
+ },
1198
1243
  async onAgentEnd(event, ctx) {
1199
1244
  if (!isSessionContextActive(ctx)) return;
1200
1245
  activityRuntime.onAgentEnd();
1201
1246
  await agentLifecycleHooks.onAgentEnd(event, ctx);
1247
+ },
1248
+ async onAgentSettled(event, ctx) {
1249
+ if (!isSessionContextActive(ctx)) return;
1250
+ await agentLifecycleHooks.onAgentSettled(event, ctx);
1202
1251
  if (deferredAutomaticCompactionNotices.length > 0) {
1203
1252
  await waitForActiveTurnDelivery();
1204
1253
  if (!isSessionContextActive(ctx)) return;
1205
1254
  await flushDeferredAutomaticCompactionNotices();
1206
1255
  }
1207
- },
1208
- async onAgentSettled(event, ctx) {
1209
- if (!isSessionContextActive(ctx)) return;
1210
- await agentLifecycleHooks.onAgentSettled(event, ctx);
1256
+ terminalAssistantMessagePendingDelivery = false;
1257
+ agentWorkActive = false;
1211
1258
  activityRuntime.onAgentSettled();
1212
1259
  modelContextAvailabilityRuntime.reconcile();
1213
1260
  },
@@ -335,7 +335,7 @@ export function createTelegramBusInstanceLifecycleAnnouncement(input: {
335
335
  }): TelegramBusFollowerLifecycleAnnouncement {
336
336
  return {
337
337
  target: { ...input.target },
338
- text: `📡 Instance <b>${formatTelegramBusInstanceLabel(input)}</b> ${input.state}.`,
338
+ text: `<b>📡 Instance <i>${formatTelegramBusInstanceLabel(input)}</i> ${input.state}.</b>`,
339
339
  parseMode: "HTML",
340
340
  };
341
341
  }
@@ -105,7 +105,6 @@ export interface TelegramConfig {
105
105
  assistant?: {
106
106
  draftPreviews?: boolean;
107
107
  rendering?: TelegramAssistantRenderingMode;
108
- proactivePush?: boolean;
109
108
  activity?: TelegramActivityVerbosity;
110
109
  timeInjection?: TelegramTimeMode;
111
110
  /** @deprecated use activity */
@@ -454,10 +453,32 @@ function omitTelegramRootProfileFields(config: TelegramConfig): TelegramConfig {
454
453
  return sharedConfig;
455
454
  }
456
455
 
456
+ function omitRetiredProactivePush(config: TelegramConfig): {
457
+ config: TelegramConfig;
458
+ changed: boolean;
459
+ } {
460
+ const assistant = config.assistant as
461
+ | (NonNullable<TelegramConfig["assistant"]> & { proactivePush?: unknown })
462
+ | undefined;
463
+ if (!assistant || !Object.hasOwn(assistant, "proactivePush")) {
464
+ return { config, changed: false };
465
+ }
466
+ const { proactivePush: _proactivePush, ...remainingAssistant } = assistant;
467
+ const next = { ...config };
468
+ if (Object.keys(remainingAssistant).length > 0) {
469
+ next.assistant = remainingAssistant;
470
+ } else {
471
+ delete next.assistant;
472
+ }
473
+ return { config: next, changed: true };
474
+ }
475
+
457
476
  export function normalizeTelegramDefaultProfileConfig(config: TelegramConfig): {
458
477
  config: TelegramConfig;
459
478
  changed: boolean;
460
479
  } {
480
+ const retiredProactivePush = omitRetiredProactivePush(config);
481
+ config = retiredProactivePush.config;
461
482
  const hasLegacyRootProfile = [
462
483
  "botToken",
463
484
  "botUsername",
@@ -465,7 +486,9 @@ export function normalizeTelegramDefaultProfileConfig(config: TelegramConfig): {
465
486
  "allowedUserId",
466
487
  "lastUpdateId",
467
488
  ].some((field) => Object.hasOwn(config, field));
468
- if (!hasLegacyRootProfile) return { config, changed: false };
489
+ if (!hasLegacyRootProfile) {
490
+ return { config, changed: retiredProactivePush.changed };
491
+ }
469
492
  const canonicalProfile = config.profiles?.[TELEGRAM_DEFAULT_PROFILE_NAME];
470
493
  const legacyToken = config.botToken?.trim();
471
494
  if (Object.hasOwn(config, "botToken") && !legacyToken) {
@@ -712,27 +735,6 @@ export function createTelegramConfigStore(
712
735
  };
713
736
  }
714
737
 
715
- export function createTelegramProactivePushChecker(
716
- configStore: Pick<TelegramConfigStore, "get">,
717
- ): () => boolean {
718
- return () => configStore.get().assistant?.proactivePush ?? true;
719
- }
720
-
721
- export function createTelegramProactivePushSetter(
722
- configStore: TelegramMutableConfigStore,
723
- ): (enabled: boolean) => Promise<void> {
724
- return async (enabled) => {
725
- await loadLatestTelegramConfig(configStore);
726
- const current = configStore.get();
727
- const config: TelegramConfig = {
728
- ...current,
729
- assistant: { ...current.assistant, proactivePush: enabled },
730
- };
731
- configStore.set(config);
732
- await configStore.persist(config);
733
- };
734
- }
735
-
736
738
  export function createTelegramDraftPreviewsChecker(
737
739
  configStore: Pick<TelegramConfigStore, "get">,
738
740
  ): () => boolean {
@@ -1019,8 +1021,6 @@ export function createTelegramConfigControls(
1019
1021
  configStore: TelegramMutableConfigStore,
1020
1022
  ) {
1021
1023
  return {
1022
- isProactivePushEnabled: createTelegramProactivePushChecker(configStore),
1023
- setProactivePushEnabled: createTelegramProactivePushSetter(configStore),
1024
1024
  areDraftPreviewsEnabled: createTelegramDraftPreviewsChecker(configStore),
1025
1025
  setDraftPreviewsEnabled: createTelegramDraftPreviewsSetter(configStore),
1026
1026
  getAssistantRenderingMode:
@@ -19,7 +19,7 @@ import { getTelegramVoiceTranscriptionProviders } from "./voice.ts";
19
19
 
20
20
  const DEFAULT_INBOUND_HANDLER_TIMEOUT_MS = 120_000;
21
21
  const INBOUND_HANDLER_REGISTRY_KEY = "__piTelegramInboundHandlers__";
22
- const MAX_INBOUND_HANDLER_OUTPUT_LENGTH = 12_000;
22
+ const MAX_INBOUND_HANDLER_OUTPUT_LENGTH = 24_000;
23
23
  const MAX_INBOUND_HANDLER_FAILURE_STREAM_LENGTH = 4_000;
24
24
 
25
25
  type TelegramInboundCommandTemplateConfig =
@@ -16,13 +16,17 @@ import type {
16
16
  ExtensionAPI,
17
17
  ExtensionContext,
18
18
  InputEvent,
19
+ MessageEndEvent,
19
20
  SessionBeforeCompactEvent,
20
21
  SessionCompactEvent,
22
+ SessionCompactFailedEvent,
21
23
  SessionShutdownEvent,
22
24
  SessionStartEvent,
23
25
  ToolExecutionEndEvent,
24
26
  ToolExecutionStartEvent,
25
27
  ToolExecutionUpdateEvent,
28
+ UIPromptEndEvent,
29
+ UIPromptStartEvent,
26
30
  } from "./pi.ts";
27
31
 
28
32
  let resetTransportReplyDedupFn: (() => void) | undefined;
@@ -78,6 +82,10 @@ export interface TelegramLifecycleRegistrationDeps {
78
82
  event: SessionCompactEvent,
79
83
  ctx: ExtensionContext,
80
84
  ) => Promise<void> | void;
85
+ onSessionCompactFailed?: (
86
+ event: SessionCompactFailedEvent,
87
+ ctx: ExtensionContext,
88
+ ) => Promise<void> | void;
81
89
  onBeforeAgentStart: (
82
90
  event: TelegramBeforeAgentStartEvent,
83
91
  ctx: ExtensionContext,
@@ -113,6 +121,18 @@ export interface TelegramLifecycleRegistrationDeps {
113
121
  },
114
122
  ctx: ExtensionContext,
115
123
  ) => Promise<void>;
124
+ onMessageEnd?: (
125
+ event: MessageEndEvent,
126
+ ctx: ExtensionContext,
127
+ ) => Promise<void> | void;
128
+ onUiPromptStart?: (
129
+ event: UIPromptStartEvent,
130
+ ctx: ExtensionContext,
131
+ ) => Promise<void> | void;
132
+ onUiPromptEnd?: (
133
+ event: UIPromptEndEvent,
134
+ ctx: ExtensionContext,
135
+ ) => Promise<void> | void;
116
136
  onAgentEnd: (event: AgentEndEvent, ctx: ExtensionContext) => Promise<void>;
117
137
  onAgentSettled?: (
118
138
  event: AgentSettledEvent,
@@ -416,6 +436,10 @@ export interface TelegramCompactionObserverRuntime<TContext> {
416
436
  ctx: TContext,
417
437
  ) => void;
418
438
  onSessionCompact: (event: SessionCompactEvent, ctx: TContext) => void;
439
+ onSessionCompactFailed: (
440
+ event: SessionCompactFailedEvent,
441
+ ctx: TContext,
442
+ ) => void;
419
443
  onSessionShutdown: () => void;
420
444
  }
421
445
 
@@ -471,6 +495,16 @@ export function createTelegramCompactionObserverRuntime<TContext>(
471
495
  deps.updateStatus(ctx);
472
496
  requestDispatch();
473
497
  },
498
+ onSessionCompactFailed: (_event, ctx) => {
499
+ clearFallbackTimer();
500
+ if (deps.isContextActive && !deps.isContextActive(ctx)) return;
501
+ deps.setCompactionInProgress(false);
502
+ if (typingStartedByObserver) deps.stopTypingLoop?.();
503
+ typingStartedByObserver = false;
504
+ deps.updateStatus(ctx);
505
+ deps.onCompactionAbandoned?.();
506
+ requestDispatch();
507
+ },
474
508
  onSessionShutdown: () => {
475
509
  clearFallbackTimer();
476
510
  if (typingStartedByObserver) deps.stopTypingLoop?.();
@@ -590,6 +624,10 @@ export function registerTelegramLifecycleHooks(
590
624
  if (!isActive(ctx)) return;
591
625
  await deps.onSessionCompact?.(event, ctx);
592
626
  });
627
+ pi.on("session_compact_failed", async (event, ctx) => {
628
+ if (!isActive(ctx)) return;
629
+ await deps.onSessionCompactFailed?.(event, ctx);
630
+ });
593
631
  // The Pi SDK still types this result as a string; compatible runtimes may
594
632
  // preserve ordered system prompt blocks through the same public hook.
595
633
  const registerBeforeAgentStart = pi.on.bind(pi) as unknown as (
@@ -630,6 +668,18 @@ export function registerTelegramLifecycleHooks(
630
668
  if (!isActive(ctx)) return;
631
669
  await deps.onMessageUpdate(event, ctx);
632
670
  });
671
+ pi.on("message_end", async (event, ctx) => {
672
+ if (!isActive(ctx)) return;
673
+ await deps.onMessageEnd?.(event, ctx);
674
+ });
675
+ pi.on("ui_prompt_start", async (event, ctx) => {
676
+ if (!isActive(ctx)) return;
677
+ await deps.onUiPromptStart?.(event, ctx);
678
+ });
679
+ pi.on("ui_prompt_end", async (event, ctx) => {
680
+ if (!isActive(ctx)) return;
681
+ await deps.onUiPromptEnd?.(event, ctx);
682
+ });
633
683
  pi.on("agent_end", async (event, ctx) => {
634
684
  if (!isActive(ctx)) return;
635
685
  await deps.onAgentEnd(event, ctx);
@@ -21,7 +21,6 @@ import type { TelegramVoiceReplyMode } from "./voice.ts";
21
21
  export type TelegramSettingsMenuReplyMarkup = TelegramInlineKeyboardMarkup;
22
22
 
23
23
  export interface TelegramSettingsStateDeps {
24
- isProactivePushEnabled: () => boolean;
25
24
  areDraftPreviewsEnabled: () => boolean;
26
25
  getAssistantRenderingMode: () => TelegramAssistantRenderingMode;
27
26
  getActivityVerbosity: () => TelegramActivityVerbosity;
@@ -32,7 +31,6 @@ export interface TelegramSettingsStateDeps {
32
31
  }
33
32
 
34
33
  export interface TelegramSettingsMutationDeps extends TelegramSettingsStateDeps {
35
- setProactivePushEnabled: (enabled: boolean) => Promise<void>;
36
34
  setDraftPreviewsEnabled: (enabled: boolean) => Promise<void>;
37
35
  setAssistantRenderingMode: (
38
36
  mode: TelegramAssistantRenderingMode,
@@ -139,7 +137,6 @@ export interface TelegramSettingsMenuRuntimeDeps<
139
137
  export const SETTINGS_MENU_TITLE = "<b>⚙️ Settings:</b>";
140
138
  export const AUTOMATIC_THREAD_CLEANUP_SETTINGS_TITLE =
141
139
  "<b>🧹 Thread cleanup:</b>";
142
- export const PROACTIVE_PUSH_SETTINGS_TITLE = "<b>📌 Proactive push:</b>";
143
140
  export const DRAFT_PREVIEWS_SETTINGS_TITLE = "<b>📝 Draft previews:</b>";
144
141
  export const ASSISTANT_RENDERING_SETTINGS_TITLE =
145
142
  "<b>🧾 Assistant rendering:</b>";
@@ -181,19 +178,6 @@ export function buildAutomaticThreadCleanupSettingsText(
181
178
  ].join("\n");
182
179
  }
183
180
 
184
- export function buildProactivePushSettingsText(
185
- proactivePushEnabled: boolean,
186
- ): string {
187
- return [
188
- `${PROACTIVE_PUSH_SETTINGS_TITLE} <code>${proactivePushEnabled ? "on" : "off"}</code>`,
189
- "",
190
- "Control whether public assistant output from local/autonomous work is projected to Telegram.",
191
- "",
192
- "<code>-</code> <code>on</code> (default): send each completed public block, including visible checkpoints and the final answer, while connected.",
193
- "<code>-</code> <code>off</code>: keep local/autonomous assistant blocks in Pi; Telegram-originated replies still use their normal delivery path.",
194
- ].join("\n");
195
- }
196
-
197
181
  export function buildDraftPreviewsSettingsText(enabled: boolean): string {
198
182
  return [
199
183
  `${DRAFT_PREVIEWS_SETTINGS_TITLE} <code>${enabled ? "on" : "off"}</code>`,
@@ -265,7 +249,6 @@ export function buildTimeInjectionModeSettingsText(
265
249
  }
266
250
 
267
251
  export function buildTelegramSettingsMenuReplyMarkup(
268
- proactivePushEnabled: boolean,
269
252
  draftPreviewsEnabled: boolean,
270
253
  assistantRenderingModeOrVoiceReplyMode:
271
254
  TelegramAssistantRenderingMode | TelegramVoiceReplyMode,
@@ -325,10 +308,6 @@ export function buildTelegramSettingsMenuReplyMarkup(
325
308
  text: `🔬 Activity: ${activityVerbosity}`,
326
309
  callback_data: "settings:open:activity-verbosity",
327
310
  },
328
- {
329
- text: `📌 Proactive push: ${proactivePushEnabled ? "on" : "off"}`,
330
- callback_data: "settings:open:proactive",
331
- },
332
311
  {
333
312
  text: `🕒 Time injection: ${getTelegramSettingsStateValueLabel(timeInjectionMode)}`,
334
313
  callback_data: "settings:open:time-injection",
@@ -362,7 +341,6 @@ export async function openTelegramSettingsMenu<
362
341
  state,
363
342
  buildTelegramSettingsMenuText(),
364
343
  buildTelegramSettingsMenuReplyMarkup(
365
- deps.isProactivePushEnabled(),
366
344
  deps.areDraftPreviewsEnabled(),
367
345
  deps.getAssistantRenderingMode(),
368
346
  deps.getVoiceReplyMode(),
@@ -399,26 +377,6 @@ export function buildAutomaticThreadCleanupSettingsReplyMarkup(
399
377
  };
400
378
  }
401
379
 
402
- export function buildProactivePushSettingsReplyMarkup(
403
- proactivePushEnabled: boolean,
404
- ): TelegramSettingsMenuReplyMarkup {
405
- return {
406
- inline_keyboard: [
407
- [{ text: "⬆️ Back", callback_data: "settings:list" }],
408
- [
409
- {
410
- text: proactivePushEnabled ? "🟢 On" : "⚫️ On",
411
- callback_data: "settings:set:proactive:on",
412
- },
413
- {
414
- text: proactivePushEnabled ? "⚫️ Off" : "🟡 Off",
415
- callback_data: "settings:set:proactive:off",
416
- },
417
- ],
418
- ],
419
- };
420
- }
421
-
422
380
  export function buildDraftPreviewsSettingsReplyMarkup(
423
381
  enabled: boolean,
424
382
  ): TelegramSettingsMenuReplyMarkup {
@@ -521,7 +479,6 @@ export async function updateTelegramSettingsMenuMessage(
521
479
  await deps.updateSettingsMessage(
522
480
  buildTelegramSettingsMenuText(),
523
481
  buildTelegramSettingsMenuReplyMarkup(
524
- deps.isProactivePushEnabled(),
525
482
  deps.areDraftPreviewsEnabled(),
526
483
  deps.getAssistantRenderingMode(),
527
484
  deps.getVoiceReplyMode(),
@@ -544,16 +501,6 @@ export async function updateAutomaticThreadCleanupSettingsMessage(
544
501
  );
545
502
  }
546
503
 
547
- export async function updateProactivePushSettingsMessage(
548
- deps: TelegramSettingsMenuCallbackDeps,
549
- ): Promise<void> {
550
- const proactivePushEnabled = deps.isProactivePushEnabled();
551
- await deps.updateSettingsMessage(
552
- buildProactivePushSettingsText(proactivePushEnabled),
553
- buildProactivePushSettingsReplyMarkup(proactivePushEnabled),
554
- );
555
- }
556
-
557
504
  export async function updateDraftPreviewsSettingsMessage(
558
505
  deps: TelegramSettingsMenuCallbackDeps,
559
506
  ): Promise<void> {
@@ -621,9 +568,16 @@ export async function handleTelegramSettingsMenuCallbackAction(
621
568
  await deps.answerCallbackQuery(callbackQueryId);
622
569
  return true;
623
570
  }
624
- if (data === "settings:open:proactive") {
625
- await updateProactivePushSettingsMessage(deps);
626
- await deps.answerCallbackQuery(callbackQueryId);
571
+ if (
572
+ data === "settings:open:proactive" ||
573
+ data === "settings:set:proactive:on" ||
574
+ data === "settings:set:proactive:off"
575
+ ) {
576
+ await updateTelegramSettingsMenuMessage(deps, deps.sectionRegistry);
577
+ await deps.answerCallbackQuery(
578
+ callbackQueryId,
579
+ "Public assistant output is always delivered while Telegram is connected.",
580
+ );
627
581
  return true;
628
582
  }
629
583
  if (
@@ -751,19 +705,6 @@ export async function handleTelegramSettingsMenuCallbackAction(
751
705
  );
752
706
  return true;
753
707
  }
754
- if (
755
- data === "settings:set:proactive:on" ||
756
- data === "settings:set:proactive:off"
757
- ) {
758
- const enabled = data.endsWith(":on");
759
- await deps.setProactivePushEnabled(enabled);
760
- await updateProactivePushSettingsMessage(deps);
761
- await deps.answerCallbackQuery(
762
- callbackQueryId,
763
- `Proactive push ${enabled ? "enabled" : "disabled"}`,
764
- );
765
- return true;
766
- }
767
708
  await deps.answerCallbackQuery(callbackQueryId);
768
709
  return true;
769
710
  }
@@ -781,7 +722,6 @@ export function createTelegramSettingsMenuRuntime<
781
722
  return openTelegramSettingsMenu(
782
723
  {
783
724
  getModelMenuState: () => deps.getModelMenuState(chatId, ctx),
784
- isProactivePushEnabled: deps.isProactivePushEnabled,
785
725
  areDraftPreviewsEnabled: deps.areDraftPreviewsEnabled,
786
726
  getAssistantRenderingMode: deps.getAssistantRenderingMode,
787
727
  getActivityVerbosity: deps.getActivityVerbosity,
@@ -805,7 +745,6 @@ export function createTelegramSettingsMenuRuntime<
805
745
  await deps.reloadConfig?.();
806
746
  return updateTelegramSettingsMenuMessage(
807
747
  {
808
- isProactivePushEnabled: deps.isProactivePushEnabled,
809
748
  areDraftPreviewsEnabled: deps.areDraftPreviewsEnabled,
810
749
  getAssistantRenderingMode: deps.getAssistantRenderingMode,
811
750
  getActivityVerbosity: deps.getActivityVerbosity,
@@ -849,7 +788,6 @@ export function createTelegramSettingsMenuRuntime<
849
788
  deps.storeModelMenuState(state);
850
789
  }
851
790
  return handleTelegramSettingsMenuCallbackAction(query.id, query.data, {
852
- isProactivePushEnabled: deps.isProactivePushEnabled,
853
791
  areDraftPreviewsEnabled: deps.areDraftPreviewsEnabled,
854
792
  getAssistantRenderingMode: deps.getAssistantRenderingMode,
855
793
  getActivityVerbosity: deps.getActivityVerbosity,
@@ -857,7 +795,6 @@ export function createTelegramSettingsMenuRuntime<
857
795
  isVoiceReplyModeConfigured: deps.isVoiceReplyModeConfigured,
858
796
  getTimeInjectionMode: deps.getTimeInjectionMode,
859
797
  isAutomaticThreadCleanupEnabled: deps.isAutomaticThreadCleanupEnabled,
860
- setProactivePushEnabled: deps.setProactivePushEnabled,
861
798
  setDraftPreviewsEnabled: deps.setDraftPreviewsEnabled,
862
799
  setAssistantRenderingMode: deps.setAssistantRenderingMode,
863
800
  setActivityVerbosity: deps.setActivityVerbosity,
@@ -14,11 +14,14 @@ import {
14
14
  type ExtensionCommandContext,
15
15
  type ExtensionContext,
16
16
  type InputEvent,
17
+ type MessageEndEvent,
17
18
  type SessionBeforeCompactEvent,
18
19
  type SessionCompactEvent,
19
20
  type SessionShutdownEvent,
20
21
  type SessionStartEvent,
21
22
  type SlashCommandInfo,
23
+ type UIPromptEndEvent,
24
+ type UIPromptStartEvent,
22
25
  SettingsManager,
23
26
  } from "@earendil-works/pi-coding-agent";
24
27
 
@@ -32,13 +35,25 @@ export type {
32
35
  ExtensionCommandContext,
33
36
  ExtensionContext,
34
37
  InputEvent,
38
+ MessageEndEvent,
35
39
  SessionBeforeCompactEvent,
36
40
  SessionCompactEvent,
37
41
  SessionShutdownEvent,
38
42
  SessionStartEvent,
39
43
  SlashCommandInfo,
44
+ UIPromptEndEvent,
45
+ UIPromptStartEvent,
40
46
  };
41
47
 
48
+ export interface SessionCompactFailedEvent {
49
+ type: "session_compact_failed";
50
+ reason: "manual" | "threshold" | "overflow";
51
+ errorMessage?: string;
52
+ aborted: boolean;
53
+ willRetry: boolean;
54
+ fromExtension: boolean;
55
+ }
56
+
42
57
  export interface ToolExecutionStartEvent {
43
58
  type: "tool_execution_start";
44
59
  toolCallId: string;
@@ -14,6 +14,7 @@ export interface TelegramUsageStats {
14
14
  totalCacheRead: number;
15
15
  totalCacheWrite: number;
16
16
  totalCost: number;
17
+ latestCacheHitRate?: number;
17
18
  }
18
19
 
19
20
  interface TelegramUsageMessage {
@@ -1473,6 +1474,9 @@ function collectUsageStats(ctx: TelegramStatusContext): TelegramUsageStats {
1473
1474
  stats.totalCacheRead += usage.cacheRead;
1474
1475
  stats.totalCacheWrite += usage.cacheWrite;
1475
1476
  stats.totalCost += usage.cost.total;
1477
+ const promptTokens = usage.input + usage.cacheRead + usage.cacheWrite;
1478
+ stats.latestCacheHitRate =
1479
+ promptTokens > 0 ? (usage.cacheRead / promptTokens) * 100 : undefined;
1476
1480
  }
1477
1481
  return stats;
1478
1482
  }
@@ -1494,6 +1498,12 @@ function buildUsageSummary(stats: TelegramUsageStats): string | undefined {
1494
1498
  tokenParts.push(`R${formatTokens(stats.totalCacheRead)}`);
1495
1499
  if (stats.totalCacheWrite)
1496
1500
  tokenParts.push(`W${formatTokens(stats.totalCacheWrite)}`);
1501
+ if (
1502
+ (stats.totalCacheRead > 0 || stats.totalCacheWrite > 0) &&
1503
+ stats.latestCacheHitRate !== undefined
1504
+ ) {
1505
+ tokenParts.push(`CH${stats.latestCacheHitRate.toFixed(1)}%`);
1506
+ }
1497
1507
  return tokenParts.length > 0 ? tokenParts.join(" ") : undefined;
1498
1508
  }
1499
1509
 
@@ -1549,7 +1559,7 @@ export function buildStatusHtml(
1549
1559
  const usageSummary = buildUsageSummary(stats);
1550
1560
  const costSummary = buildCostSummary(stats, usesSubscription);
1551
1561
  if (usageSummary) {
1552
- lines.push(buildStatusRow("Usage", usageSummary));
1562
+ lines.push(buildStatusRow("Tokens", usageSummary));
1553
1563
  }
1554
1564
  if (costSummary) {
1555
1565
  lines.push(buildStatusRow("Cost", costSummary));
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-telegram",
3
- "version": "0.39.5",
3
+ "version": "0.41.0",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -71,9 +71,9 @@
71
71
  "image": "https://raw.githubusercontent.com/llblab/pi-telegram/main/screenshot.png"
72
72
  },
73
73
  "peerDependencies": {
74
- "@earendil-works/pi-agent-core": ">=0.80.6",
75
- "@earendil-works/pi-ai": ">=0.80.6",
76
- "@earendil-works/pi-coding-agent": ">=0.80.6",
74
+ "@earendil-works/pi-agent-core": ">=0.84.4",
75
+ "@earendil-works/pi-ai": ">=0.84.4",
76
+ "@earendil-works/pi-coding-agent": ">=0.84.4",
77
77
  "@sinclair/typebox": "*"
78
78
  },
79
79
  "devDependencies": {