@llblab/pi-telegram 0.45.11 → 0.47.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 (204) hide show
  1. package/BACKLOG.md +5 -1
  2. package/CHANGELOG.md +12 -0
  3. package/README.md +6 -6
  4. package/dist/api/activity.d.ts +6 -0
  5. package/dist/api/activity.js +6 -0
  6. package/dist/api/commands.d.ts +6 -0
  7. package/dist/api/commands.js +6 -0
  8. package/dist/api/delivery.d.ts +6 -0
  9. package/dist/api/delivery.js +6 -0
  10. package/dist/api/inbound.d.ts +6 -0
  11. package/dist/api/inbound.js +6 -0
  12. package/dist/api/keyboard.d.ts +6 -0
  13. package/dist/api/keyboard.js +6 -0
  14. package/dist/api/outbound.d.ts +6 -0
  15. package/dist/api/outbound.js +6 -0
  16. package/dist/api/sections.d.ts +7 -0
  17. package/dist/api/sections.js +6 -0
  18. package/dist/api/status.d.ts +6 -0
  19. package/dist/api/status.js +6 -0
  20. package/dist/api/updates.d.ts +6 -0
  21. package/dist/api/updates.js +6 -0
  22. package/dist/api/voice.d.ts +6 -0
  23. package/dist/api/voice.js +6 -0
  24. package/dist/index.d.ts +6 -0
  25. package/dist/index.js +6 -0
  26. package/dist/lib/activity-verbosity.d.ts +52 -0
  27. package/dist/lib/activity-verbosity.js +596 -0
  28. package/dist/lib/activity.d.ts +220 -0
  29. package/dist/lib/activity.js +574 -0
  30. package/dist/lib/agent-messages.d.ts +28 -0
  31. package/dist/lib/agent-messages.js +86 -0
  32. package/dist/lib/bindings.d.ts +250 -0
  33. package/dist/lib/bindings.js +936 -0
  34. package/dist/lib/bus-api.d.ts +18 -0
  35. package/dist/lib/bus-api.js +254 -0
  36. package/dist/lib/bus-follower.d.ts +464 -0
  37. package/dist/lib/bus-follower.js +1686 -0
  38. package/dist/lib/bus-leader.d.ts +292 -0
  39. package/dist/lib/bus-leader.js +2242 -0
  40. package/dist/lib/bus-transport.d.ts +64 -0
  41. package/dist/lib/bus-transport.js +140 -0
  42. package/dist/lib/bus.d.ts +518 -0
  43. package/dist/lib/bus.js +2055 -0
  44. package/dist/lib/channel-posts.d.ts +170 -0
  45. package/dist/lib/channel-posts.js +611 -0
  46. package/dist/lib/command-templates.d.ts +70 -0
  47. package/dist/lib/command-templates.js +744 -0
  48. package/dist/lib/commands.d.ts +541 -0
  49. package/dist/lib/commands.js +1155 -0
  50. package/dist/lib/config.d.ts +252 -0
  51. package/dist/lib/config.js +889 -0
  52. package/dist/lib/delivery.d.ts +163 -0
  53. package/dist/lib/delivery.js +542 -0
  54. package/dist/lib/extension.d.ts +7 -0
  55. package/dist/lib/extension.js +1608 -0
  56. package/dist/lib/generative-app-worker.mjs +104 -0
  57. package/dist/lib/generative-apps.d.ts +212 -0
  58. package/dist/lib/generative-apps.js +1006 -0
  59. package/dist/lib/inbound.d.ts +91 -0
  60. package/dist/lib/inbound.js +502 -0
  61. package/dist/lib/journal.d.ts +665 -0
  62. package/dist/lib/journal.js +3701 -0
  63. package/dist/lib/keyboard.d.ts +23 -0
  64. package/dist/lib/keyboard.js +37 -0
  65. package/dist/lib/lifecycle.d.ts +157 -0
  66. package/dist/lib/lifecycle.js +395 -0
  67. package/dist/lib/locks.d.ts +164 -0
  68. package/dist/lib/locks.js +1208 -0
  69. package/dist/lib/logging.d.ts +50 -0
  70. package/dist/lib/logging.js +274 -0
  71. package/dist/lib/media.d.ts +181 -0
  72. package/dist/lib/media.js +602 -0
  73. package/dist/lib/menu-model.d.ts +217 -0
  74. package/dist/lib/menu-model.js +663 -0
  75. package/dist/lib/menu-queue.d.ts +37 -0
  76. package/dist/lib/menu-queue.js +408 -0
  77. package/dist/lib/menu-settings.d.ts +116 -0
  78. package/dist/lib/menu-settings.js +595 -0
  79. package/dist/lib/menu-status.d.ts +32 -0
  80. package/dist/lib/menu-status.js +112 -0
  81. package/dist/lib/menu-thinking.d.ts +29 -0
  82. package/dist/lib/menu-thinking.js +82 -0
  83. package/dist/lib/menu.d.ts +171 -0
  84. package/dist/lib/menu.js +323 -0
  85. package/dist/lib/model.d.ts +127 -0
  86. package/dist/lib/model.js +409 -0
  87. package/dist/lib/outbound-attachments.d.ts +241 -0
  88. package/dist/lib/outbound-attachments.js +639 -0
  89. package/dist/lib/outbound-buttons.d.ts +69 -0
  90. package/dist/lib/outbound-buttons.js +248 -0
  91. package/dist/lib/outbound-markup.d.ts +42 -0
  92. package/dist/lib/outbound-markup.js +678 -0
  93. package/dist/lib/outbound-voice.d.ts +55 -0
  94. package/dist/lib/outbound-voice.js +152 -0
  95. package/dist/lib/outbound.d.ts +185 -0
  96. package/dist/lib/outbound.js +516 -0
  97. package/dist/lib/ownership.d.ts +77 -0
  98. package/dist/lib/ownership.js +174 -0
  99. package/dist/lib/paths.d.ts +40 -0
  100. package/dist/lib/paths.js +94 -0
  101. package/dist/lib/pi.d.ts +72 -0
  102. package/dist/lib/pi.js +121 -0
  103. package/dist/lib/polling.d.ts +351 -0
  104. package/dist/lib/polling.js +1196 -0
  105. package/dist/lib/preview.d.ts +192 -0
  106. package/dist/lib/preview.js +614 -0
  107. package/dist/lib/prompt-templates.d.ts +24 -0
  108. package/dist/lib/prompt-templates.js +118 -0
  109. package/dist/lib/prompts.d.ts +57 -0
  110. package/dist/lib/prompts.js +167 -0
  111. package/dist/lib/queue.d.ts +766 -0
  112. package/dist/lib/queue.js +1965 -0
  113. package/dist/lib/recovery.d.ts +86 -0
  114. package/dist/lib/recovery.js +285 -0
  115. package/dist/lib/rendering.d.ts +20 -0
  116. package/dist/lib/rendering.js +983 -0
  117. package/dist/lib/replies.d.ts +214 -0
  118. package/dist/lib/replies.js +737 -0
  119. package/dist/lib/routing.d.ts +207 -0
  120. package/dist/lib/routing.js +2282 -0
  121. package/dist/lib/runtime.d.ts +172 -0
  122. package/dist/lib/runtime.js +402 -0
  123. package/dist/lib/sections.d.ts +165 -0
  124. package/dist/lib/sections.js +327 -0
  125. package/dist/lib/setup.d.ts +82 -0
  126. package/dist/lib/setup.js +150 -0
  127. package/dist/lib/skills.d.ts +8 -0
  128. package/dist/lib/skills.js +12 -0
  129. package/dist/lib/status.d.ts +442 -0
  130. package/dist/lib/status.js +1006 -0
  131. package/dist/lib/sync.d.ts +179 -0
  132. package/dist/lib/sync.js +782 -0
  133. package/dist/lib/target.d.ts +20 -0
  134. package/dist/lib/target.js +27 -0
  135. package/dist/lib/telegram-api.d.ts +541 -0
  136. package/dist/lib/telegram-api.js +1159 -0
  137. package/dist/lib/text-groups.d.ts +89 -0
  138. package/dist/lib/text-groups.js +317 -0
  139. package/dist/lib/thread-cleanup-manager.d.ts +288 -0
  140. package/dist/lib/thread-cleanup-manager.js +560 -0
  141. package/dist/lib/thread-display.d.ts +46 -0
  142. package/dist/lib/thread-display.js +257 -0
  143. package/dist/lib/thread-naming.d.ts +46 -0
  144. package/dist/lib/thread-naming.js +78 -0
  145. package/dist/lib/thread-reconciler.d.ts +239 -0
  146. package/dist/lib/thread-reconciler.js +644 -0
  147. package/dist/lib/threads.d.ts +621 -0
  148. package/dist/lib/threads.js +3679 -0
  149. package/dist/lib/time-injection.d.ts +15 -0
  150. package/dist/lib/time-injection.js +56 -0
  151. package/dist/lib/turns.d.ts +109 -0
  152. package/dist/lib/turns.js +500 -0
  153. package/dist/lib/updates.d.ts +1290 -0
  154. package/dist/lib/updates.js +3634 -0
  155. package/dist/lib/voice.d.ts +117 -0
  156. package/dist/lib/voice.js +174 -0
  157. package/dist/lib/workspace-admission.d.ts +264 -0
  158. package/dist/lib/workspace-admission.js +1136 -0
  159. package/dist/lib/workspace-retirement.d.ts +219 -0
  160. package/dist/lib/workspace-retirement.js +587 -0
  161. package/dist/lib/workspace-slots.d.ts +30 -0
  162. package/dist/lib/workspace-slots.js +54 -0
  163. package/dist/package.json +126 -0
  164. package/dist/pi-telegram/index.js +1 -0
  165. package/dist/skills/generated-control-surface/SKILL.md +107 -0
  166. package/dist/skills/generated-control-surface/references/capability-adapters.md +27 -0
  167. package/dist/skills/generated-control-surface/references/layout-and-state.md +37 -0
  168. package/dist/skills/generative-apps/SKILL.md +115 -0
  169. package/dist/skills/show-me/SKILL.md +166 -0
  170. package/dist/skills/show-me/references/telegram-surfaces.md +43 -0
  171. package/dist/skills/telegram-bridge/SKILL.md +129 -0
  172. package/dist/skills/telegram-bridge/references/configuration.md +15 -0
  173. package/dist/skills/telegram-bridge/references/delivery-and-threads.md +27 -0
  174. package/dist/skills/telegram-bridge/references/diagnosis.md +18 -0
  175. package/docs/architecture.md +1 -1
  176. package/docs/compact-matrix-literal.md +10 -3
  177. package/docs/generative-apps.md +15 -13
  178. package/docs/multi-instance-bus.md +17 -3
  179. package/docs/outbound.md +1 -1
  180. package/docs/public-api.md +2 -2
  181. package/lib/bindings.ts +89 -6
  182. package/lib/bus-follower.ts +11 -3
  183. package/lib/bus-leader.ts +57 -20
  184. package/lib/bus.ts +12 -2
  185. package/lib/commands.ts +2 -2
  186. package/lib/config.ts +14 -7
  187. package/lib/delivery.ts +38 -6
  188. package/lib/extension.ts +26 -1
  189. package/lib/generative-apps.ts +379 -13
  190. package/lib/menu-settings.ts +21 -5
  191. package/lib/outbound-markup.ts +16 -7
  192. package/lib/pi.ts +4 -0
  193. package/lib/sync.ts +3 -0
  194. package/lib/telegram-api.ts +12 -1
  195. package/lib/thread-cleanup-manager.ts +36 -6
  196. package/lib/thread-display.ts +96 -18
  197. package/lib/threads.ts +150 -30
  198. package/package.json +56 -13
  199. package/scripts/build-dist.mjs +44 -0
  200. package/scripts/measure-bus.mjs +8 -1
  201. package/scripts/measure-workspace.mjs +8 -1
  202. package/skills/generative-apps/SKILL.md +1 -1
  203. package/skills/show-me/SKILL.md +1 -1
  204. package/skills/telegram-bridge/SKILL.md +1 -1
@@ -0,0 +1,15 @@
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
+ import type { ResolvedTelegramTimeConfig } from "./config.ts";
7
+ export interface TimeInjectionRuntime {
8
+ resolveLine: (chatId: number, now?: Date) => string | null;
9
+ }
10
+ export interface TimeInjectionRuntimeDeps {
11
+ getConfig: () => ResolvedTelegramTimeConfig;
12
+ recordRuntimeEvent?: (category: string, error: unknown, details?: Record<string, unknown>) => void;
13
+ }
14
+ export declare function formatTelegramTimeInjectionLine(now: Date, timezone: string): string;
15
+ export declare function createTimeInjectionRuntime(deps: TimeInjectionRuntimeDeps): TimeInjectionRuntime;
@@ -0,0 +1,56 @@
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
+ export function formatTelegramTimeInjectionLine(now, timezone) {
7
+ const parts = new Intl.DateTimeFormat("en-CA", {
8
+ timeZone: timezone,
9
+ year: "numeric",
10
+ month: "2-digit",
11
+ day: "2-digit",
12
+ hour: "2-digit",
13
+ minute: "2-digit",
14
+ second: "2-digit",
15
+ hour12: false,
16
+ }).formatToParts(now);
17
+ const get = (type) => parts.find((part) => part.type === type)?.value ?? "";
18
+ const year = get("year");
19
+ const month = get("month");
20
+ const day = get("day");
21
+ const hourRaw = get("hour");
22
+ const hour = hourRaw === "24" ? "00" : hourRaw;
23
+ const minute = get("minute");
24
+ const second = get("second");
25
+ return `${year}-${month}-${day} ${hour}:${minute}:${second} ${timezone}`;
26
+ }
27
+ export function createTimeInjectionRuntime(deps) {
28
+ const lastInjectedAt = new Map();
29
+ return {
30
+ resolveLine: (chatId, now = new Date()) => {
31
+ const config = deps.getConfig();
32
+ if (config.injectionMode === "hidden")
33
+ return null;
34
+ let line;
35
+ try {
36
+ line = formatTelegramTimeInjectionLine(now, config.timezone);
37
+ }
38
+ catch (error) {
39
+ deps.recordRuntimeEvent?.("time-injection", error, {
40
+ timezone: config.timezone,
41
+ });
42
+ return null;
43
+ }
44
+ if (config.injectionMode === "always")
45
+ return line;
46
+ const previous = lastInjectedAt.get(chatId);
47
+ const nowMs = now.getTime();
48
+ if (previous !== undefined &&
49
+ nowMs - previous < config.interval) {
50
+ return null;
51
+ }
52
+ lastInjectedAt.set(chatId, nowMs);
53
+ return line;
54
+ },
55
+ };
56
+ }
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Telegram turn-building helpers
3
+ * Zones: telegram inbound, pi agent prompt content, queue
4
+ * Owns prompt-turn summary and content construction so queued Telegram turns are assembled consistently
5
+ */
6
+ import { type DownloadedTelegramMessageFile, type DownloadTelegramMessageFilesDeps, type TelegramMediaMessage } from "./media.ts";
7
+ import { truncateTelegramQueueSummary, type PendingTelegramTurn, type TelegramPreparedPromptTurn, type TelegramQueueItem, type TelegramQueueStore } from "./queue.ts";
8
+ import { type TelegramVoiceReplyMode } from "./voice.ts";
9
+ export declare const TELEGRAM_PREFIX = "[telegram]";
10
+ export declare const TELEGRAM_GUEST_TURN_NOTE = "[guest] delivery: answer quickly with one concise, self-contained reply";
11
+ export interface TelegramTurnTarget {
12
+ chatId: number;
13
+ threadId?: number;
14
+ }
15
+ export interface TelegramTurnMessage {
16
+ message_id: number;
17
+ message_thread_id?: number;
18
+ pi_telegram_agent_source_thread?: string;
19
+ pi_telegram_source_update_id?: number;
20
+ chat: {
21
+ id: number;
22
+ type?: string;
23
+ };
24
+ }
25
+ export type DownloadedTelegramTurnFile = DownloadedTelegramMessageFile;
26
+ export declare function createTelegramTurnPrefix(attributes?: Record<string, string | undefined>): string;
27
+ export declare function formatTelegramTurnPrefix(_message: TelegramTurnMessage, basePrefix?: string): string;
28
+ export { truncateTelegramQueueSummary };
29
+ export declare function formatTelegramTurnStatusSummary(rawText: string, files: DownloadedTelegramTurnFile[], handlerOutputs?: string[]): string;
30
+ export declare function buildTelegramTurnPrompt(options: {
31
+ telegramPrefix: string;
32
+ rawText: string;
33
+ files: DownloadedTelegramTurnFile[];
34
+ promptFiles?: DownloadedTelegramTurnFile[];
35
+ displayFiles?: DownloadedTelegramTurnFile[];
36
+ handlerOutputs?: string[];
37
+ sourceContext?: string;
38
+ historyTurns?: Pick<PendingTelegramTurn, "historyText">[];
39
+ timeLine?: string | null;
40
+ voiceContext?: Record<string, string>;
41
+ guestTurn?: boolean;
42
+ }): string;
43
+ export declare function updateTelegramPromptTurnText(options: {
44
+ turn: PendingTelegramTurn;
45
+ telegramPrefix: string;
46
+ rawText: string;
47
+ statusText?: string;
48
+ }): PendingTelegramTurn;
49
+ export declare function updateQueuedTelegramPromptTurnText<TContext = unknown>(options: {
50
+ items: TelegramQueueItem<TContext>[];
51
+ sourceMessageId: number | undefined;
52
+ telegramPrefix: string;
53
+ rawText: string;
54
+ statusText?: string;
55
+ }): {
56
+ items: TelegramQueueItem<TContext>[];
57
+ changed: boolean;
58
+ };
59
+ export interface TelegramQueuedPromptEditRuntimeDeps<TContext = unknown> extends TelegramQueueStore<TContext> {
60
+ updateStatus: (ctx: TContext) => void;
61
+ }
62
+ export declare function createTelegramQueuedPromptEditRuntime<TMessage extends TelegramMediaMessage, TContext = unknown>(deps: TelegramQueuedPromptEditRuntimeDeps<TContext>): {
63
+ updateFromEditedMessage: (message: TMessage, ctx: TContext) => boolean;
64
+ };
65
+ export interface BuildTelegramPromptTurnOptions {
66
+ telegramPrefix: string;
67
+ messages: TelegramTurnMessage[];
68
+ historyTurns?: PendingTelegramTurn[];
69
+ queueOrder: number;
70
+ rawText: string;
71
+ statusText?: string;
72
+ files: DownloadedTelegramTurnFile[];
73
+ promptFiles?: DownloadedTelegramTurnFile[];
74
+ displayFiles?: DownloadedTelegramTurnFile[];
75
+ handlerOutputs?: string[];
76
+ sourceContext?: string;
77
+ timeLine?: string | null;
78
+ readBinaryFile: (path: string) => Promise<Uint8Array>;
79
+ inferImageMimeType: (path: string) => string | undefined;
80
+ voiceReplyMode?: TelegramVoiceReplyMode;
81
+ voicePromptContribution?: string;
82
+ admissionScope?: string;
83
+ admissionJournalBinding?: string;
84
+ }
85
+ export type BuildTelegramPromptTurnRuntimeOptions = Omit<BuildTelegramPromptTurnOptions, "readBinaryFile">;
86
+ export interface TelegramPromptTurnRuntimeBuilderDeps<TContext = unknown> extends DownloadTelegramMessageFilesDeps {
87
+ allocateQueueOrder: () => number;
88
+ processAttachments?: (files: DownloadedTelegramTurnFile[], rawText: string, ctx: TContext) => Promise<{
89
+ rawText: string;
90
+ promptFiles?: DownloadedTelegramTurnFile[];
91
+ handlerOutputs?: string[];
92
+ }>;
93
+ resolveTimeLine?: (chatId: number) => string | null;
94
+ getVoiceReplyMode?: () => TelegramVoiceReplyMode;
95
+ /** Returns the visible thread label for a message target, used to add thread context to the prompt prefix. */
96
+ getTelegramThreadLabel?: (message: {
97
+ chat: {
98
+ id: number;
99
+ };
100
+ message_thread_id?: number;
101
+ }) => string | undefined;
102
+ getAllowedUserId?: () => number | undefined;
103
+ getAdmissionScope?: () => string | undefined;
104
+ getAdmissionJournalBinding?: () => string | undefined;
105
+ assertExecutionCurrent?: (message: TelegramTurnMessage) => void;
106
+ }
107
+ export declare function createTelegramPromptTurnRuntimePreparer<TMessage extends TelegramTurnMessage & TelegramMediaMessage, TContext = unknown>(deps: TelegramPromptTurnRuntimeBuilderDeps<TContext>): (messages: TMessage[], ctx?: TContext) => Promise<TelegramPreparedPromptTurn>;
108
+ export declare function buildTelegramPromptTurn(options: BuildTelegramPromptTurnOptions): Promise<PendingTelegramTurn>;
109
+ export declare function buildTelegramPromptTurnRuntime(options: BuildTelegramPromptTurnRuntimeOptions): Promise<PendingTelegramTurn>;
@@ -0,0 +1,500 @@
1
+ /**
2
+ * Telegram turn-building helpers
3
+ * Zones: telegram inbound, pi agent prompt content, queue
4
+ * Owns prompt-turn summary and content construction so queued Telegram turns are assembled consistently
5
+ */
6
+ import { readFile } from "node:fs/promises";
7
+ import { basename, dirname, join } from "node:path";
8
+ import { buildTelegramReplyContextBlock, collectTelegramFileInfos, collectTelegramMessageIds, downloadTelegramMessageFiles, extractTelegramForwardContextText, extractTelegramMessagesPromptText, extractTelegramMessagesText, extractTelegramMessageText, formatTelegramHistoryText, guessMediaType, } from "./media.js";
9
+ import { createTelegramQueueAdmissionReceipt, truncateTelegramQueueSummary, } from "./queue.js";
10
+ import { computeVoicePromptContribution, computeVoiceTurnFlags, getTelegramVoiceReplyMode, } from "./voice.js";
11
+ export const TELEGRAM_PREFIX = "[telegram]";
12
+ export const TELEGRAM_GUEST_TURN_NOTE = "[guest] delivery: answer quickly with one concise, self-contained reply";
13
+ function getTelegramTurnTarget(message) {
14
+ return Number.isInteger(message.message_thread_id)
15
+ ? { chatId: message.chat.id, threadId: message.message_thread_id }
16
+ : { chatId: message.chat.id };
17
+ }
18
+ function formatTelegramPrefixAttributeValue(value) {
19
+ return value
20
+ .replace(/[\]\n\r|]+/g, " ")
21
+ .replace(/\s+/g, " ")
22
+ .trim();
23
+ }
24
+ export function createTelegramTurnPrefix(attributes = {}) {
25
+ const parts = [TELEGRAM_PREFIX.slice(1, -1)];
26
+ for (const [key, value] of Object.entries(attributes)) {
27
+ const normalized = value ? formatTelegramPrefixAttributeValue(value) : "";
28
+ if (normalized)
29
+ parts.push(`${key}:${normalized}`);
30
+ }
31
+ return `[${parts.join("|")}]`;
32
+ }
33
+ export function formatTelegramTurnPrefix(_message, basePrefix = TELEGRAM_PREFIX) {
34
+ return basePrefix;
35
+ }
36
+ export { truncateTelegramQueueSummary };
37
+ export function formatTelegramTurnStatusSummary(rawText, files, handlerOutputs = []) {
38
+ const textSummary = truncateTelegramQueueSummary(rawText);
39
+ if (textSummary)
40
+ return textSummary;
41
+ const handlerSummary = truncateTelegramQueueSummary(handlerOutputs.join(" "));
42
+ if (handlerSummary)
43
+ return handlerSummary;
44
+ if (files.length === 1) {
45
+ const fileName = basename(files[0]?.fileName || files[0]?.path || "attachment");
46
+ return `📎 ${truncateTelegramQueueSummary(fileName, 4, 32) || "attachment"}`;
47
+ }
48
+ if (files.length > 1)
49
+ return `📎 ${files.length} attachments`;
50
+ return "(empty message)";
51
+ }
52
+ function appendTelegramListSection(text, title, items) {
53
+ if (items.length === 0)
54
+ return text;
55
+ const prefix = text.length > 0 ? `${text}\n\n` : "";
56
+ return `${prefix}[${title}]\n${items.map((item) => `- ${item}`).join("\n")}`;
57
+ }
58
+ function appendTelegramAttachmentSection(text, files) {
59
+ if (files.length === 0)
60
+ return text;
61
+ const dirs = [...new Set(files.map((file) => dirname(file.path)))];
62
+ const sameDir = dirs.length === 1;
63
+ const header = sameDir ? `[attachments] ${dirs[0]}` : "[attachments]";
64
+ const items = sameDir
65
+ ? files.map((file) => `/${basename(file.path)}`)
66
+ : files.map((file) => file.path);
67
+ const prefix = text.length > 0 ? `${text}\n\n` : "";
68
+ return `${prefix}${header}\n${items.map((item) => `- ${item}`).join("\n")}`;
69
+ }
70
+ function appendTelegramSourceContext(text, sourceContext) {
71
+ if (!sourceContext)
72
+ return text;
73
+ return text ? `${text}\n\n${sourceContext}` : sourceContext;
74
+ }
75
+ function buildTelegramForwardContextBlock(options) {
76
+ const metadata = options.context.replace(/:\s+/g, ":");
77
+ const from = metadata.match(/^from:(.+)$/)?.[1];
78
+ let block = `[forward|${metadata}]${options.text ? ` ${options.text}` : ""}`;
79
+ if (options.files.length === 0)
80
+ return block;
81
+ const dirs = [...new Set(options.files.map((file) => dirname(file.path)))];
82
+ const sameDir = dirs.length === 1;
83
+ const header = `[attachments${from ? `|from:${from}` : ""}]${sameDir ? ` ${dirs[0]}` : ""}`;
84
+ const items = sameDir
85
+ ? options.files.map((file) => `/${basename(file.path)}`)
86
+ : options.files.map((file) => file.path);
87
+ block += `\n\n${header}\n${items.map((item) => `- ${item}`).join("\n")}`;
88
+ return block;
89
+ }
90
+ function appendTelegramPromptText(prompt, rawText) {
91
+ if (!rawText)
92
+ return prompt;
93
+ if (rawText.startsWith("\n"))
94
+ return `${prompt}${rawText}`;
95
+ return `${prompt} ${rawText}`;
96
+ }
97
+ function appendTelegramVoiceContext(prompt, entries) {
98
+ const prefix = prompt.length > 0 ? `${prompt}\n\n` : "";
99
+ const pairs = Object.entries(entries);
100
+ if (pairs.length === 1) {
101
+ const [key, value] = pairs[0];
102
+ return `${prefix}[voice] ${key}: ${value}`;
103
+ }
104
+ return `${prefix}[voice]\n${pairs
105
+ .map(([key, value]) => `- ${key}: ${value}`)
106
+ .join("\n")}`;
107
+ }
108
+ // --- Voice Policy And Tagging ---
109
+ export function buildTelegramTurnPrompt(options) {
110
+ let prompt = options.telegramPrefix;
111
+ if ((options.historyTurns?.length ?? 0) > 0) {
112
+ prompt +=
113
+ "\n\nEarlier Telegram messages arrived after an aborted turn. Treat them as prior user messages, in order:";
114
+ for (const [index, turn] of (options.historyTurns ?? []).entries()) {
115
+ prompt += `\n\n${index + 1}. ${turn.historyText}`;
116
+ }
117
+ prompt += "\n\nCurrent Telegram message:";
118
+ }
119
+ if (options.rawText.length > 0) {
120
+ prompt =
121
+ (options.historyTurns?.length ?? 0) > 0
122
+ ? `${prompt}\n${options.rawText}`
123
+ : appendTelegramPromptText(prompt, options.rawText);
124
+ }
125
+ const displayFiles = options.displayFiles ?? options.promptFiles ?? options.files;
126
+ prompt = appendTelegramAttachmentSection(prompt, displayFiles);
127
+ prompt = appendTelegramListSection(prompt, "outputs", options.handlerOutputs ?? []);
128
+ prompt = appendTelegramSourceContext(prompt, options.sourceContext);
129
+ if (options.voiceContext) {
130
+ prompt = appendTelegramVoiceContext(prompt, options.voiceContext);
131
+ }
132
+ if (options.timeLine) {
133
+ prompt = `${prompt}\n\n[time] ${options.timeLine}`;
134
+ }
135
+ if (options.guestTurn) {
136
+ prompt = `${prompt}\n\n${TELEGRAM_GUEST_TURN_NOTE}`;
137
+ }
138
+ return prompt;
139
+ }
140
+ function splitTelegramPromptAttachmentSuffix(prompt) {
141
+ const marker = "\n\n[attachments]";
142
+ const markerIndex = prompt.indexOf(marker);
143
+ if (markerIndex === -1) {
144
+ return {
145
+ promptWithoutAttachments: prompt,
146
+ attachmentSuffix: "",
147
+ attachmentFiles: [],
148
+ };
149
+ }
150
+ const promptWithoutAttachments = prompt.slice(0, markerIndex);
151
+ const attachmentSuffix = prompt.slice(markerIndex);
152
+ const attachmentLines = [];
153
+ let readingAttachments = false;
154
+ let attachmentDir;
155
+ for (const line of attachmentSuffix.split("\n")) {
156
+ const trimmed = line.trim();
157
+ const attachmentMatch = trimmed.match(/^\[attachments\](?:\s+(.+))?$/);
158
+ if (attachmentMatch) {
159
+ readingAttachments = true;
160
+ attachmentDir = attachmentMatch[1]?.trim();
161
+ continue;
162
+ }
163
+ if (readingAttachments && /^\[[^\]]+\](?:\s+.*)?$/.test(trimmed))
164
+ break;
165
+ if (readingAttachments)
166
+ attachmentLines.push(line);
167
+ }
168
+ const attachmentFiles = attachmentLines
169
+ .map((line) => line.match(/^- (.+)$/)?.[1]?.trim())
170
+ .filter((path) => !!path)
171
+ .map((path) => attachmentDir ? join(attachmentDir, path.replace(/^\/+/, "")) : path)
172
+ .map((path) => ({
173
+ path,
174
+ fileName: basename(path),
175
+ isImage: false,
176
+ kind: "document",
177
+ }));
178
+ return { promptWithoutAttachments, attachmentSuffix, attachmentFiles };
179
+ }
180
+ function buildEditedTelegramPromptText(options) {
181
+ const { promptWithoutAttachments, attachmentSuffix, attachmentFiles } = splitTelegramPromptAttachmentSuffix(options.existingPrompt);
182
+ const currentMessageMarker = "Current Telegram message:";
183
+ const currentMessageIndex = promptWithoutAttachments.lastIndexOf(currentMessageMarker);
184
+ if (currentMessageIndex !== -1) {
185
+ const prefix = promptWithoutAttachments.slice(0, currentMessageIndex + currentMessageMarker.length);
186
+ const separator = options.rawText.length > 0 ? "\n" : "";
187
+ return {
188
+ text: `${prefix}${separator}${options.rawText}${attachmentSuffix}`,
189
+ attachmentFiles,
190
+ };
191
+ }
192
+ return {
193
+ text: `${appendTelegramPromptText(options.telegramPrefix, options.rawText)}${attachmentSuffix}`,
194
+ attachmentFiles,
195
+ };
196
+ }
197
+ export function updateTelegramPromptTurnText(options) {
198
+ let attachmentFiles = [];
199
+ const nextContent = options.turn.content.map((block, index) => {
200
+ if (index !== 0 || block.type !== "text")
201
+ return block;
202
+ const updated = buildEditedTelegramPromptText({
203
+ existingPrompt: block.text,
204
+ telegramPrefix: options.telegramPrefix,
205
+ rawText: options.rawText,
206
+ });
207
+ attachmentFiles = updated.attachmentFiles;
208
+ return {
209
+ ...block,
210
+ text: updated.text,
211
+ };
212
+ });
213
+ return {
214
+ ...options.turn,
215
+ content: nextContent,
216
+ historyText: formatTelegramHistoryText(options.rawText, attachmentFiles),
217
+ statusSummary: formatTelegramTurnStatusSummary(options.statusText ?? options.rawText, attachmentFiles),
218
+ };
219
+ }
220
+ export function updateQueuedTelegramPromptTurnText(options) {
221
+ if (options.sourceMessageId === undefined) {
222
+ return { items: options.items, changed: false };
223
+ }
224
+ let changed = false;
225
+ const items = options.items.map((item) => {
226
+ if (item.kind !== "prompt" ||
227
+ !item.sourceMessageIds.includes(options.sourceMessageId)) {
228
+ return item;
229
+ }
230
+ changed = true;
231
+ return updateTelegramPromptTurnText({
232
+ turn: item,
233
+ telegramPrefix: options.telegramPrefix,
234
+ rawText: options.rawText,
235
+ statusText: options.statusText,
236
+ });
237
+ });
238
+ return { items, changed };
239
+ }
240
+ export function createTelegramQueuedPromptEditRuntime(deps) {
241
+ return {
242
+ updateFromEditedMessage: (message, ctx) => {
243
+ const { changed, items } = updateQueuedTelegramPromptTurnText({
244
+ items: deps.getQueuedItems(),
245
+ sourceMessageId: message.message_id,
246
+ telegramPrefix: TELEGRAM_PREFIX,
247
+ rawText: extractTelegramMessagesPromptText([message]),
248
+ statusText: extractTelegramMessagesText([message]),
249
+ });
250
+ deps.setQueuedItems(items);
251
+ if (changed)
252
+ deps.updateStatus(ctx);
253
+ return changed;
254
+ },
255
+ };
256
+ }
257
+ export function createTelegramPromptTurnRuntimePreparer(deps) {
258
+ return async (messages, ctx) => {
259
+ const rawText = extractTelegramMessagesText(messages);
260
+ const firstMessage = messages[0];
261
+ if (firstMessage)
262
+ deps.assertExecutionCurrent?.(firstMessage);
263
+ const replyFiles = firstMessage?.reply_to_message
264
+ ? await downloadTelegramMessageFiles([firstMessage.reply_to_message], { downloadFile: deps.downloadFile })
265
+ : [];
266
+ if (firstMessage)
267
+ deps.assertExecutionCurrent?.(firstMessage);
268
+ const processedReply = deps.processAttachments && replyFiles.length > 0
269
+ ? await deps.processAttachments(replyFiles, "", ctx)
270
+ : undefined;
271
+ if (firstMessage)
272
+ deps.assertExecutionCurrent?.(firstMessage);
273
+ const replyContext = firstMessage
274
+ ? buildTelegramReplyContextBlock(firstMessage, processedReply?.promptFiles ?? replyFiles, processedReply?.handlerOutputs)
275
+ : "";
276
+ const forwardEntries = messages.flatMap((message) => {
277
+ const context = extractTelegramForwardContextText(message, deps.getAllowedUserId?.());
278
+ return context
279
+ ? [
280
+ {
281
+ context,
282
+ text: extractTelegramMessageText(message),
283
+ message,
284
+ fileNames: new Set(collectTelegramFileInfos([message]).map((file) => file.fileName)),
285
+ },
286
+ ]
287
+ : [];
288
+ });
289
+ const files = await downloadTelegramMessageFiles(messages, {
290
+ downloadFile: deps.downloadFile,
291
+ });
292
+ if (firstMessage)
293
+ deps.assertExecutionCurrent?.(firstMessage);
294
+ const processed = deps.processAttachments
295
+ ? await deps.processAttachments(files, rawText, ctx)
296
+ : { rawText, promptFiles: files };
297
+ if (firstMessage)
298
+ deps.assertExecutionCurrent?.(firstMessage);
299
+ const sourceBlocks = [];
300
+ let promptRawText = processed.rawText;
301
+ const forwardedFilePaths = new Set();
302
+ const getForwardFiles = (entry) => (processed.promptFiles ?? files).filter((file) => {
303
+ if (!entry.fileNames.has(file.fileName))
304
+ return false;
305
+ forwardedFilePaths.add(file.path);
306
+ return true;
307
+ });
308
+ if (forwardEntries.length === 1 && messages.length === 1) {
309
+ const [forward] = forwardEntries;
310
+ sourceBlocks.push(buildTelegramForwardContextBlock({
311
+ context: forward.context,
312
+ text: processed.rawText,
313
+ files: getForwardFiles(forward),
314
+ }));
315
+ promptRawText = "";
316
+ }
317
+ else if (forwardEntries.length > 0 && processed.rawText === rawText) {
318
+ const forwardedMessages = new Set(forwardEntries.map((entry) => entry.message));
319
+ promptRawText = messages
320
+ .filter((message) => !forwardedMessages.has(message))
321
+ .map(extractTelegramMessageText)
322
+ .filter(Boolean)
323
+ .join("\n\n");
324
+ sourceBlocks.push(...forwardEntries.map((entry) => buildTelegramForwardContextBlock({
325
+ context: entry.context,
326
+ text: entry.text,
327
+ files: getForwardFiles(entry),
328
+ })));
329
+ }
330
+ else if (forwardEntries.length > 0) {
331
+ sourceBlocks.push(...forwardEntries.map((entry) => buildTelegramForwardContextBlock({
332
+ context: entry.context,
333
+ text: "",
334
+ files: getForwardFiles(entry),
335
+ })));
336
+ }
337
+ if (replyContext)
338
+ sourceBlocks.push(replyContext);
339
+ const sourceContext = sourceBlocks.join("\n\n");
340
+ // Compute voice mode once and pass it to both the turn builder and the prompt contribution helper
341
+ const voiceReplyMode = deps.getVoiceReplyMode?.();
342
+ const chatId = messages[0]?.chat.id;
343
+ const timeLine = deps.resolveTimeLine && chatId !== undefined
344
+ ? deps.resolveTimeLine(chatId)
345
+ : null;
346
+ const threadLabel = firstMessage
347
+ ? deps.getTelegramThreadLabel?.(firstMessage)
348
+ : undefined;
349
+ const telegramPrefix = createTelegramTurnPrefix({
350
+ thread: threadLabel,
351
+ "from-thread": firstMessage?.pi_telegram_agent_source_thread,
352
+ });
353
+ const buildTurn = await prepareTelegramPromptTurn({
354
+ telegramPrefix,
355
+ messages,
356
+ readBinaryFile: readFile,
357
+ rawText: promptRawText,
358
+ sourceContext,
359
+ statusText: processed.rawText,
360
+ files,
361
+ promptFiles: processed.promptFiles,
362
+ displayFiles: (processed.promptFiles ?? files).filter((file) => !forwardedFilePaths.has(file.path)),
363
+ handlerOutputs: processed.handlerOutputs,
364
+ timeLine,
365
+ inferImageMimeType: guessMediaType,
366
+ voiceReplyMode,
367
+ voicePromptContribution: computeVoicePromptContribution(voiceReplyMode, files, rawText),
368
+ admissionScope: deps.getAdmissionScope?.(),
369
+ admissionJournalBinding: deps.getAdmissionJournalBinding?.(),
370
+ });
371
+ return (historyTurns) => {
372
+ if (firstMessage)
373
+ deps.assertExecutionCurrent?.(firstMessage);
374
+ return buildTurn(deps.allocateQueueOrder(), historyTurns);
375
+ };
376
+ };
377
+ }
378
+ function getTelegramVoicePromptContext(voiceReplyMode, hasVoiceFile) {
379
+ if (voiceReplyMode !== "always" &&
380
+ !(voiceReplyMode === "mirror" && hasVoiceFile)) {
381
+ return undefined;
382
+ }
383
+ return { delivery: "automatic voice" };
384
+ }
385
+ function collectTelegramTurnAdmissionReceipts(messages, historyTurns, admissionScope, admissionJournalBinding) {
386
+ const receipts = new Map();
387
+ const addReceipt = (receipt) => {
388
+ const existing = receipts.get(receipt.receiptId);
389
+ if (existing) {
390
+ if (existing.queueKind !== receipt.queueKind ||
391
+ existing.journalBindingKey !== receipt.journalBindingKey ||
392
+ existing.sourceUpdateIds.length !== receipt.sourceUpdateIds.length ||
393
+ existing.sourceUpdateIds.some((updateId, index) => updateId !== receipt.sourceUpdateIds[index])) {
394
+ throw new Error(`Conflicting Telegram turn receipt: ${receipt.receiptId}`);
395
+ }
396
+ return;
397
+ }
398
+ receipts.set(receipt.receiptId, structuredClone(receipt));
399
+ };
400
+ for (const turn of historyTurns) {
401
+ for (const receipt of turn.admissionReceipts ?? [])
402
+ addReceipt(receipt);
403
+ }
404
+ const sourceUpdateIds = messages.flatMap((message) => typeof message.pi_telegram_source_update_id === "number"
405
+ ? [message.pi_telegram_source_update_id]
406
+ : []);
407
+ if (sourceUpdateIds.length > 0) {
408
+ const currentReceipt = createTelegramQueueAdmissionReceipt({
409
+ queueKind: "prompt",
410
+ scope: admissionScope ?? "",
411
+ sourceUpdateIds,
412
+ });
413
+ if (currentReceipt) {
414
+ addReceipt({
415
+ ...currentReceipt,
416
+ ...(admissionJournalBinding
417
+ ? { journalBindingKey: admissionJournalBinding }
418
+ : {}),
419
+ });
420
+ }
421
+ }
422
+ return [...receipts.values()];
423
+ }
424
+ async function prepareTelegramPromptTurn(options) {
425
+ const images = [];
426
+ for (const file of options.files) {
427
+ if (!file.isImage)
428
+ continue;
429
+ const mediaType = file.mimeType || options.inferImageMimeType(file.path);
430
+ if (!mediaType)
431
+ continue;
432
+ const buffer = await options.readBinaryFile(file.path);
433
+ images.push({
434
+ type: "image",
435
+ data: Buffer.from(buffer).toString("base64"),
436
+ mimeType: mediaType,
437
+ });
438
+ }
439
+ return (queueOrder, historyTurns) => buildPreparedTelegramPromptTurn({ ...options, queueOrder, historyTurns }, images);
440
+ }
441
+ function buildPreparedTelegramPromptTurn(options, images) {
442
+ const firstMessage = options.messages[0];
443
+ if (!firstMessage) {
444
+ throw new Error("Missing Telegram message for turn creation");
445
+ }
446
+ const hasVoiceFile = options.files.some((f) => f.kind === "voice" || f.kind === "audio");
447
+ const voiceReplyMode = options.voiceReplyMode ?? getTelegramVoiceReplyMode();
448
+ const content = [
449
+ {
450
+ type: "text",
451
+ text: buildTelegramTurnPrompt({
452
+ telegramPrefix: formatTelegramTurnPrefix(firstMessage, options.telegramPrefix),
453
+ rawText: options.rawText,
454
+ files: options.files,
455
+ promptFiles: options.promptFiles,
456
+ displayFiles: options.displayFiles,
457
+ handlerOutputs: options.handlerOutputs,
458
+ sourceContext: options.sourceContext,
459
+ historyTurns: options.historyTurns,
460
+ timeLine: options.timeLine,
461
+ voiceContext: getTelegramVoicePromptContext(voiceReplyMode, hasVoiceFile),
462
+ }),
463
+ },
464
+ ...images,
465
+ ];
466
+ if (options.voicePromptContribution?.trim()) {
467
+ const textItem = content.find((c) => c.type === "text");
468
+ if (textItem) {
469
+ textItem.text = `${textItem.text}\n\n${options.voicePromptContribution.trim()}`;
470
+ }
471
+ }
472
+ const admissionReceipts = collectTelegramTurnAdmissionReceipts(options.messages, options.historyTurns ?? [], options.admissionScope, options.admissionJournalBinding);
473
+ return {
474
+ kind: "prompt",
475
+ chatId: firstMessage.chat.id,
476
+ target: getTelegramTurnTarget(firstMessage),
477
+ replyToMessageId: firstMessage.message_id,
478
+ sourceMessageIds: collectTelegramMessageIds(options.messages),
479
+ queueOrder: options.queueOrder,
480
+ queueLane: "default",
481
+ laneOrder: options.queueOrder,
482
+ queuedAttachments: [],
483
+ content,
484
+ historyText: appendTelegramSourceContext(formatTelegramHistoryText(options.rawText, options.displayFiles ?? options.promptFiles ?? options.files, options.handlerOutputs), options.sourceContext),
485
+ statusSummary: formatTelegramTurnStatusSummary(options.statusText ?? options.rawText, options.displayFiles ?? options.promptFiles ?? options.files, options.handlerOutputs),
486
+ ...(admissionReceipts.length > 0 ? { admissionReceipts } : {}),
487
+ // Voice tagging (used for preview suppression and prompt guidance)
488
+ ...computeVoiceTurnFlags(voiceReplyMode, hasVoiceFile),
489
+ };
490
+ }
491
+ export async function buildTelegramPromptTurn(options) {
492
+ const buildTurn = await prepareTelegramPromptTurn(options);
493
+ return buildTurn(options.queueOrder, options.historyTurns ?? []);
494
+ }
495
+ export async function buildTelegramPromptTurnRuntime(options) {
496
+ return buildTelegramPromptTurn({
497
+ ...options,
498
+ readBinaryFile: readFile,
499
+ });
500
+ }