@llblab/pi-telegram 0.24.2 → 0.24.4
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.
- package/AGENTS.md +6 -6
- package/BACKLOG.md +12 -0
- package/CHANGELOG.md +12 -0
- package/README.md +8 -4
- package/docs/architecture.md +6 -6
- package/docs/multi-instance-bus.md +5 -5
- package/docs/public-api.md +7 -3
- package/index.ts +40 -4
- package/lib/bindings.ts +21 -10
- package/lib/bus-follower.ts +5 -1
- package/lib/config.ts +51 -0
- package/lib/locks.ts +7 -0
- package/lib/menu-settings.ts +94 -6
- package/lib/outbound-attachments.ts +10 -15
- package/lib/pi.ts +6 -0
- package/lib/prompts.ts +126 -8
- package/lib/queue.ts +27 -3
- package/lib/routing.ts +301 -256
- package/lib/runtime.ts +6 -1
- package/lib/telegram-api.ts +100 -1
- package/package.json +10 -7
- package/scripts/audit-dependencies.ts +78 -0
- package/scripts/dependency-audit-policy.ts +300 -0
package/lib/bus-follower.ts
CHANGED
|
@@ -841,7 +841,9 @@ export function createTelegramBusFollowerSessionRefreshHook<TContext>(
|
|
|
841
841
|
};
|
|
842
842
|
}
|
|
843
843
|
|
|
844
|
-
export function createTelegramBusFollowerRegistrationState(
|
|
844
|
+
export function createTelegramBusFollowerRegistrationState(
|
|
845
|
+
options: { onAvailabilityChanged?: () => void } = {},
|
|
846
|
+
): TelegramBusFollowerRegistrationState {
|
|
845
847
|
let registered = false;
|
|
846
848
|
let target: TelegramTarget | undefined;
|
|
847
849
|
let slot: string | undefined;
|
|
@@ -861,11 +863,13 @@ export function createTelegramBusFollowerRegistrationState(): TelegramBusFollowe
|
|
|
861
863
|
).sort();
|
|
862
864
|
},
|
|
863
865
|
setRegistered: (next, nextTarget, metadata) => {
|
|
866
|
+
const availabilityChanged = registered !== next;
|
|
864
867
|
registered = next;
|
|
865
868
|
target = next ? (nextTarget ? { ...nextTarget } : undefined) : undefined;
|
|
866
869
|
slot = next ? metadata?.slot : undefined;
|
|
867
870
|
threadName = next ? metadata?.threadName : undefined;
|
|
868
871
|
generation = next ? metadata?.generation : undefined;
|
|
872
|
+
if (availabilityChanged) options.onAvailabilityChanged?.();
|
|
869
873
|
},
|
|
870
874
|
};
|
|
871
875
|
}
|
package/lib/config.ts
CHANGED
|
@@ -87,6 +87,10 @@ export interface TelegramConfig {
|
|
|
87
87
|
sendTranscript?: boolean;
|
|
88
88
|
};
|
|
89
89
|
time?: TelegramTimeConfig;
|
|
90
|
+
threads?: {
|
|
91
|
+
/** Delete this instance's bound Telegram thread on graceful Pi quit. */
|
|
92
|
+
automaticCleanup?: boolean;
|
|
93
|
+
};
|
|
90
94
|
/** Canonical bot/session profiles, including profiles.default. */
|
|
91
95
|
profiles?: Record<string, TelegramBotProfile>;
|
|
92
96
|
}
|
|
@@ -140,6 +144,7 @@ export interface TelegramConfigStore {
|
|
|
140
144
|
getOutboundHandlers: () => TelegramOutboundHandlerConfig[] | undefined;
|
|
141
145
|
setAllowedUserId: (userId: number) => void;
|
|
142
146
|
load: () => Promise<void>;
|
|
147
|
+
didLastLoadRecoverInvalidConfig: () => boolean;
|
|
143
148
|
persist: (config?: TelegramConfig) => Promise<void>;
|
|
144
149
|
}
|
|
145
150
|
|
|
@@ -188,6 +193,7 @@ type TelegramMutableConfigStore = Pick<
|
|
|
188
193
|
"get" | "set" | "persist"
|
|
189
194
|
> & {
|
|
190
195
|
load?: () => Promise<void>;
|
|
196
|
+
didLastLoadRecoverInvalidConfig?: () => boolean;
|
|
191
197
|
};
|
|
192
198
|
|
|
193
199
|
function isEmptyTelegramConfig(config: TelegramConfig): boolean {
|
|
@@ -498,6 +504,7 @@ export function createTelegramConfigStore(
|
|
|
498
504
|
let mutationVersion = 0;
|
|
499
505
|
let persistQueue: Promise<void> = Promise.resolve();
|
|
500
506
|
let activeProfileName: string | undefined;
|
|
507
|
+
let lastLoadRecoveredInvalidConfig = false;
|
|
501
508
|
const agentDir = options.agentDir ?? resolveAgentDir();
|
|
502
509
|
const configPath = options.configPath ?? getConfigPath();
|
|
503
510
|
const getEffectiveConfig = () =>
|
|
@@ -556,8 +563,10 @@ export function createTelegramConfigStore(
|
|
|
556
563
|
setEffectiveConfig(nextConfig);
|
|
557
564
|
},
|
|
558
565
|
load: async () => {
|
|
566
|
+
lastLoadRecoveredInvalidConfig = false;
|
|
559
567
|
const loadedConfig = await readTelegramConfig(configPath, {
|
|
560
568
|
onInvalidConfig: (recovery) => {
|
|
569
|
+
lastLoadRecoveredInvalidConfig = true;
|
|
561
570
|
options.recordRuntimeEvent?.("config", recovery.error, {
|
|
562
571
|
phase: "load",
|
|
563
572
|
configPath: recovery.configPath,
|
|
@@ -593,6 +602,7 @@ export function createTelegramConfigStore(
|
|
|
593
602
|
persistedConfig = cloneTelegramConfig(config);
|
|
594
603
|
mutationVersion += 1;
|
|
595
604
|
},
|
|
605
|
+
didLastLoadRecoverInvalidConfig: () => lastLoadRecoveredInvalidConfig,
|
|
596
606
|
persist: (nextConfig = getEffectiveConfig()) => {
|
|
597
607
|
const profileName = activeProfileName;
|
|
598
608
|
const desiredConfig = storeTelegramEffectiveConfig(
|
|
@@ -862,6 +872,41 @@ export function createTelegramProactivePushTargetGetter(deps: {
|
|
|
862
872
|
};
|
|
863
873
|
}
|
|
864
874
|
|
|
875
|
+
export function createTelegramAutomaticThreadCleanupChecker(
|
|
876
|
+
configStore: Pick<TelegramConfigStore, "get">,
|
|
877
|
+
): () => boolean {
|
|
878
|
+
return () => configStore.get().threads?.automaticCleanup ?? true;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
export function createTelegramAutomaticThreadCleanupResolver(
|
|
882
|
+
configStore: TelegramMutableConfigStore,
|
|
883
|
+
): () => Promise<boolean> {
|
|
884
|
+
return async () => {
|
|
885
|
+
await loadLatestTelegramConfig(configStore);
|
|
886
|
+
if (configStore.didLastLoadRecoverInvalidConfig?.()) {
|
|
887
|
+
throw new Error(
|
|
888
|
+
"Automatic thread cleanup setting is unavailable after invalid Telegram config recovery.",
|
|
889
|
+
);
|
|
890
|
+
}
|
|
891
|
+
return createTelegramAutomaticThreadCleanupChecker(configStore)();
|
|
892
|
+
};
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
export function createTelegramAutomaticThreadCleanupSetter(
|
|
896
|
+
configStore: TelegramMutableConfigStore,
|
|
897
|
+
): (enabled: boolean) => Promise<void> {
|
|
898
|
+
return async (enabled) => {
|
|
899
|
+
await loadLatestTelegramConfig(configStore);
|
|
900
|
+
const current = configStore.get();
|
|
901
|
+
const config = {
|
|
902
|
+
...current,
|
|
903
|
+
threads: { ...current.threads, automaticCleanup: enabled },
|
|
904
|
+
};
|
|
905
|
+
configStore.set(config);
|
|
906
|
+
await configStore.persist(config);
|
|
907
|
+
};
|
|
908
|
+
}
|
|
909
|
+
|
|
865
910
|
export function createTelegramConfigControls(
|
|
866
911
|
configStore: TelegramMutableConfigStore,
|
|
867
912
|
) {
|
|
@@ -880,6 +925,12 @@ export function createTelegramConfigControls(
|
|
|
880
925
|
setVoiceReplyMode: createTelegramVoiceReplyModeSetter(configStore),
|
|
881
926
|
getTimeInjectionMode: createTelegramTimeInjectionModeGetter(configStore),
|
|
882
927
|
setTimeInjectionMode: createTelegramTimeInjectionModeSetter(configStore),
|
|
928
|
+
isAutomaticThreadCleanupEnabled:
|
|
929
|
+
createTelegramAutomaticThreadCleanupChecker(configStore),
|
|
930
|
+
resolveAutomaticThreadCleanupEnabled:
|
|
931
|
+
createTelegramAutomaticThreadCleanupResolver(configStore),
|
|
932
|
+
setAutomaticThreadCleanupEnabled:
|
|
933
|
+
createTelegramAutomaticThreadCleanupSetter(configStore),
|
|
883
934
|
};
|
|
884
935
|
}
|
|
885
936
|
|
package/lib/locks.ts
CHANGED
|
@@ -1139,6 +1139,7 @@ export interface TelegramLockedPollingRuntimeDeps<
|
|
|
1139
1139
|
owner: TelegramLockEntry,
|
|
1140
1140
|
) => boolean | undefined | Promise<boolean | undefined>;
|
|
1141
1141
|
stopFollowerRegistration?: () => void;
|
|
1142
|
+
onTransportAvailabilityChanged?: () => void;
|
|
1142
1143
|
updateStatus: (ctx: TContext) => void;
|
|
1143
1144
|
recordRuntimeEvent?: (
|
|
1144
1145
|
category: string,
|
|
@@ -1189,6 +1190,7 @@ export function createTelegramLockedPollingRuntime<
|
|
|
1189
1190
|
const stopAfterOwnershipLoss = () => {
|
|
1190
1191
|
if (ownershipStop) return;
|
|
1191
1192
|
stopOwnershipWatcher();
|
|
1193
|
+
deps.onTransportAvailabilityChanged?.();
|
|
1192
1194
|
ownershipStop = deps
|
|
1193
1195
|
.stopPolling()
|
|
1194
1196
|
.catch((error) =>
|
|
@@ -1242,12 +1244,14 @@ export function createTelegramLockedPollingRuntime<
|
|
|
1242
1244
|
});
|
|
1243
1245
|
}
|
|
1244
1246
|
deps.lock.release();
|
|
1247
|
+
deps.onTransportAvailabilityChanged?.();
|
|
1245
1248
|
throw error;
|
|
1246
1249
|
}
|
|
1247
1250
|
if (deps.lock.owns(ctx)) return true;
|
|
1248
1251
|
stopOwnershipWatcher();
|
|
1249
1252
|
if (ownershipStop) await ownershipStop;
|
|
1250
1253
|
await deps.stopPolling();
|
|
1254
|
+
deps.onTransportAvailabilityChanged?.();
|
|
1251
1255
|
return false;
|
|
1252
1256
|
};
|
|
1253
1257
|
const canStartPolling = (ctx: TContext): boolean =>
|
|
@@ -1337,6 +1341,7 @@ export function createTelegramLockedPollingRuntime<
|
|
|
1337
1341
|
message: "Telegram leadership changed during polling startup.",
|
|
1338
1342
|
};
|
|
1339
1343
|
}
|
|
1344
|
+
deps.onTransportAvailabilityChanged?.();
|
|
1340
1345
|
deps.updateStatus(ctx);
|
|
1341
1346
|
const staleSuffix = acquired.replacedStale ? " Replaced stale lock." : "";
|
|
1342
1347
|
return { ok: true, message: `Telegram bridge connected.${staleSuffix}` };
|
|
@@ -1344,6 +1349,7 @@ export function createTelegramLockedPollingRuntime<
|
|
|
1344
1349
|
stop: async () => {
|
|
1345
1350
|
await suspendPolling();
|
|
1346
1351
|
const state = deps.lock.release();
|
|
1352
|
+
deps.onTransportAvailabilityChanged?.();
|
|
1347
1353
|
if (state.kind === "active-elsewhere") {
|
|
1348
1354
|
return `Telegram bridge is active in another Pi instance (${formatTelegramLockEntry(state.lock)}).`;
|
|
1349
1355
|
}
|
|
@@ -1391,6 +1397,7 @@ export function createTelegramLockedPollingRuntime<
|
|
|
1391
1397
|
if (generation !== sessionAutoStartGeneration) return;
|
|
1392
1398
|
if (!(await runOwnedPollingStart(ctx, {}))) return;
|
|
1393
1399
|
if (generation !== sessionAutoStartGeneration) return;
|
|
1400
|
+
deps.onTransportAvailabilityChanged?.();
|
|
1394
1401
|
deps.updateStatus(ctx);
|
|
1395
1402
|
deps.recordRuntimeEvent?.("lock", "Telegram auto-start completed", {
|
|
1396
1403
|
phase: "auto-start-complete",
|
package/lib/menu-settings.ts
CHANGED
|
@@ -26,6 +26,7 @@ export interface TelegramSettingsStateDeps {
|
|
|
26
26
|
getTimeInjectionMode: () => TelegramTimeMode;
|
|
27
27
|
getVoiceReplyMode: () => TelegramVoiceReplyMode;
|
|
28
28
|
isVoiceReplyModeConfigured: () => boolean;
|
|
29
|
+
isAutomaticThreadCleanupEnabled: () => boolean;
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
export interface TelegramSettingsMutationDeps extends TelegramSettingsStateDeps {
|
|
@@ -38,6 +39,7 @@ export interface TelegramSettingsMutationDeps extends TelegramSettingsStateDeps
|
|
|
38
39
|
mode: TelegramVoiceReplyMode | undefined,
|
|
39
40
|
) => Promise<void>;
|
|
40
41
|
setTimeInjectionMode: (mode: TelegramTimeMode) => Promise<void>;
|
|
42
|
+
setAutomaticThreadCleanupEnabled: (enabled: boolean) => Promise<void>;
|
|
41
43
|
}
|
|
42
44
|
|
|
43
45
|
export interface TelegramSettingsMenuOpenDeps<
|
|
@@ -99,6 +101,7 @@ export interface TelegramSettingsMenuRuntimeDeps<
|
|
|
99
101
|
TContext,
|
|
100
102
|
TModel extends MenuModel = MenuModel,
|
|
101
103
|
> extends TelegramSettingsMutationDeps {
|
|
104
|
+
reloadConfig?: () => Promise<void>;
|
|
102
105
|
getModelMenuState: (
|
|
103
106
|
chatId: number,
|
|
104
107
|
ctx: TContext,
|
|
@@ -129,6 +132,8 @@ export interface TelegramSettingsMenuRuntimeDeps<
|
|
|
129
132
|
}
|
|
130
133
|
|
|
131
134
|
export const SETTINGS_MENU_TITLE = "<b>⚙️ Settings:</b>";
|
|
135
|
+
export const AUTOMATIC_THREAD_CLEANUP_SETTINGS_TITLE =
|
|
136
|
+
"<b>🧹 Automatic thread cleanup:</b>";
|
|
132
137
|
export const PROACTIVE_PUSH_SETTINGS_TITLE = "<b>📌 Proactive push:</b>";
|
|
133
138
|
export const DRAFT_PREVIEWS_SETTINGS_TITLE = "<b>📝 Draft previews:</b>";
|
|
134
139
|
export const ASSISTANT_RENDERING_SETTINGS_TITLE =
|
|
@@ -158,6 +163,19 @@ export function buildTelegramSettingsMenuText(): string {
|
|
|
158
163
|
return SETTINGS_MENU_TITLE;
|
|
159
164
|
}
|
|
160
165
|
|
|
166
|
+
export function buildAutomaticThreadCleanupSettingsText(
|
|
167
|
+
enabled: boolean,
|
|
168
|
+
): string {
|
|
169
|
+
return [
|
|
170
|
+
`${AUTOMATIC_THREAD_CLEANUP_SETTINGS_TITLE} <code>${enabled ? "on" : "off"}</code>`,
|
|
171
|
+
"",
|
|
172
|
+
"Delete this Pi instance's Telegram tab when Pi quits normally.",
|
|
173
|
+
"",
|
|
174
|
+
"<code>-</code> <code>on</code> (default): delete the bound thread and release Telegram authority on graceful quit.",
|
|
175
|
+
"<code>-</code> <code>off</code>: preserve the tab as a restart hint; manual /telegram-disconnect still confirms and deletes it.",
|
|
176
|
+
].join("\n");
|
|
177
|
+
}
|
|
178
|
+
|
|
161
179
|
export function buildProactivePushSettingsText(
|
|
162
180
|
proactivePushEnabled: boolean,
|
|
163
181
|
): string {
|
|
@@ -236,6 +254,7 @@ export function buildTelegramSettingsMenuReplyMarkup(
|
|
|
236
254
|
TelegramTimeMode | TelegramSectionRegistry,
|
|
237
255
|
sectionRegistryOrVoiceReplyModeConfigured?: TelegramSectionRegistry | boolean,
|
|
238
256
|
voiceReplyModeConfigured = true,
|
|
257
|
+
automaticThreadCleanupEnabled = true,
|
|
239
258
|
): TelegramSettingsMenuReplyMarkup {
|
|
240
259
|
const hasRenderingMode =
|
|
241
260
|
assistantRenderingModeOrVoiceReplyMode === "rich" ||
|
|
@@ -269,6 +288,12 @@ export function buildTelegramSettingsMenuReplyMarkup(
|
|
|
269
288
|
}
|
|
270
289
|
}
|
|
271
290
|
rows.push(
|
|
291
|
+
[
|
|
292
|
+
{
|
|
293
|
+
text: `🧹 Auto thread cleanup: ${automaticThreadCleanupEnabled ? "on" : "off"}`,
|
|
294
|
+
callback_data: "settings:open:automatic-thread-cleanup",
|
|
295
|
+
},
|
|
296
|
+
],
|
|
272
297
|
[
|
|
273
298
|
{
|
|
274
299
|
text: `👄 Voice reply: ${getTelegramSettingsStateValueLabel(
|
|
@@ -328,6 +353,7 @@ export async function openTelegramSettingsMenu<
|
|
|
328
353
|
deps.getTimeInjectionMode(),
|
|
329
354
|
sectionRegistry,
|
|
330
355
|
deps.isVoiceReplyModeConfigured(),
|
|
356
|
+
deps.isAutomaticThreadCleanupEnabled(),
|
|
331
357
|
),
|
|
332
358
|
);
|
|
333
359
|
if (messageId === undefined) return;
|
|
@@ -336,6 +362,26 @@ export async function openTelegramSettingsMenu<
|
|
|
336
362
|
deps.storeModelMenuState(state);
|
|
337
363
|
}
|
|
338
364
|
|
|
365
|
+
export function buildAutomaticThreadCleanupSettingsReplyMarkup(
|
|
366
|
+
enabled: boolean,
|
|
367
|
+
): TelegramSettingsMenuReplyMarkup {
|
|
368
|
+
return {
|
|
369
|
+
inline_keyboard: [
|
|
370
|
+
[{ text: "⬆️ Back", callback_data: "settings:list" }],
|
|
371
|
+
[
|
|
372
|
+
{
|
|
373
|
+
text: enabled ? "🟢 On" : "⚫️ On",
|
|
374
|
+
callback_data: "settings:set:automatic-thread-cleanup:on",
|
|
375
|
+
},
|
|
376
|
+
{
|
|
377
|
+
text: enabled ? "⚫️ Off" : "🟡 Off",
|
|
378
|
+
callback_data: "settings:set:automatic-thread-cleanup:off",
|
|
379
|
+
},
|
|
380
|
+
],
|
|
381
|
+
],
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
|
|
339
385
|
export function buildProactivePushSettingsReplyMarkup(
|
|
340
386
|
proactivePushEnabled: boolean,
|
|
341
387
|
): TelegramSettingsMenuReplyMarkup {
|
|
@@ -443,10 +489,21 @@ export async function updateTelegramSettingsMenuMessage(
|
|
|
443
489
|
deps.getTimeInjectionMode(),
|
|
444
490
|
sectionRegistry,
|
|
445
491
|
deps.isVoiceReplyModeConfigured(),
|
|
492
|
+
deps.isAutomaticThreadCleanupEnabled(),
|
|
446
493
|
),
|
|
447
494
|
);
|
|
448
495
|
}
|
|
449
496
|
|
|
497
|
+
export async function updateAutomaticThreadCleanupSettingsMessage(
|
|
498
|
+
deps: TelegramSettingsMenuCallbackDeps,
|
|
499
|
+
): Promise<void> {
|
|
500
|
+
const enabled = deps.isAutomaticThreadCleanupEnabled();
|
|
501
|
+
await deps.updateSettingsMessage(
|
|
502
|
+
buildAutomaticThreadCleanupSettingsText(enabled),
|
|
503
|
+
buildAutomaticThreadCleanupSettingsReplyMarkup(enabled),
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
|
|
450
507
|
export async function updateProactivePushSettingsMessage(
|
|
451
508
|
deps: TelegramSettingsMenuCallbackDeps,
|
|
452
509
|
): Promise<void> {
|
|
@@ -509,6 +566,11 @@ export async function handleTelegramSettingsMenuCallbackAction(
|
|
|
509
566
|
await deps.answerCallbackQuery(callbackQueryId);
|
|
510
567
|
return true;
|
|
511
568
|
}
|
|
569
|
+
if (data === "settings:open:automatic-thread-cleanup") {
|
|
570
|
+
await updateAutomaticThreadCleanupSettingsMessage(deps);
|
|
571
|
+
await deps.answerCallbackQuery(callbackQueryId);
|
|
572
|
+
return true;
|
|
573
|
+
}
|
|
512
574
|
if (data === "settings:open:proactive") {
|
|
513
575
|
await updateProactivePushSettingsMessage(deps);
|
|
514
576
|
await deps.answerCallbackQuery(callbackQueryId);
|
|
@@ -598,6 +660,19 @@ export async function handleTelegramSettingsMenuCallbackAction(
|
|
|
598
660
|
return true;
|
|
599
661
|
}
|
|
600
662
|
}
|
|
663
|
+
if (
|
|
664
|
+
data === "settings:set:automatic-thread-cleanup:on" ||
|
|
665
|
+
data === "settings:set:automatic-thread-cleanup:off"
|
|
666
|
+
) {
|
|
667
|
+
const enabled = data.endsWith(":on");
|
|
668
|
+
await deps.setAutomaticThreadCleanupEnabled(enabled);
|
|
669
|
+
await updateAutomaticThreadCleanupSettingsMessage(deps);
|
|
670
|
+
await deps.answerCallbackQuery(
|
|
671
|
+
callbackQueryId,
|
|
672
|
+
`Automatic thread cleanup ${enabled ? "enabled" : "disabled"}`,
|
|
673
|
+
);
|
|
674
|
+
return true;
|
|
675
|
+
}
|
|
601
676
|
if (
|
|
602
677
|
data === "settings:set:proactive:on" ||
|
|
603
678
|
data === "settings:set:proactive:off"
|
|
@@ -623,8 +698,9 @@ export function createTelegramSettingsMenuRuntime<
|
|
|
623
698
|
sectionRegistry?: TelegramSectionRegistry,
|
|
624
699
|
): TelegramSettingsMenuRuntime<TContext> {
|
|
625
700
|
return {
|
|
626
|
-
openSettingsMenu: (chatId, _replyToMessageId, ctx) =>
|
|
627
|
-
|
|
701
|
+
openSettingsMenu: async (chatId, _replyToMessageId, ctx) => {
|
|
702
|
+
await deps.reloadConfig?.();
|
|
703
|
+
return openTelegramSettingsMenu(
|
|
628
704
|
{
|
|
629
705
|
getModelMenuState: () => deps.getModelMenuState(chatId, ctx),
|
|
630
706
|
isProactivePushEnabled: deps.isProactivePushEnabled,
|
|
@@ -633,6 +709,8 @@ export function createTelegramSettingsMenuRuntime<
|
|
|
633
709
|
getVoiceReplyMode: deps.getVoiceReplyMode,
|
|
634
710
|
isVoiceReplyModeConfigured: deps.isVoiceReplyModeConfigured,
|
|
635
711
|
getTimeInjectionMode: deps.getTimeInjectionMode,
|
|
712
|
+
isAutomaticThreadCleanupEnabled:
|
|
713
|
+
deps.isAutomaticThreadCleanupEnabled,
|
|
636
714
|
sendSettingsMenu: (state, text, replyMarkup) =>
|
|
637
715
|
deps.sendInteractiveMessage(
|
|
638
716
|
state.chatId,
|
|
@@ -643,9 +721,11 @@ export function createTelegramSettingsMenuRuntime<
|
|
|
643
721
|
storeModelMenuState: deps.storeModelMenuState,
|
|
644
722
|
},
|
|
645
723
|
sectionRegistry,
|
|
646
|
-
)
|
|
647
|
-
|
|
648
|
-
|
|
724
|
+
);
|
|
725
|
+
},
|
|
726
|
+
updateSettingsMenuMessage: async (state) => {
|
|
727
|
+
await deps.reloadConfig?.();
|
|
728
|
+
return updateTelegramSettingsMenuMessage(
|
|
649
729
|
{
|
|
650
730
|
isProactivePushEnabled: deps.isProactivePushEnabled,
|
|
651
731
|
areDraftPreviewsEnabled: deps.areDraftPreviewsEnabled,
|
|
@@ -653,6 +733,8 @@ export function createTelegramSettingsMenuRuntime<
|
|
|
653
733
|
getVoiceReplyMode: deps.getVoiceReplyMode,
|
|
654
734
|
isVoiceReplyModeConfigured: deps.isVoiceReplyModeConfigured,
|
|
655
735
|
getTimeInjectionMode: deps.getTimeInjectionMode,
|
|
736
|
+
isAutomaticThreadCleanupEnabled:
|
|
737
|
+
deps.isAutomaticThreadCleanupEnabled,
|
|
656
738
|
updateSettingsMessage: (text, replyMarkup) =>
|
|
657
739
|
deps.editInteractiveMessage(
|
|
658
740
|
state.chatId,
|
|
@@ -663,9 +745,11 @@ export function createTelegramSettingsMenuRuntime<
|
|
|
663
745
|
),
|
|
664
746
|
},
|
|
665
747
|
sectionRegistry,
|
|
666
|
-
)
|
|
748
|
+
);
|
|
749
|
+
},
|
|
667
750
|
handleCallbackQuery: async (query, ctx) => {
|
|
668
751
|
if (!query.data?.startsWith("settings:")) return false;
|
|
752
|
+
await deps.reloadConfig?.();
|
|
669
753
|
const messageId = query.message?.message_id;
|
|
670
754
|
const chatId = query.message?.chat?.id;
|
|
671
755
|
let state = deps.getStoredModelMenuState(messageId, chatId);
|
|
@@ -693,11 +777,15 @@ export function createTelegramSettingsMenuRuntime<
|
|
|
693
777
|
getVoiceReplyMode: deps.getVoiceReplyMode,
|
|
694
778
|
isVoiceReplyModeConfigured: deps.isVoiceReplyModeConfigured,
|
|
695
779
|
getTimeInjectionMode: deps.getTimeInjectionMode,
|
|
780
|
+
isAutomaticThreadCleanupEnabled:
|
|
781
|
+
deps.isAutomaticThreadCleanupEnabled,
|
|
696
782
|
setProactivePushEnabled: deps.setProactivePushEnabled,
|
|
697
783
|
setDraftPreviewsEnabled: deps.setDraftPreviewsEnabled,
|
|
698
784
|
setAssistantRenderingMode: deps.setAssistantRenderingMode,
|
|
699
785
|
setVoiceReplyMode: deps.setVoiceReplyMode,
|
|
700
786
|
setTimeInjectionMode: deps.setTimeInjectionMode,
|
|
787
|
+
setAutomaticThreadCleanupEnabled:
|
|
788
|
+
deps.setAutomaticThreadCleanupEnabled,
|
|
701
789
|
updateSettingsMessage: (text, replyMarkup) =>
|
|
702
790
|
deps.editInteractiveMessage(
|
|
703
791
|
state.chatId,
|
|
@@ -10,6 +10,12 @@ import { basename } from "node:path";
|
|
|
10
10
|
import { Type } from "@sinclair/typebox";
|
|
11
11
|
|
|
12
12
|
import type { ExtensionAPI } from "./pi.ts";
|
|
13
|
+
import {
|
|
14
|
+
TELEGRAM_ATTACH_PROMPT_GUIDELINES,
|
|
15
|
+
TELEGRAM_ATTACH_PROMPT_SNIPPET,
|
|
16
|
+
TELEGRAM_MESSAGE_PROMPT_GUIDELINES,
|
|
17
|
+
TELEGRAM_MESSAGE_PROMPT_SNIPPET,
|
|
18
|
+
} from "./prompts.ts";
|
|
13
19
|
import {
|
|
14
20
|
buildTelegramMultipartReplyParameters,
|
|
15
21
|
normalizeTelegramNativeMarkdown,
|
|
@@ -432,13 +438,8 @@ export function registerTelegramOutboundAttachmentTool(
|
|
|
432
438
|
label: "Telegram Attach",
|
|
433
439
|
description:
|
|
434
440
|
"Queue one or more local files for the active Telegram reply, or send them immediately to Telegram when no Telegram turn is active.",
|
|
435
|
-
promptSnippet:
|
|
436
|
-
|
|
437
|
-
promptGuidelines: [
|
|
438
|
-
"When handling a [telegram] message and the user asked for a file or generated artifact, call telegram_attach with the local path instead of only mentioning the path in text.",
|
|
439
|
-
"When a local/TUI user explicitly asks to send a generated file to Telegram, telegram_attach can deliver it to the paired/default Telegram chat even without an active Telegram turn.",
|
|
440
|
-
"For an explicit thread target, provide chat_id plus thread_id; registered multi-instance followers default to their assigned thread target.",
|
|
441
|
-
],
|
|
441
|
+
promptSnippet: TELEGRAM_ATTACH_PROMPT_SNIPPET,
|
|
442
|
+
promptGuidelines: [...TELEGRAM_ATTACH_PROMPT_GUIDELINES],
|
|
442
443
|
parameters: Type.Object({
|
|
443
444
|
paths: Type.Array(
|
|
444
445
|
Type.String({ description: "Local file path to attach" }),
|
|
@@ -499,14 +500,8 @@ export function registerTelegramOutboundMessageTool(
|
|
|
499
500
|
label: "Telegram Message",
|
|
500
501
|
description:
|
|
501
502
|
"Send a Markdown text message directly to the paired/default Telegram chat or an explicit chat_id. Hidden telegram_button comments in the text become attached inline prompt buttons.",
|
|
502
|
-
promptSnippet:
|
|
503
|
-
|
|
504
|
-
promptGuidelines: [
|
|
505
|
-
"Use telegram_message only when the user explicitly asks to send a message to Telegram from the local/TUI side, or names a concrete Telegram delivery target.",
|
|
506
|
-
"For an explicit thread target, provide chat_id plus thread_id; registered multi-instance followers default to their assigned thread target.",
|
|
507
|
-
"Add buttons by embedding the same top-level telegram_button HTML comments used in normal Telegram replies; Telegram does not support standalone buttons.",
|
|
508
|
-
"Do not use this tool for ordinary Telegram-originated replies; answer normally so the bridge can deliver the active turn reply.",
|
|
509
|
-
],
|
|
503
|
+
promptSnippet: TELEGRAM_MESSAGE_PROMPT_SNIPPET,
|
|
504
|
+
promptGuidelines: [...TELEGRAM_MESSAGE_PROMPT_GUIDELINES],
|
|
510
505
|
parameters: Type.Object({
|
|
511
506
|
text: Type.String({ description: "Message text to send" }),
|
|
512
507
|
chat_id: Type.Optional(
|
package/lib/pi.ts
CHANGED
|
@@ -120,6 +120,8 @@ export interface PiExtensionApiRuntimePorts {
|
|
|
120
120
|
getCommands: ExtensionAPI["getCommands"];
|
|
121
121
|
getThinkingLevel: ExtensionAPI["getThinkingLevel"];
|
|
122
122
|
setThinkingLevel: ExtensionAPI["setThinkingLevel"];
|
|
123
|
+
getActiveTools: ExtensionAPI["getActiveTools"];
|
|
124
|
+
setActiveTools: ExtensionAPI["setActiveTools"];
|
|
123
125
|
setModel: ExtensionAPI["setModel"];
|
|
124
126
|
}
|
|
125
127
|
|
|
@@ -131,6 +133,8 @@ export function createExtensionApiRuntimePorts(
|
|
|
131
133
|
| "getCommands"
|
|
132
134
|
| "getThinkingLevel"
|
|
133
135
|
| "setThinkingLevel"
|
|
136
|
+
| "getActiveTools"
|
|
137
|
+
| "setActiveTools"
|
|
134
138
|
| "setModel"
|
|
135
139
|
>,
|
|
136
140
|
): PiExtensionApiRuntimePorts {
|
|
@@ -141,6 +145,8 @@ export function createExtensionApiRuntimePorts(
|
|
|
141
145
|
getCommands: () => api.getCommands(),
|
|
142
146
|
getThinkingLevel: () => api.getThinkingLevel(),
|
|
143
147
|
setThinkingLevel: (level) => api.setThinkingLevel(level),
|
|
148
|
+
getActiveTools: () => api.getActiveTools(),
|
|
149
|
+
setActiveTools: (names) => api.setActiveTools(names),
|
|
144
150
|
setModel: (model) => api.setModel(model),
|
|
145
151
|
};
|
|
146
152
|
}
|
package/lib/prompts.ts
CHANGED
|
@@ -18,6 +18,106 @@ const TELEGRAM_TURN_SYSTEM_PROMPT_SUFFIX = `
|
|
|
18
18
|
|
|
19
19
|
Telegram turn note: Call \`telegram_help\` if you need the pi-telegram bridge action contract.`;
|
|
20
20
|
|
|
21
|
+
export const TELEGRAM_ATTACH_PROMPT_SNIPPET =
|
|
22
|
+
"Queue files for the active Telegram reply; outside Telegram turns, send files directly to Telegram.";
|
|
23
|
+
export const TELEGRAM_ATTACH_PROMPT_GUIDELINES = [
|
|
24
|
+
"When handling a [telegram] message and the user asked for a file or generated artifact, call telegram_attach with the local path instead of only mentioning the path in text.",
|
|
25
|
+
"When a local/TUI user explicitly asks to send a generated file to Telegram, telegram_attach can deliver it to the paired/default Telegram chat even without an active Telegram turn.",
|
|
26
|
+
"For an explicit thread target, provide chat_id plus thread_id; registered multi-instance followers default to their assigned thread target.",
|
|
27
|
+
] as const;
|
|
28
|
+
export const TELEGRAM_MESSAGE_PROMPT_SNIPPET =
|
|
29
|
+
"Send direct Telegram Markdown text when the user explicitly asks for Telegram delivery outside the normal reply flow.";
|
|
30
|
+
export const TELEGRAM_MESSAGE_PROMPT_GUIDELINES = [
|
|
31
|
+
"Use telegram_message only when the user explicitly asks to send a message to Telegram from the local/TUI side, or names a concrete Telegram delivery target.",
|
|
32
|
+
"For an explicit thread target, provide chat_id plus thread_id; registered multi-instance followers default to their assigned thread target.",
|
|
33
|
+
"Add buttons by embedding the same top-level telegram_button HTML comments used in normal Telegram replies; Telegram does not support standalone buttons.",
|
|
34
|
+
"Do not use this tool for ordinary Telegram-originated replies; answer normally so the bridge can deliver the active turn reply.",
|
|
35
|
+
] as const;
|
|
36
|
+
|
|
37
|
+
const TELEGRAM_MODEL_CONTEXT_TOOL_NAMES = new Set([
|
|
38
|
+
"telegram_attach",
|
|
39
|
+
"telegram_message",
|
|
40
|
+
"telegram_help",
|
|
41
|
+
]);
|
|
42
|
+
const TELEGRAM_MODEL_CONTEXT_MEMORY_KEY = Symbol.for(
|
|
43
|
+
"@llblab/pi-telegram:model-context-suspended-tools",
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
export interface TelegramModelContextAvailabilityMemory {
|
|
47
|
+
suspended: boolean;
|
|
48
|
+
toolNames: Set<string>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function getTelegramModelContextAvailabilityMemory(): TelegramModelContextAvailabilityMemory {
|
|
52
|
+
const globals = globalThis as unknown as Record<symbol, unknown>;
|
|
53
|
+
const existing = globals[TELEGRAM_MODEL_CONTEXT_MEMORY_KEY];
|
|
54
|
+
if (
|
|
55
|
+
existing &&
|
|
56
|
+
typeof existing === "object" &&
|
|
57
|
+
"toolNames" in existing &&
|
|
58
|
+
(existing as { toolNames?: unknown }).toolNames instanceof Set
|
|
59
|
+
) {
|
|
60
|
+
return existing as TelegramModelContextAvailabilityMemory;
|
|
61
|
+
}
|
|
62
|
+
const memory: TelegramModelContextAvailabilityMemory = {
|
|
63
|
+
suspended: false,
|
|
64
|
+
toolNames: new Set<string>(),
|
|
65
|
+
};
|
|
66
|
+
globals[TELEGRAM_MODEL_CONTEXT_MEMORY_KEY] = memory;
|
|
67
|
+
return memory;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface TelegramModelContextAvailabilityRuntime {
|
|
71
|
+
reconcile: () => void;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function createTelegramModelContextAvailabilityRuntime(deps: {
|
|
75
|
+
getActiveTools: () => string[];
|
|
76
|
+
setActiveTools: (names: string[]) => void;
|
|
77
|
+
isAvailable: () => boolean;
|
|
78
|
+
canReconcile?: () => boolean;
|
|
79
|
+
memory?: TelegramModelContextAvailabilityMemory;
|
|
80
|
+
}): TelegramModelContextAvailabilityRuntime {
|
|
81
|
+
const memory =
|
|
82
|
+
deps.memory ?? getTelegramModelContextAvailabilityMemory();
|
|
83
|
+
return {
|
|
84
|
+
reconcile() {
|
|
85
|
+
if (deps.canReconcile && !deps.canReconcile()) return;
|
|
86
|
+
const activeTools = deps.getActiveTools();
|
|
87
|
+
if (!deps.isAvailable()) {
|
|
88
|
+
if (!memory.suspended) {
|
|
89
|
+
memory.toolNames.clear();
|
|
90
|
+
for (const name of activeTools) {
|
|
91
|
+
if (TELEGRAM_MODEL_CONTEXT_TOOL_NAMES.has(name)) {
|
|
92
|
+
memory.toolNames.add(name);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
memory.suspended = true;
|
|
96
|
+
}
|
|
97
|
+
const nextTools = activeTools.filter(
|
|
98
|
+
(name) => !TELEGRAM_MODEL_CONTEXT_TOOL_NAMES.has(name),
|
|
99
|
+
);
|
|
100
|
+
if (nextTools.length !== activeTools.length) {
|
|
101
|
+
deps.setActiveTools(nextTools);
|
|
102
|
+
}
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (!memory.suspended) return;
|
|
106
|
+
const nextTools = [...activeTools];
|
|
107
|
+
for (const name of TELEGRAM_MODEL_CONTEXT_TOOL_NAMES) {
|
|
108
|
+
if (memory.toolNames.has(name) && !nextTools.includes(name)) {
|
|
109
|
+
nextTools.push(name);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
memory.toolNames.clear();
|
|
113
|
+
memory.suspended = false;
|
|
114
|
+
if (nextTools.length !== activeTools.length) {
|
|
115
|
+
deps.setActiveTools(nextTools);
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
21
121
|
function buildTelegramHelpText(profileName?: string): string {
|
|
22
122
|
const diagnosticsPaths = getTelegramDiagnosticsDisplayPaths(profileName);
|
|
23
123
|
return `--- TELEGRAM BRIDGE HELP ---
|
|
@@ -143,11 +243,25 @@ export function createTelegramBeforeAgentStartHook(
|
|
|
143
243
|
});
|
|
144
244
|
}
|
|
145
245
|
|
|
246
|
+
function stripTelegramToolMetadataFromSystemPrompt(
|
|
247
|
+
systemPrompt: string,
|
|
248
|
+
): string {
|
|
249
|
+
const telegramLines = new Set([
|
|
250
|
+
`- telegram_attach: ${TELEGRAM_ATTACH_PROMPT_SNIPPET}`,
|
|
251
|
+
`- telegram_message: ${TELEGRAM_MESSAGE_PROMPT_SNIPPET}`,
|
|
252
|
+
...TELEGRAM_ATTACH_PROMPT_GUIDELINES.map((line) => `- ${line}`),
|
|
253
|
+
...TELEGRAM_MESSAGE_PROMPT_GUIDELINES.map((line) => `- ${line}`),
|
|
254
|
+
]);
|
|
255
|
+
return systemPrompt
|
|
256
|
+
.split("\n")
|
|
257
|
+
.filter((line) => !telegramLines.has(line))
|
|
258
|
+
.join("\n");
|
|
259
|
+
}
|
|
260
|
+
|
|
146
261
|
export interface TelegramProactivePromptHookDeps<TContext> {
|
|
147
262
|
baseHook?: (event: BeforeAgentStartEvent) => { systemPrompt: string };
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
isCurrentOwner: (ctx: TContext) => boolean;
|
|
263
|
+
reconcileAvailability?: () => void;
|
|
264
|
+
isAvailable: (ctx: TContext) => boolean;
|
|
151
265
|
}
|
|
152
266
|
|
|
153
267
|
export function createTelegramProactiveBeforeAgentStartHook<TContext>(
|
|
@@ -158,10 +272,14 @@ export function createTelegramProactiveBeforeAgentStartHook<TContext>(
|
|
|
158
272
|
) => Promise<{ systemPrompt: string }> {
|
|
159
273
|
const baseHook = deps.baseHook ?? createTelegramBeforeAgentStartHook();
|
|
160
274
|
return async (event, ctx) => {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
275
|
+
deps.reconcileAvailability?.();
|
|
276
|
+
if (!deps.isAvailable(ctx)) {
|
|
277
|
+
return {
|
|
278
|
+
systemPrompt: stripTelegramToolMetadataFromSystemPrompt(
|
|
279
|
+
event.systemPrompt,
|
|
280
|
+
),
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
return baseHook(event);
|
|
166
284
|
};
|
|
167
285
|
}
|