@llblab/pi-telegram 0.11.0 → 0.11.2

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/lib/config.ts CHANGED
@@ -35,6 +35,19 @@ export interface TelegramOutboundHandlerConfig extends CommandTemplateObjectConf
35
35
  timeout?: number;
36
36
  }
37
37
 
38
+ export type TelegramTimeMode = "hidden" | "always" | "interval";
39
+
40
+ export interface TelegramTimeConfig {
41
+ injectionMode?: TelegramTimeMode;
42
+ interval?: number;
43
+ }
44
+
45
+ export interface ResolvedTelegramTimeConfig {
46
+ injectionMode: TelegramTimeMode;
47
+ interval: number;
48
+ timezone: string;
49
+ }
50
+
38
51
  export interface TelegramConfig {
39
52
  botToken?: string;
40
53
  botUsername?: string;
@@ -50,6 +63,7 @@ export interface TelegramConfig {
50
63
  /** Whether to attach the provider's transcriptText as caption on voice messages */
51
64
  sendTranscript?: boolean;
52
65
  };
66
+ time?: TelegramTimeConfig;
53
67
  }
54
68
 
55
69
  export interface TelegramConfigStore {
@@ -211,6 +225,56 @@ export function createTelegramVoiceReplyModeSetter(
211
225
  };
212
226
  }
213
227
 
228
+ function getSystemTimezone(): string {
229
+ try {
230
+ const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
231
+ return tz && tz.length > 0 ? tz : "UTC";
232
+ } catch {
233
+ return "UTC";
234
+ }
235
+ }
236
+
237
+ export function resolveTelegramTimeConfig(
238
+ raw: TelegramTimeConfig | undefined,
239
+ ): ResolvedTelegramTimeConfig {
240
+ const injectionMode: TelegramTimeMode =
241
+ raw?.injectionMode === "always" || raw?.injectionMode === "interval"
242
+ ? raw.injectionMode
243
+ : "hidden";
244
+ const interval =
245
+ typeof raw?.interval === "number" && raw.interval > 0
246
+ ? raw.interval
247
+ : 60 * 60 * 1000;
248
+ const timezone = getSystemTimezone();
249
+ return { injectionMode, interval, timezone };
250
+ }
251
+
252
+ export function createTelegramTimeConfigGetter(
253
+ configStore: Pick<TelegramConfigStore, "get">,
254
+ ): () => ResolvedTelegramTimeConfig {
255
+ return () => resolveTelegramTimeConfig(configStore.get().time);
256
+ }
257
+
258
+ export function createTelegramTimeInjectionModeGetter(
259
+ configStore: Pick<TelegramConfigStore, "get">,
260
+ ): () => TelegramTimeMode {
261
+ return () => resolveTelegramTimeConfig(configStore.get().time).injectionMode;
262
+ }
263
+
264
+ export function createTelegramTimeInjectionModeSetter(
265
+ configStore: Pick<TelegramConfigStore, "get" | "set" | "persist">,
266
+ ): (injectionMode: TelegramTimeMode) => Promise<void> {
267
+ return async (injectionMode) => {
268
+ const current = configStore.get();
269
+ const next = {
270
+ ...current,
271
+ time: { ...(current.time ?? {}), injectionMode },
272
+ };
273
+ configStore.set(next);
274
+ await configStore.persist(next);
275
+ };
276
+ }
277
+
214
278
  export function createTelegramProactivePushChatIdGetter(deps: {
215
279
  getActiveTurnChatId: () => number | undefined;
216
280
  getAllowedUserId: () => number | undefined;
package/lib/lifecycle.ts CHANGED
@@ -10,6 +10,8 @@ import type {
10
10
  BeforeAgentStartEvent,
11
11
  ExtensionAPI,
12
12
  ExtensionContext,
13
+ SessionBeforeCompactEvent,
14
+ SessionCompactEvent,
13
15
  SessionShutdownEvent,
14
16
  SessionStartEvent,
15
17
  } from "./pi.ts";
@@ -50,6 +52,14 @@ export interface TelegramLifecycleRegistrationDeps {
50
52
  event: SessionShutdownEvent,
51
53
  ctx: ExtensionContext,
52
54
  ) => Promise<void>;
55
+ onSessionBeforeCompact?: (
56
+ event: SessionBeforeCompactEvent,
57
+ ctx: ExtensionContext,
58
+ ) => Promise<void> | void;
59
+ onSessionCompact?: (
60
+ event: SessionCompactEvent,
61
+ ctx: ExtensionContext,
62
+ ) => Promise<void> | void;
53
63
  onBeforeAgentStart: (
54
64
  event: BeforeAgentStartEvent,
55
65
  ctx: ExtensionContext,
@@ -92,6 +102,76 @@ export interface TelegramSessionLifecycleHooks {
92
102
  ) => Promise<void>;
93
103
  }
94
104
 
105
+ type TelegramLifecycleTimer = number | ReturnType<typeof setTimeout>;
106
+
107
+ export interface TelegramCompactionObserverRuntimeDeps<TContext> {
108
+ setCompactionInProgress: (inProgress: boolean) => void;
109
+ updateStatus: (ctx: TContext) => void;
110
+ requestDeferredDispatchNextQueuedTelegramTurn: (
111
+ dispatch: (ctx: TContext) => void,
112
+ ) => void;
113
+ dispatchNextQueuedTelegramTurn: (ctx: TContext) => void;
114
+ recordRuntimeEvent?: (category: string, error: unknown) => void;
115
+ timeoutMs?: number;
116
+ setTimer?: (
117
+ callback: () => void,
118
+ ms: number,
119
+ ) => TelegramLifecycleTimer;
120
+ clearTimer?: (timer: TelegramLifecycleTimer) => void;
121
+ }
122
+
123
+ export interface TelegramCompactionObserverRuntime<TContext> {
124
+ onSessionBeforeCompact: (
125
+ event: SessionBeforeCompactEvent,
126
+ ctx: TContext,
127
+ ) => void;
128
+ onSessionCompact: (event: SessionCompactEvent, ctx: TContext) => void;
129
+ onSessionShutdown: () => void;
130
+ }
131
+
132
+ export function createTelegramCompactionObserverRuntime<TContext>(
133
+ deps: TelegramCompactionObserverRuntimeDeps<TContext>,
134
+ ): TelegramCompactionObserverRuntime<TContext> {
135
+ const timeoutMs = deps.timeoutMs ?? 300_000;
136
+ const setTimer = deps.setTimer ?? setTimeout;
137
+ const clearTimer = deps.clearTimer ?? clearTimeout;
138
+ let fallbackTimer: TelegramLifecycleTimer | undefined;
139
+ const clearFallbackTimer = (): void => {
140
+ if (!fallbackTimer) return;
141
+ clearTimer(fallbackTimer);
142
+ fallbackTimer = undefined;
143
+ };
144
+ const requestDispatch = (): void => {
145
+ deps.requestDeferredDispatchNextQueuedTelegramTurn(
146
+ deps.dispatchNextQueuedTelegramTurn,
147
+ );
148
+ };
149
+ return {
150
+ onSessionBeforeCompact: (_event, ctx) => {
151
+ deps.setCompactionInProgress(true);
152
+ deps.updateStatus(ctx);
153
+ clearFallbackTimer();
154
+ fallbackTimer = setTimer(() => {
155
+ fallbackTimer = undefined;
156
+ deps.setCompactionInProgress(false);
157
+ deps.updateStatus(ctx);
158
+ deps.recordRuntimeEvent?.(
159
+ "compact",
160
+ new Error("Compaction observer timed out"),
161
+ );
162
+ requestDispatch();
163
+ }, timeoutMs);
164
+ },
165
+ onSessionCompact: (_event, ctx) => {
166
+ clearFallbackTimer();
167
+ deps.setCompactionInProgress(false);
168
+ deps.updateStatus(ctx);
169
+ requestDispatch();
170
+ },
171
+ onSessionShutdown: clearFallbackTimer,
172
+ };
173
+ }
174
+
95
175
  export function createDedupAgentStartHook(
96
176
  dedup: { reset(): void },
97
177
  inner: (event: AgentStartEvent, ctx: ExtensionContext) => Promise<void>,
@@ -139,6 +219,12 @@ export function registerTelegramLifecycleHooks(
139
219
  pi.on("session_shutdown", async (event, ctx) => {
140
220
  await deps.onSessionShutdown(event, ctx);
141
221
  });
222
+ pi.on("session_before_compact", async (event, ctx) => {
223
+ await deps.onSessionBeforeCompact?.(event, ctx);
224
+ });
225
+ pi.on("session_compact", async (event, ctx) => {
226
+ await deps.onSessionCompact?.(event, ctx);
227
+ });
142
228
  pi.on("before_agent_start", async (event, ctx) => {
143
229
  return deps.onBeforeAgentStart(event, ctx);
144
230
  });
@@ -8,6 +8,7 @@ import {
8
8
  getTelegramExtensionSettingsRows,
9
9
  type TelegramSectionRegistry,
10
10
  } from "./extension-sections.ts";
11
+ import type { TelegramTimeMode } from "./config.ts";
11
12
  import type { TelegramInlineKeyboardMarkup } from "./keyboard.ts";
12
13
  import type { TelegramModelMenuState } from "./menu-model.ts";
13
14
  import type { MenuModel } from "./model.ts";
@@ -17,6 +18,7 @@ export type TelegramSettingsMenuReplyMarkup = TelegramInlineKeyboardMarkup;
17
18
 
18
19
  export interface TelegramSettingsStateDeps {
19
20
  isProactivePushEnabled: () => boolean;
21
+ getTimeInjectionMode: () => TelegramTimeMode;
20
22
  getVoiceReplyMode: () => TelegramVoiceReplyMode;
21
23
  isVoiceReplyModeConfigured: () => boolean;
22
24
  }
@@ -26,6 +28,7 @@ export interface TelegramSettingsMutationDeps extends TelegramSettingsStateDeps
26
28
  setVoiceReplyMode: (
27
29
  mode: TelegramVoiceReplyMode | undefined,
28
30
  ) => Promise<void>;
31
+ setTimeInjectionMode: (mode: TelegramTimeMode) => Promise<void>;
29
32
  }
30
33
 
31
34
  export interface TelegramSettingsMenuOpenDeps<
@@ -111,8 +114,9 @@ export interface TelegramSettingsMenuRuntimeDeps<
111
114
  }
112
115
 
113
116
  export const SETTINGS_MENU_TITLE = "<b>⚙️ Settings:</b>";
114
- export const PROACTIVE_PUSH_SETTINGS_TITLE = "<b>Proactive push:</b>";
115
- export const VOICE_REPLY_MODE_SETTINGS_TITLE = "<b>Voice reply mode:</b>";
117
+ export const PROACTIVE_PUSH_SETTINGS_TITLE = "<b>📌 Proactive push:</b>";
118
+ export const TIME_INJECTION_MODE_SETTINGS_TITLE = "<b>🕒 Time injection mode:</b>";
119
+ export const VOICE_REPLY_MODE_SETTINGS_TITLE = "<b>👄 Voice reply mode:</b>";
116
120
 
117
121
  type TelegramVoiceReplyModeSetting = TelegramVoiceReplyMode | "hidden";
118
122
 
@@ -131,17 +135,24 @@ export function buildTelegramSettingsMenuText(): string {
131
135
  return SETTINGS_MENU_TITLE;
132
136
  }
133
137
 
134
- export function buildProactivePushSettingsText(): string {
138
+ export function buildProactivePushSettingsText(
139
+ proactivePushEnabled: boolean,
140
+ ): string {
135
141
  return [
136
- PROACTIVE_PUSH_SETTINGS_TITLE,
142
+ `${PROACTIVE_PUSH_SETTINGS_TITLE} <code>${proactivePushEnabled ? "on" : "off"}</code>`,
137
143
  "",
138
144
  "Send successful local π task results to Telegram when the bridge is connected.",
139
145
  ].join("\n");
140
146
  }
141
147
 
142
- export function buildVoiceReplyModeSettingsText(): string {
148
+ export function buildVoiceReplyModeSettingsText(
149
+ mode: TelegramVoiceReplyMode,
150
+ configured = true,
151
+ ): string {
143
152
  return [
144
- VOICE_REPLY_MODE_SETTINGS_TITLE,
153
+ `${VOICE_REPLY_MODE_SETTINGS_TITLE} <code>${getVoiceReplyModeLabel(
154
+ getVoiceReplyModeSetting(mode, configured),
155
+ )}</code>`,
145
156
  "",
146
157
  "Controls when pi-telegram converts assistant text replies into Telegram voice messages.",
147
158
  "",
@@ -152,9 +163,24 @@ export function buildVoiceReplyModeSettingsText(): string {
152
163
  ].join("\n");
153
164
  }
154
165
 
166
+ export function buildTimeInjectionModeSettingsText(
167
+ mode: TelegramTimeMode,
168
+ ): string {
169
+ return [
170
+ `${TIME_INJECTION_MODE_SETTINGS_TITLE} <code>${mode}</code>`,
171
+ "",
172
+ "Controls whether Telegram-originated prompts include a compact wall-clock [time] line.",
173
+ "",
174
+ "<code>-</code> <code>hidden</code> (default): no time line is added to prompt context.",
175
+ "<code>-</code> <code>always</code>: add time to every Telegram turn.",
176
+ "<code>-</code> <code>interval</code>: add time at most once per chat interval (default: 1 hour).",
177
+ ].join("\n");
178
+ }
179
+
155
180
  export function buildTelegramSettingsMenuReplyMarkup(
156
181
  proactivePushEnabled: boolean,
157
182
  voiceReplyMode: TelegramVoiceReplyMode,
183
+ timeInjectionMode: TelegramTimeMode,
158
184
  sectionRegistry?: TelegramSectionRegistry,
159
185
  voiceReplyModeConfigured = true,
160
186
  ): TelegramSettingsMenuReplyMarkup {
@@ -179,7 +205,13 @@ export function buildTelegramSettingsMenuReplyMarkup(
179
205
  ],
180
206
  [
181
207
  {
182
- text: `${proactivePushEnabled ? "🟢" : "⚫️"} Proactive push`,
208
+ text: `🕒 Time injection: ${timeInjectionMode}`,
209
+ callback_data: "settings:open:time-injection",
210
+ },
211
+ ],
212
+ [
213
+ {
214
+ text: `📌 Proactive push: ${proactivePushEnabled ? "on" : "off"}`,
183
215
  callback_data: "settings:open:proactive",
184
216
  },
185
217
  ],
@@ -200,6 +232,7 @@ export async function openTelegramSettingsMenu<
200
232
  buildTelegramSettingsMenuReplyMarkup(
201
233
  deps.isProactivePushEnabled(),
202
234
  deps.getVoiceReplyMode(),
235
+ deps.getTimeInjectionMode(),
203
236
  sectionRegistry,
204
237
  deps.isVoiceReplyModeConfigured(),
205
238
  ),
@@ -230,6 +263,23 @@ export function buildProactivePushSettingsReplyMarkup(
230
263
  };
231
264
  }
232
265
 
266
+ export function buildTimeInjectionModeSettingsReplyMarkup(
267
+ mode: TelegramTimeMode,
268
+ ): TelegramSettingsMenuReplyMarkup {
269
+ const modes: TelegramTimeMode[] = ["hidden", "always", "interval"];
270
+ return {
271
+ inline_keyboard: [
272
+ [{ text: "⬆️ Back", callback_data: "settings:list" }],
273
+ ...modes.map((value) => [
274
+ {
275
+ text: `${value === mode ? "🟢 " : ""}${value}`,
276
+ callback_data: `settings:set:time-injection:${value}`,
277
+ },
278
+ ]),
279
+ ],
280
+ };
281
+ }
282
+
233
283
  export function buildVoiceReplyModeSettingsReplyMarkup(
234
284
  mode: TelegramVoiceReplyMode,
235
285
  configured = true,
@@ -263,6 +313,7 @@ export async function updateTelegramSettingsMenuMessage(
263
313
  buildTelegramSettingsMenuReplyMarkup(
264
314
  deps.isProactivePushEnabled(),
265
315
  deps.getVoiceReplyMode(),
316
+ deps.getTimeInjectionMode(),
266
317
  sectionRegistry,
267
318
  deps.isVoiceReplyModeConfigured(),
268
319
  ),
@@ -272,21 +323,31 @@ export async function updateTelegramSettingsMenuMessage(
272
323
  export async function updateProactivePushSettingsMessage(
273
324
  deps: TelegramSettingsMenuCallbackDeps,
274
325
  ): Promise<void> {
326
+ const proactivePushEnabled = deps.isProactivePushEnabled();
327
+ await deps.updateSettingsMessage(
328
+ buildProactivePushSettingsText(proactivePushEnabled),
329
+ buildProactivePushSettingsReplyMarkup(proactivePushEnabled),
330
+ );
331
+ }
332
+
333
+ export async function updateTimeInjectionModeSettingsMessage(
334
+ deps: TelegramSettingsMenuCallbackDeps,
335
+ ): Promise<void> {
336
+ const mode = deps.getTimeInjectionMode();
275
337
  await deps.updateSettingsMessage(
276
- buildProactivePushSettingsText(),
277
- buildProactivePushSettingsReplyMarkup(deps.isProactivePushEnabled()),
338
+ buildTimeInjectionModeSettingsText(mode),
339
+ buildTimeInjectionModeSettingsReplyMarkup(mode),
278
340
  );
279
341
  }
280
342
 
281
343
  export async function updateVoiceReplyModeSettingsMessage(
282
344
  deps: TelegramSettingsMenuCallbackDeps,
283
345
  ): Promise<void> {
346
+ const mode = deps.getVoiceReplyMode();
347
+ const configured = deps.isVoiceReplyModeConfigured();
284
348
  await deps.updateSettingsMessage(
285
- buildVoiceReplyModeSettingsText(),
286
- buildVoiceReplyModeSettingsReplyMarkup(
287
- deps.getVoiceReplyMode(),
288
- deps.isVoiceReplyModeConfigured(),
289
- ),
349
+ buildVoiceReplyModeSettingsText(mode, configured),
350
+ buildVoiceReplyModeSettingsReplyMarkup(mode, configured),
290
351
  );
291
352
  }
292
353
 
@@ -311,6 +372,11 @@ export async function handleTelegramSettingsMenuCallbackAction(
311
372
  await deps.answerCallbackQuery(callbackQueryId);
312
373
  return true;
313
374
  }
375
+ if (data === "settings:open:time-injection" || data === "settings:open:time") {
376
+ await updateTimeInjectionModeSettingsMessage(deps);
377
+ await deps.answerCallbackQuery(callbackQueryId);
378
+ return true;
379
+ }
314
380
  if (data.startsWith("settings:set:voice-reply:")) {
315
381
  const mode = data.slice("settings:set:voice-reply:".length);
316
382
  if (
@@ -328,6 +394,28 @@ export async function handleTelegramSettingsMenuCallbackAction(
328
394
  return true;
329
395
  }
330
396
  }
397
+ if (
398
+ data.startsWith("settings:set:time-injection:") ||
399
+ data.startsWith("settings:set:time:")
400
+ ) {
401
+ const mode = data.startsWith("settings:set:time-injection:")
402
+ ? data.slice("settings:set:time-injection:".length)
403
+ : data.slice("settings:set:time:".length);
404
+ const normalizedMode = mode === "off" ? "hidden" : mode;
405
+ if (
406
+ normalizedMode === "hidden" ||
407
+ normalizedMode === "always" ||
408
+ normalizedMode === "interval"
409
+ ) {
410
+ await deps.setTimeInjectionMode(normalizedMode);
411
+ await updateTimeInjectionModeSettingsMessage(deps);
412
+ await deps.answerCallbackQuery(
413
+ callbackQueryId,
414
+ `Time injection: ${normalizedMode}`,
415
+ );
416
+ return true;
417
+ }
418
+ }
331
419
  if (
332
420
  data === "settings:set:proactive:on" ||
333
421
  data === "settings:set:proactive:off"
@@ -360,6 +448,7 @@ export function createTelegramSettingsMenuRuntime<
360
448
  isProactivePushEnabled: deps.isProactivePushEnabled,
361
449
  getVoiceReplyMode: deps.getVoiceReplyMode,
362
450
  isVoiceReplyModeConfigured: deps.isVoiceReplyModeConfigured,
451
+ getTimeInjectionMode: deps.getTimeInjectionMode,
363
452
  sendSettingsMenu: (state, text, replyMarkup) =>
364
453
  deps.sendInteractiveMessage(
365
454
  state.chatId,
@@ -377,6 +466,7 @@ export function createTelegramSettingsMenuRuntime<
377
466
  isProactivePushEnabled: deps.isProactivePushEnabled,
378
467
  getVoiceReplyMode: deps.getVoiceReplyMode,
379
468
  isVoiceReplyModeConfigured: deps.isVoiceReplyModeConfigured,
469
+ getTimeInjectionMode: deps.getTimeInjectionMode,
380
470
  updateSettingsMessage: (text, replyMarkup) =>
381
471
  deps.editInteractiveMessage(
382
472
  state.chatId,
@@ -392,16 +482,42 @@ export function createTelegramSettingsMenuRuntime<
392
482
  if (!query.data?.startsWith("settings:")) return false;
393
483
  const state = deps.getStoredModelMenuState(query.message?.message_id);
394
484
  if (!state) {
395
- const mode = query.data.slice("settings:set:voice-reply:".length);
485
+ const voiceMode = query.data.slice("settings:set:voice-reply:".length);
396
486
  if (
397
487
  query.data.startsWith("settings:set:voice-reply:") &&
398
- (mode === "hidden" ||
399
- mode === "manual" ||
400
- mode === "mirror" ||
401
- mode === "always")
488
+ (voiceMode === "hidden" ||
489
+ voiceMode === "manual" ||
490
+ voiceMode === "mirror" ||
491
+ voiceMode === "always")
492
+ ) {
493
+ await deps.setVoiceReplyMode(
494
+ voiceMode === "hidden" ? undefined : voiceMode,
495
+ );
496
+ await deps.answerCallbackQuery(
497
+ query.id,
498
+ `Voice reply mode: ${voiceMode}`,
499
+ );
500
+ return true;
501
+ }
502
+ const hasTimeInjectionPrefix = query.data.startsWith(
503
+ "settings:set:time-injection:",
504
+ );
505
+ const timeMode = hasTimeInjectionPrefix
506
+ ? query.data.slice("settings:set:time-injection:".length)
507
+ : query.data.slice("settings:set:time:".length);
508
+ if (
509
+ (hasTimeInjectionPrefix || query.data.startsWith("settings:set:time:")) &&
510
+ (timeMode === "off" ||
511
+ timeMode === "hidden" ||
512
+ timeMode === "always" ||
513
+ timeMode === "interval")
402
514
  ) {
403
- await deps.setVoiceReplyMode(mode === "hidden" ? undefined : mode);
404
- await deps.answerCallbackQuery(query.id, `Voice reply mode: ${mode}`);
515
+ const normalizedMode = timeMode === "off" ? "hidden" : timeMode;
516
+ await deps.setTimeInjectionMode(normalizedMode);
517
+ await deps.answerCallbackQuery(
518
+ query.id,
519
+ `Time injection: ${normalizedMode}`,
520
+ );
405
521
  return true;
406
522
  }
407
523
  await deps.answerCallbackQuery(
@@ -414,8 +530,10 @@ export function createTelegramSettingsMenuRuntime<
414
530
  isProactivePushEnabled: deps.isProactivePushEnabled,
415
531
  getVoiceReplyMode: deps.getVoiceReplyMode,
416
532
  isVoiceReplyModeConfigured: deps.isVoiceReplyModeConfigured,
533
+ getTimeInjectionMode: deps.getTimeInjectionMode,
417
534
  setProactivePushEnabled: deps.setProactivePushEnabled,
418
535
  setVoiceReplyMode: deps.setVoiceReplyMode,
536
+ setTimeInjectionMode: deps.setTimeInjectionMode,
419
537
  updateSettingsMessage: (text, replyMarkup) =>
420
538
  deps.editInteractiveMessage(
421
539
  state.chatId,
package/lib/pi.ts CHANGED
@@ -11,6 +11,8 @@ import {
11
11
  type ExtensionAPI,
12
12
  type ExtensionCommandContext,
13
13
  type ExtensionContext,
14
+ type SessionBeforeCompactEvent,
15
+ type SessionCompactEvent,
14
16
  type SessionShutdownEvent,
15
17
  type SessionStartEvent,
16
18
  type SlashCommandInfo,
@@ -24,6 +26,8 @@ export type {
24
26
  ExtensionAPI,
25
27
  ExtensionCommandContext,
26
28
  ExtensionContext,
29
+ SessionBeforeCompactEvent,
30
+ SessionCompactEvent,
27
31
  SessionShutdownEvent,
28
32
  SessionStartEvent,
29
33
  SlashCommandInfo,
package/lib/prompts.ts CHANGED
@@ -15,6 +15,7 @@ Inbound context:
15
15
  - \`[telegram]\` marks Telegram-originated messages. Suffixes \`|from:user\` (sender) and \`|guest:group\` (guest mode — message from another chat where the bot is not a member) may be present; the bot sees the message as if forwarded from that user/chat.
16
16
  - \`[reply]\` is quoted context from the replied-to message, not a new instruction by itself. Suffix \`|from:user\` identifies the original author in guest-mode replies. Use it to resolve references like "this", "it", or "that message"; the actual instruction is before [reply] unless it explicitly asks to act on the quote.
17
17
  - \`[attachments]\` gives a base directory plus relative local files; resolve and read them as needed. \`[outputs]\` contains inbound-handler stdout such as transcriptions or extracted text for those attachments.
18
+ - \`[time]\` gives the wall-clock time for this Telegram turn when the operator enabled time injection. Use it for relative-date requests like "today", "now", or scheduling; otherwise do not mention it.
18
19
  - \`[voice]\` describes Telegram voice reply policy for this turn. \`manual\` means answer normally and use explicit \`telegram_voice\` markup only when a spoken reply is useful; \`mirror\` means voice input prefers a voice reply; \`always\` means the final reply is expected to be converted to voice, so keep it TTS-friendly.
19
20
  - Unknown \`[callback] ...\` messages may be intended for another extension; if you see one, say the callback was not handled and the environment may be misconfigured.
20
21
 
package/lib/routing.ts CHANGED
@@ -114,6 +114,7 @@ export interface TelegramInboundRouteRuntimeDeps<
114
114
  typeof PromptTemplates.getTelegramPromptTemplateCommands
115
115
  >[0];
116
116
  downloadFile: Media.DownloadTelegramMessageFilesDeps["downloadFile"];
117
+ resolveTimeLine?: (chatId: number) => string | null;
117
118
  getThinkingLevel: () => Model.ThinkingLevel;
118
119
  setThinkingLevel: (level: Model.ThinkingLevel) => void;
119
120
  persistScopedModelPatterns?: (
@@ -287,6 +288,7 @@ export function createTelegramInboundRouteRuntime<
287
288
  allocateQueueOrder: deps.bridgeRuntime.queue.allocateItemOrder,
288
289
  downloadFile: deps.downloadFile,
289
290
  processAttachments: deps.inboundHandlerRuntime.process,
291
+ resolveTimeLine: deps.resolveTimeLine,
290
292
 
291
293
  // Voice policy for the current turn. Missing config still behaves as manual,
292
294
  // but only explicit telegram.json voice.replyMode is shown in prompt context.
@@ -300,28 +302,19 @@ export function createTelegramInboundRouteRuntime<
300
302
  message: TMessage,
301
303
  ctx: TContext,
302
304
  ): Promise<void> => {
303
- const enqueuePlan = Queue.planTelegramPromptEnqueue(
304
- deps.telegramQueueStore.getQueuedItems(),
305
- deps.bridgeRuntime.lifecycle.shouldPreserveQueuedTurnsAsHistory(),
306
- );
307
305
  deps.bridgeRuntime.lifecycle.setPreserveQueuedTurnsAsHistory(false);
308
306
  const continueMessage = {
309
307
  ...message,
310
308
  text: "continue",
311
309
  caption: undefined,
312
310
  } as TMessage;
313
- const turn = await promptTurnBuilder(
314
- [continueMessage],
315
- enqueuePlan.historyTurns,
316
- ctx,
317
- );
311
+ const turn = await promptTurnBuilder([continueMessage], [], ctx);
318
312
  const continueTurn = {
319
313
  ...turn,
320
314
  queueLane: "priority" as const,
321
315
  laneOrder: Number.MIN_SAFE_INTEGER + turn.queueOrder,
322
316
  statusSummary: "continue",
323
317
  };
324
- deps.telegramQueueStore.setQueuedItems(enqueuePlan.remainingItems);
325
318
  deps.queueMutationRuntime.append(continueTurn, ctx);
326
319
  deps.dispatchNextQueuedTelegramTurn(ctx);
327
320
  };
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Telegram per-chat time injection runtime
3
+ * Zones: telegram inbound, prompt content
4
+ * Owns the formatted `[time]` line and the per-chat interval bookkeeping that decides when to emit it
5
+ */
6
+
7
+ import type { ResolvedTelegramTimeConfig } from "./config.ts";
8
+
9
+ export interface TimeInjectionRuntime {
10
+ resolveLine: (chatId: number, now?: Date) => string | null;
11
+ }
12
+
13
+ export interface TimeInjectionRuntimeDeps {
14
+ getConfig: () => ResolvedTelegramTimeConfig;
15
+ recordRuntimeEvent?: (
16
+ category: string,
17
+ error: unknown,
18
+ details?: Record<string, unknown>,
19
+ ) => void;
20
+ }
21
+
22
+ export function formatTelegramTimeInjectionLine(
23
+ now: Date,
24
+ timezone: string,
25
+ ): string {
26
+ const parts = new Intl.DateTimeFormat("en-CA", {
27
+ timeZone: timezone,
28
+ year: "numeric",
29
+ month: "2-digit",
30
+ day: "2-digit",
31
+ hour: "2-digit",
32
+ minute: "2-digit",
33
+ second: "2-digit",
34
+ hour12: false,
35
+ }).formatToParts(now);
36
+ const get = (type: Intl.DateTimeFormatPartTypes): string =>
37
+ parts.find((part) => part.type === type)?.value ?? "";
38
+ const year = get("year");
39
+ const month = get("month");
40
+ const day = get("day");
41
+ const hourRaw = get("hour");
42
+ const hour = hourRaw === "24" ? "00" : hourRaw;
43
+ const minute = get("minute");
44
+ const second = get("second");
45
+ return `${year}-${month}-${day} ${hour}:${minute}:${second} ${timezone}`;
46
+ }
47
+
48
+ export function createTimeInjectionRuntime(
49
+ deps: TimeInjectionRuntimeDeps,
50
+ ): TimeInjectionRuntime {
51
+ const lastInjectedAt = new Map<number, number>();
52
+ return {
53
+ resolveLine: (chatId, now = new Date()) => {
54
+ const config = deps.getConfig();
55
+ if (config.injectionMode === "hidden") return null;
56
+ let line: string;
57
+ try {
58
+ line = formatTelegramTimeInjectionLine(now, config.timezone);
59
+ } catch (error) {
60
+ deps.recordRuntimeEvent?.("time-injection", error, {
61
+ timezone: config.timezone,
62
+ });
63
+ return null;
64
+ }
65
+ if (config.injectionMode === "always") return line;
66
+ const previous = lastInjectedAt.get(chatId);
67
+ const nowMs = now.getTime();
68
+ if (
69
+ previous !== undefined &&
70
+ nowMs - previous < config.interval
71
+ ) {
72
+ return null;
73
+ }
74
+ lastInjectedAt.set(chatId, nowMs);
75
+ return line;
76
+ },
77
+ };
78
+ }