@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/AGENTS.md +4 -2
- package/CHANGELOG.md +22 -2
- package/README.md +38 -10
- package/docs/architecture.md +137 -30
- package/docs/command-templates.md +81 -24
- package/index.ts +33 -42
- package/lib/command-templates.ts +163 -32
- package/lib/config.ts +64 -0
- package/lib/lifecycle.ts +86 -0
- package/lib/menu-settings.ts +139 -21
- package/lib/pi.ts +4 -0
- package/lib/prompts.ts +1 -0
- package/lib/routing.ts +3 -10
- package/lib/time-injection.ts +78 -0
- package/lib/turns.ts +13 -0
- package/package.json +1 -1
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
|
});
|
package/lib/menu-settings.ts
CHANGED
|
@@ -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
|
|
115
|
-
export const
|
|
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(
|
|
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(
|
|
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:
|
|
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
|
-
|
|
277
|
-
|
|
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
|
|
485
|
+
const voiceMode = query.data.slice("settings:set:voice-reply:".length);
|
|
396
486
|
if (
|
|
397
487
|
query.data.startsWith("settings:set:voice-reply:") &&
|
|
398
|
-
(
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
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
|
-
|
|
404
|
-
await deps.
|
|
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
|
+
}
|