@llblab/pi-telegram 0.46.0 → 0.48.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/AGENTS.md +4 -2
  2. package/BACKLOG.md +0 -6
  3. package/CHANGELOG.md +14 -0
  4. package/README.md +5 -2
  5. package/dist/api/activity.d.ts +6 -0
  6. package/dist/api/activity.js +6 -0
  7. package/dist/api/commands.d.ts +6 -0
  8. package/dist/api/commands.js +6 -0
  9. package/dist/api/delivery.d.ts +6 -0
  10. package/dist/api/delivery.js +6 -0
  11. package/dist/api/inbound.d.ts +6 -0
  12. package/dist/api/inbound.js +6 -0
  13. package/dist/api/keyboard.d.ts +6 -0
  14. package/dist/api/keyboard.js +6 -0
  15. package/dist/api/outbound.d.ts +6 -0
  16. package/dist/api/outbound.js +6 -0
  17. package/dist/api/sections.d.ts +7 -0
  18. package/dist/api/sections.js +6 -0
  19. package/dist/api/status.d.ts +6 -0
  20. package/dist/api/status.js +6 -0
  21. package/dist/api/updates.d.ts +6 -0
  22. package/dist/api/updates.js +6 -0
  23. package/dist/api/voice.d.ts +6 -0
  24. package/dist/api/voice.js +6 -0
  25. package/dist/index.d.ts +6 -0
  26. package/dist/index.js +6 -0
  27. package/dist/lib/activity-verbosity.d.ts +52 -0
  28. package/dist/lib/activity-verbosity.js +596 -0
  29. package/dist/lib/activity.d.ts +220 -0
  30. package/dist/lib/activity.js +574 -0
  31. package/dist/lib/agent-messages.d.ts +28 -0
  32. package/dist/lib/agent-messages.js +86 -0
  33. package/dist/lib/bindings.d.ts +250 -0
  34. package/dist/lib/bindings.js +936 -0
  35. package/dist/lib/bus-api.d.ts +18 -0
  36. package/dist/lib/bus-api.js +254 -0
  37. package/dist/lib/bus-follower.d.ts +464 -0
  38. package/dist/lib/bus-follower.js +1686 -0
  39. package/dist/lib/bus-leader.d.ts +292 -0
  40. package/dist/lib/bus-leader.js +2242 -0
  41. package/dist/lib/bus-transport.d.ts +64 -0
  42. package/dist/lib/bus-transport.js +140 -0
  43. package/dist/lib/bus.d.ts +518 -0
  44. package/dist/lib/bus.js +2055 -0
  45. package/dist/lib/channel-posts.d.ts +170 -0
  46. package/dist/lib/channel-posts.js +611 -0
  47. package/dist/lib/command-templates.d.ts +70 -0
  48. package/dist/lib/command-templates.js +744 -0
  49. package/dist/lib/commands.d.ts +541 -0
  50. package/dist/lib/commands.js +1155 -0
  51. package/dist/lib/config.d.ts +252 -0
  52. package/dist/lib/config.js +889 -0
  53. package/dist/lib/delivery.d.ts +163 -0
  54. package/dist/lib/delivery.js +542 -0
  55. package/dist/lib/extension.d.ts +7 -0
  56. package/dist/lib/extension.js +1604 -0
  57. package/dist/lib/generative-app-worker.mjs +104 -0
  58. package/dist/lib/generative-apps.d.ts +212 -0
  59. package/dist/lib/generative-apps.js +1006 -0
  60. package/dist/lib/inbound.d.ts +91 -0
  61. package/dist/lib/inbound.js +502 -0
  62. package/dist/lib/journal.d.ts +665 -0
  63. package/dist/lib/journal.js +3701 -0
  64. package/dist/lib/keyboard.d.ts +23 -0
  65. package/dist/lib/keyboard.js +37 -0
  66. package/dist/lib/lifecycle.d.ts +157 -0
  67. package/dist/lib/lifecycle.js +395 -0
  68. package/dist/lib/locks.d.ts +164 -0
  69. package/dist/lib/locks.js +1208 -0
  70. package/dist/lib/logging.d.ts +50 -0
  71. package/dist/lib/logging.js +274 -0
  72. package/dist/lib/media.d.ts +181 -0
  73. package/dist/lib/media.js +602 -0
  74. package/dist/lib/menu-model.d.ts +217 -0
  75. package/dist/lib/menu-model.js +663 -0
  76. package/dist/lib/menu-queue.d.ts +37 -0
  77. package/dist/lib/menu-queue.js +408 -0
  78. package/dist/lib/menu-settings.d.ts +115 -0
  79. package/dist/lib/menu-settings.js +595 -0
  80. package/dist/lib/menu-status.d.ts +32 -0
  81. package/dist/lib/menu-status.js +112 -0
  82. package/dist/lib/menu-thinking.d.ts +29 -0
  83. package/dist/lib/menu-thinking.js +82 -0
  84. package/dist/lib/menu.d.ts +171 -0
  85. package/dist/lib/menu.js +323 -0
  86. package/dist/lib/model.d.ts +127 -0
  87. package/dist/lib/model.js +409 -0
  88. package/dist/lib/outbound-attachments.d.ts +241 -0
  89. package/dist/lib/outbound-attachments.js +639 -0
  90. package/dist/lib/outbound-buttons.d.ts +69 -0
  91. package/dist/lib/outbound-buttons.js +248 -0
  92. package/dist/lib/outbound-markup.d.ts +42 -0
  93. package/dist/lib/outbound-markup.js +678 -0
  94. package/dist/lib/outbound-voice.d.ts +55 -0
  95. package/dist/lib/outbound-voice.js +152 -0
  96. package/dist/lib/outbound.d.ts +185 -0
  97. package/dist/lib/outbound.js +516 -0
  98. package/dist/lib/ownership.d.ts +77 -0
  99. package/dist/lib/ownership.js +174 -0
  100. package/dist/lib/paths.d.ts +40 -0
  101. package/dist/lib/paths.js +94 -0
  102. package/dist/lib/pi.d.ts +72 -0
  103. package/dist/lib/pi.js +121 -0
  104. package/dist/lib/polling.d.ts +351 -0
  105. package/dist/lib/polling.js +1196 -0
  106. package/dist/lib/preview.d.ts +192 -0
  107. package/dist/lib/preview.js +614 -0
  108. package/dist/lib/prompt-templates.d.ts +24 -0
  109. package/dist/lib/prompt-templates.js +118 -0
  110. package/dist/lib/prompts.d.ts +57 -0
  111. package/dist/lib/prompts.js +167 -0
  112. package/dist/lib/queue.d.ts +766 -0
  113. package/dist/lib/queue.js +1965 -0
  114. package/dist/lib/recovery.d.ts +86 -0
  115. package/dist/lib/recovery.js +285 -0
  116. package/dist/lib/rendering.d.ts +20 -0
  117. package/dist/lib/rendering.js +983 -0
  118. package/dist/lib/replies.d.ts +214 -0
  119. package/dist/lib/replies.js +737 -0
  120. package/dist/lib/routing.d.ts +207 -0
  121. package/dist/lib/routing.js +2282 -0
  122. package/dist/lib/runtime.d.ts +172 -0
  123. package/dist/lib/runtime.js +402 -0
  124. package/dist/lib/sections.d.ts +165 -0
  125. package/dist/lib/sections.js +327 -0
  126. package/dist/lib/setup.d.ts +82 -0
  127. package/dist/lib/setup.js +150 -0
  128. package/dist/lib/skills.d.ts +8 -0
  129. package/dist/lib/skills.js +12 -0
  130. package/dist/lib/status.d.ts +442 -0
  131. package/dist/lib/status.js +1008 -0
  132. package/dist/lib/sync.d.ts +179 -0
  133. package/dist/lib/sync.js +782 -0
  134. package/dist/lib/target.d.ts +20 -0
  135. package/dist/lib/target.js +27 -0
  136. package/dist/lib/telegram-api.d.ts +541 -0
  137. package/dist/lib/telegram-api.js +1159 -0
  138. package/dist/lib/text-groups.d.ts +89 -0
  139. package/dist/lib/text-groups.js +317 -0
  140. package/dist/lib/thread-cleanup-manager.d.ts +288 -0
  141. package/dist/lib/thread-cleanup-manager.js +560 -0
  142. package/dist/lib/thread-display.d.ts +46 -0
  143. package/dist/lib/thread-display.js +257 -0
  144. package/dist/lib/thread-naming.d.ts +46 -0
  145. package/dist/lib/thread-naming.js +78 -0
  146. package/dist/lib/thread-reconciler.d.ts +239 -0
  147. package/dist/lib/thread-reconciler.js +644 -0
  148. package/dist/lib/threads.d.ts +621 -0
  149. package/dist/lib/threads.js +3679 -0
  150. package/dist/lib/time-injection.d.ts +15 -0
  151. package/dist/lib/time-injection.js +56 -0
  152. package/dist/lib/turns.d.ts +109 -0
  153. package/dist/lib/turns.js +500 -0
  154. package/dist/lib/updates.d.ts +1290 -0
  155. package/dist/lib/updates.js +3634 -0
  156. package/dist/lib/voice.d.ts +117 -0
  157. package/dist/lib/voice.js +174 -0
  158. package/dist/lib/workspace-admission.d.ts +264 -0
  159. package/dist/lib/workspace-admission.js +1136 -0
  160. package/dist/lib/workspace-retirement.d.ts +219 -0
  161. package/dist/lib/workspace-retirement.js +587 -0
  162. package/dist/lib/workspace-slots.d.ts +30 -0
  163. package/dist/lib/workspace-slots.js +54 -0
  164. package/dist/package.json +126 -0
  165. package/dist/pi-telegram/index.js +1 -0
  166. package/dist/skills/generated-control-surface/SKILL.md +107 -0
  167. package/dist/skills/generated-control-surface/references/capability-adapters.md +27 -0
  168. package/dist/skills/generated-control-surface/references/layout-and-state.md +37 -0
  169. package/dist/skills/generative-apps/SKILL.md +115 -0
  170. package/dist/skills/show-me/SKILL.md +166 -0
  171. package/dist/skills/show-me/references/telegram-surfaces.md +43 -0
  172. package/dist/skills/telegram-bridge/SKILL.md +129 -0
  173. package/dist/skills/telegram-bridge/references/configuration.md +15 -0
  174. package/dist/skills/telegram-bridge/references/delivery-and-threads.md +27 -0
  175. package/dist/skills/telegram-bridge/references/diagnosis.md +18 -0
  176. package/docs/README.md +2 -0
  177. package/docs/architecture.md +1 -1
  178. package/docs/compact-matrix-literal.md +10 -3
  179. package/docs/generative-apps.md +15 -13
  180. package/docs/multi-instance-bus.md +6 -4
  181. package/docs/outbound.md +1 -1
  182. package/docs/public-api.md +3 -3
  183. package/lib/bindings.ts +89 -6
  184. package/lib/bus-follower.ts +7 -3
  185. package/lib/bus-leader.ts +44 -19
  186. package/lib/bus.ts +4 -1
  187. package/lib/commands.ts +2 -2
  188. package/lib/config.ts +14 -7
  189. package/lib/delivery.ts +38 -6
  190. package/lib/extension.ts +7 -0
  191. package/lib/generative-apps.ts +379 -13
  192. package/lib/menu-settings.ts +20 -6
  193. package/lib/outbound-markup.ts +16 -7
  194. package/lib/status.ts +15 -12
  195. package/lib/telegram-api.ts +12 -1
  196. package/lib/thread-display.ts +96 -18
  197. package/package.json +56 -13
  198. package/scripts/build-dist.mjs +44 -0
  199. package/scripts/measure-bus.mjs +8 -1
  200. package/scripts/measure-workspace.mjs +8 -1
  201. package/skills/generative-apps/SKILL.md +1 -1
  202. package/skills/show-me/SKILL.md +1 -1
  203. package/skills/show-me/references/telegram-surfaces.md +1 -1
  204. package/skills/telegram-bridge/SKILL.md +1 -1
@@ -0,0 +1,889 @@
1
+ /**
2
+ * Telegram bridge config and pairing helpers
3
+ * Zones: telegram config, pairing, filesystem
4
+ * Owns persisted bot/session pairing state, local config storage, live config controls, authorization policy, and first-user pairing side effects
5
+ */
6
+ import { createHash, randomUUID } from "node:crypto";
7
+ import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
8
+ import { chmod, mkdir, rename, writeFile } from "node:fs/promises";
9
+ import { resolveAgentDir, resolveTelegramConfigPath, TELEGRAM_DEFAULT_PROFILE_NAME, } from "./paths.js";
10
+ export { TELEGRAM_DEFAULT_PROFILE_NAME } from "./paths.js";
11
+ import { withTelegramFileTransaction } from "./locks.js";
12
+ const CONFIG_RUNTIME_KEY = "__piTelegramConfigRuntime__";
13
+ const CONFIG_REPLACE_RETRY_ATTEMPTS = 5;
14
+ const CONFIG_REPLACE_RETRY_DELAY_MS = 25;
15
+ function isRetryableConfigReplaceError(error) {
16
+ const code = error?.code;
17
+ return code === "EPERM" || code === "EACCES" || code === "EBUSY";
18
+ }
19
+ function sleepConfigReplaceRetry(ms) {
20
+ if (ms <= 0)
21
+ return;
22
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
23
+ }
24
+ function replaceTelegramConfigFile(tempPath, configPath) {
25
+ for (let attempt = 0; attempt < CONFIG_REPLACE_RETRY_ATTEMPTS; attempt += 1) {
26
+ try {
27
+ renameSync(tempPath, configPath);
28
+ return;
29
+ }
30
+ catch (error) {
31
+ if (!isRetryableConfigReplaceError(error) ||
32
+ attempt === CONFIG_REPLACE_RETRY_ATTEMPTS - 1) {
33
+ throw error;
34
+ }
35
+ sleepConfigReplaceRetry(CONFIG_REPLACE_RETRY_DELAY_MS * (attempt + 1));
36
+ }
37
+ }
38
+ }
39
+ function getConfigPath() {
40
+ return resolveTelegramConfigPath();
41
+ }
42
+ const TELEGRAM_BOT_TOKEN_ENV_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
43
+ /**
44
+ * Parse a persisted bot-token value. `$NAME` and `${NAME}` are exact
45
+ * environment-variable references. Any other `$`-prefixed value is malformed
46
+ * rather than a literal secret so a broken reference fails closed.
47
+ */
48
+ export function getTelegramBotTokenReference(value) {
49
+ const trimmed = value?.trim();
50
+ if (!trimmed)
51
+ return undefined;
52
+ if (!trimmed.startsWith("$"))
53
+ return { kind: "literal", token: trimmed };
54
+ const body = trimmed.startsWith("${") && trimmed.endsWith("}")
55
+ ? trimmed.slice(2, -1)
56
+ : trimmed.slice(1);
57
+ return TELEGRAM_BOT_TOKEN_ENV_NAME_PATTERN.test(body)
58
+ ? { kind: "environment", variable: body }
59
+ : { kind: "malformed" };
60
+ }
61
+ /** Resolve a persisted token at a validation/activation boundary. */
62
+ export function resolveTelegramBotToken(value, env = process.env) {
63
+ const reference = getTelegramBotTokenReference(value);
64
+ if (reference?.kind === "literal")
65
+ return reference.token;
66
+ if (reference?.kind !== "environment")
67
+ return undefined;
68
+ return env[reference.variable]?.trim() || undefined;
69
+ }
70
+ /** Redacted diagnostic for an unresolved or malformed token reference. */
71
+ export function getTelegramBotTokenDiagnostic(value, env = process.env) {
72
+ const reference = getTelegramBotTokenReference(value);
73
+ if (reference?.kind === "malformed") {
74
+ return "Telegram bot token environment reference is malformed; use $NAME or ${NAME}.";
75
+ }
76
+ if (reference?.kind !== "environment")
77
+ return undefined;
78
+ if (resolveTelegramBotToken(value, env))
79
+ return undefined;
80
+ return `Telegram bot token environment variable ${reference.variable} is not set.`;
81
+ }
82
+ const TELEGRAM_THREAD_DISPLAY_MODES = [
83
+ "letters", "names", "directory-snake", "directory-title",
84
+ ];
85
+ export function resolveTelegramThreadDisplayMode(config) {
86
+ return TELEGRAM_THREAD_DISPLAY_MODES.includes(config.threadDisplayMode)
87
+ ? config.threadDisplayMode
88
+ : "letters";
89
+ }
90
+ export async function setTelegramThreadDisplayMode(store, mode, isCurrent) {
91
+ if (!TELEGRAM_THREAD_DISPLAY_MODES.includes(mode)) {
92
+ throw new Error("Invalid Telegram Thread display mode.");
93
+ }
94
+ const profile = store.getActiveProfileName();
95
+ const current = () => isCurrent() && store.getActiveProfileName() === profile;
96
+ if (!current())
97
+ throw new Error("Telegram Thread display setting lost authority.");
98
+ await store.load();
99
+ if (!current() || !store.hasBotToken()) {
100
+ throw new Error("Telegram Thread display setting lost its configured profile.");
101
+ }
102
+ await store.persist({ ...store.get(), threadDisplayMode: mode }, { isCurrent: current });
103
+ if (!current())
104
+ throw new Error("Telegram Thread display setting changed during persistence.");
105
+ }
106
+ /** Profile names must contain only lowercase ASCII letters and digits; max 32 chars. */
107
+ const TELEGRAM_PROFILE_NAME_PATTERN = /^[a-z0-9]{1,32}$/;
108
+ const TELEGRAM_RESERVED_PROFILE_NAMES = new Set([
109
+ "main",
110
+ "active",
111
+ ]);
112
+ export function isValidTelegramProfileName(name) {
113
+ return (TELEGRAM_PROFILE_NAME_PATTERN.test(name) &&
114
+ !TELEGRAM_RESERVED_PROFILE_NAMES.has(name));
115
+ }
116
+ /** List defined profile names. */
117
+ export function getTelegramProfileNames(config) {
118
+ return Object.keys(config.profiles ?? {}).sort();
119
+ }
120
+ export function createTelegramConfigBotIdGetter(store) {
121
+ return () => store.get().botId;
122
+ }
123
+ export function createTelegramActiveProfileKeyGetter(store) {
124
+ return () => store.getActiveProfileName() ?? TELEGRAM_DEFAULT_PROFILE_NAME;
125
+ }
126
+ export function setGlobalTelegramConfigRuntime(runtime) {
127
+ const globals = globalThis;
128
+ if (runtime)
129
+ globals[CONFIG_RUNTIME_KEY] = runtime;
130
+ else
131
+ delete globals[CONFIG_RUNTIME_KEY];
132
+ }
133
+ export function updateTelegramVoiceConfig(voice) {
134
+ const runtime = globalThis[CONFIG_RUNTIME_KEY];
135
+ if (!runtime || typeof runtime.updateVoiceConfig !== "function")
136
+ return false;
137
+ runtime.updateVoiceConfig(voice);
138
+ return true;
139
+ }
140
+ function isEmptyTelegramConfig(config) {
141
+ return Object.keys(config).length === 0;
142
+ }
143
+ async function loadLatestTelegramConfig(configStore) {
144
+ if (!configStore.load)
145
+ return;
146
+ const before = configStore.get();
147
+ await configStore.load();
148
+ if (!isEmptyTelegramConfig(before) &&
149
+ isEmptyTelegramConfig(configStore.get())) {
150
+ configStore.set(before);
151
+ }
152
+ }
153
+ export function bindGlobalTelegramConfigRuntime(configStore) {
154
+ setGlobalTelegramConfigRuntime({
155
+ updateVoiceConfig(voice) {
156
+ const current = configStore.get();
157
+ const next = {
158
+ ...current,
159
+ voice: { ...(current.voice ?? {}), ...voice },
160
+ };
161
+ configStore.set(next);
162
+ void configStore.persist(next);
163
+ },
164
+ });
165
+ }
166
+ function getInvalidTelegramConfigRecoveryPath(configPath) {
167
+ return `${configPath}.invalid-${process.pid}-${Date.now()}`;
168
+ }
169
+ export async function readTelegramConfig(configPath, options = {}) {
170
+ if (!existsSync(configPath))
171
+ return {};
172
+ const content = readFileSync(configPath, "utf8");
173
+ try {
174
+ return JSON.parse(content);
175
+ }
176
+ catch {
177
+ // Atomic config publication makes ordinary reads safe without serialization.
178
+ // Acquire the transaction only before destructive invalid-file recovery.
179
+ return withTelegramFileTransaction(`${configPath}.transaction`, () => {
180
+ if (!existsSync(configPath))
181
+ return {};
182
+ const identity = statSync(configPath);
183
+ const currentContent = readFileSync(configPath, "utf8");
184
+ try {
185
+ return JSON.parse(currentContent);
186
+ }
187
+ catch (error) {
188
+ const currentIdentity = statSync(configPath);
189
+ if (currentIdentity.dev !== identity.dev ||
190
+ currentIdentity.ino !== identity.ino ||
191
+ currentIdentity.size !== identity.size ||
192
+ currentIdentity.mtimeMs !== identity.mtimeMs) {
193
+ throw new Error(`Telegram config changed while validating invalid content: ${configPath}`, { cause: error });
194
+ }
195
+ const recoveryPath = getInvalidTelegramConfigRecoveryPath(configPath);
196
+ renameSync(configPath, recoveryPath);
197
+ options.onInvalidConfig?.({ configPath, recoveryPath, error });
198
+ return {};
199
+ }
200
+ });
201
+ }
202
+ }
203
+ export async function writeTelegramConfig(agentDir, configPath, config) {
204
+ await mkdir(agentDir, { recursive: true });
205
+ const tempConfigPath = `${configPath}.tmp-${process.pid}-${Date.now()}`;
206
+ await writeFile(tempConfigPath, JSON.stringify(config, null, "\t") + "\n", {
207
+ encoding: "utf8",
208
+ mode: 0o600,
209
+ });
210
+ await chmod(tempConfigPath, 0o600);
211
+ await rename(tempConfigPath, configPath);
212
+ await chmod(configPath, 0o600);
213
+ }
214
+ function isPlainConfigRecord(value) {
215
+ return !!value && typeof value === "object" && !Array.isArray(value);
216
+ }
217
+ function cloneTelegramConfig(value) {
218
+ return structuredClone(value);
219
+ }
220
+ function configValuesEqual(left, right) {
221
+ return JSON.stringify(left) === JSON.stringify(right);
222
+ }
223
+ function mergeTelegramConfigDelta(base, desired, latest) {
224
+ const merged = cloneTelegramConfig(latest);
225
+ for (const key of new Set([...Object.keys(base), ...Object.keys(desired)])) {
226
+ const baseHas = Object.hasOwn(base, key);
227
+ const desiredHas = Object.hasOwn(desired, key);
228
+ const baseValue = base[key];
229
+ const desiredValue = desired[key];
230
+ if (baseHas === desiredHas && configValuesEqual(baseValue, desiredValue)) {
231
+ continue;
232
+ }
233
+ if (!desiredHas) {
234
+ delete merged[key];
235
+ continue;
236
+ }
237
+ if (isPlainConfigRecord(desiredValue) &&
238
+ (!baseHas || isPlainConfigRecord(baseValue))) {
239
+ merged[key] = mergeTelegramConfigDelta(isPlainConfigRecord(baseValue) ? baseValue : {}, desiredValue, isPlainConfigRecord(merged[key]) ? merged[key] : {});
240
+ continue;
241
+ }
242
+ merged[key] = cloneTelegramConfig(desiredValue);
243
+ }
244
+ return merged;
245
+ }
246
+ function readTelegramConfigForTransaction(configPath) {
247
+ if (!existsSync(configPath))
248
+ return {};
249
+ const parsed = JSON.parse(readFileSync(configPath, "utf8"));
250
+ if (!isPlainConfigRecord(parsed)) {
251
+ throw new Error(`Invalid Telegram config object: ${configPath}`);
252
+ }
253
+ return parsed;
254
+ }
255
+ function writeTelegramConfigInTransaction(agentDir, configPath, config) {
256
+ mkdirSync(agentDir, { recursive: true, mode: 0o700 });
257
+ const tempConfigPath = `${configPath}.tmp-${process.pid}-${randomUUID()}`;
258
+ writeFileSync(tempConfigPath, `${JSON.stringify(config, null, "\t")}\n`, {
259
+ encoding: "utf8",
260
+ mode: 0o600,
261
+ });
262
+ chmodSync(tempConfigPath, 0o600);
263
+ try {
264
+ replaceTelegramConfigFile(tempConfigPath, configPath);
265
+ chmodSync(configPath, 0o600);
266
+ }
267
+ finally {
268
+ try {
269
+ unlinkSync(tempConfigPath);
270
+ }
271
+ catch {
272
+ /* rename consumed the temp file or cleanup is best effort */
273
+ }
274
+ }
275
+ }
276
+ export function getTelegramProfileFields(config) {
277
+ const token = config.botToken?.trim();
278
+ if (!token)
279
+ return undefined;
280
+ const legacyCursor = config
281
+ .lastUpdateId;
282
+ return {
283
+ botToken: token,
284
+ ...(config.botUsername !== undefined
285
+ ? { botUsername: config.botUsername }
286
+ : {}),
287
+ ...(config.botId !== undefined ? { botId: config.botId } : {}),
288
+ ...(config.allowedUserId !== undefined
289
+ ? { allowedUserId: config.allowedUserId }
290
+ : {}),
291
+ ...(config.threadDisplayMode !== undefined
292
+ ? { threadDisplayMode: config.threadDisplayMode }
293
+ : {}),
294
+ ...(legacyCursor !== undefined ? { lastUpdateId: legacyCursor } : {}),
295
+ };
296
+ }
297
+ function omitTelegramRootProfileFields(config) {
298
+ const { botToken: _botToken, botUsername: _botUsername, botId: _botId, allowedUserId: _allowedUserId, threadDisplayMode: _threadDisplayMode, lastUpdateId: _lastUpdateId, ...sharedConfig } = config;
299
+ return sharedConfig;
300
+ }
301
+ function omitRetiredProactivePush(config) {
302
+ const assistant = config.assistant;
303
+ if (!assistant || !Object.hasOwn(assistant, "proactivePush")) {
304
+ return { config, changed: false };
305
+ }
306
+ const { proactivePush: _proactivePush, ...remainingAssistant } = assistant;
307
+ const next = { ...config };
308
+ if (Object.keys(remainingAssistant).length > 0) {
309
+ next.assistant = remainingAssistant;
310
+ }
311
+ else {
312
+ delete next.assistant;
313
+ }
314
+ return { config: next, changed: true };
315
+ }
316
+ export function normalizeTelegramDefaultProfileConfig(config) {
317
+ const retiredProactivePush = omitRetiredProactivePush(config);
318
+ config = retiredProactivePush.config;
319
+ const hasLegacyRootProfile = [
320
+ "botToken",
321
+ "botUsername",
322
+ "botId",
323
+ "allowedUserId",
324
+ "threadDisplayMode",
325
+ "lastUpdateId",
326
+ ].some((field) => Object.hasOwn(config, field));
327
+ if (!hasLegacyRootProfile) {
328
+ return { config, changed: retiredProactivePush.changed };
329
+ }
330
+ const canonicalProfile = config.profiles?.[TELEGRAM_DEFAULT_PROFILE_NAME];
331
+ const legacyToken = config.botToken?.trim();
332
+ if (Object.hasOwn(config, "botToken") && !legacyToken) {
333
+ throw new Error("Legacy Telegram default profile has no bot token");
334
+ }
335
+ const legacyProfile = {
336
+ ...(legacyToken ? { botToken: legacyToken } : {}),
337
+ ...(config.botUsername !== undefined
338
+ ? { botUsername: config.botUsername }
339
+ : {}),
340
+ ...(config.botId !== undefined ? { botId: config.botId } : {}),
341
+ ...(config.allowedUserId !== undefined
342
+ ? { allowedUserId: config.allowedUserId }
343
+ : {}),
344
+ ...(config.threadDisplayMode !== undefined
345
+ ? { threadDisplayMode: config.threadDisplayMode }
346
+ : {}),
347
+ ...(config
348
+ .lastUpdateId !== undefined
349
+ ? {
350
+ lastUpdateId: config
351
+ .lastUpdateId,
352
+ }
353
+ : {}),
354
+ };
355
+ if (!canonicalProfile && !legacyToken) {
356
+ throw new Error("Legacy Telegram default profile has no bot token");
357
+ }
358
+ const hasConflict = canonicalProfile
359
+ ? Object.entries(legacyProfile).some(([field, value]) => Object.hasOwn(canonicalProfile, field) &&
360
+ !configValuesEqual(canonicalProfile[field], value))
361
+ : false;
362
+ if (hasConflict) {
363
+ throw new Error("Conflicting Telegram default profile identity at root and profiles.default");
364
+ }
365
+ const normalizedProfile = canonicalProfile
366
+ ? { ...legacyProfile, ...canonicalProfile }
367
+ : legacyProfile;
368
+ return {
369
+ config: {
370
+ ...omitTelegramRootProfileFields(config),
371
+ profiles: {
372
+ ...(config.profiles ?? {}),
373
+ [TELEGRAM_DEFAULT_PROFILE_NAME]: normalizedProfile,
374
+ },
375
+ },
376
+ changed: true,
377
+ };
378
+ }
379
+ function applyTelegramProfile(config, profileName) {
380
+ const effectiveProfileName = profileName ?? TELEGRAM_DEFAULT_PROFILE_NAME;
381
+ const profile = config.profiles?.[effectiveProfileName];
382
+ if (!profile)
383
+ return omitTelegramRootProfileFields(config);
384
+ return {
385
+ ...omitTelegramRootProfileFields(config),
386
+ ...profile,
387
+ };
388
+ }
389
+ function storeTelegramEffectiveConfig(baseConfig, nextConfig, profileName) {
390
+ const effectiveProfileName = profileName ?? TELEGRAM_DEFAULT_PROFILE_NAME;
391
+ const profile = getTelegramProfileFields(nextConfig);
392
+ const profiles = { ...(baseConfig.profiles ?? {}) };
393
+ if (profile)
394
+ profiles[effectiveProfileName] = profile;
395
+ else
396
+ delete profiles[effectiveProfileName];
397
+ return {
398
+ ...omitTelegramRootProfileFields(nextConfig),
399
+ profiles: Object.keys(profiles).length > 0 ? profiles : undefined,
400
+ };
401
+ }
402
+ export function createTelegramConfigStore(options = {}) {
403
+ let config = normalizeTelegramDefaultProfileConfig(cloneTelegramConfig(options.initialConfig ?? {})).config;
404
+ let persistedConfig = {};
405
+ let mutationVersion = 0;
406
+ let persistQueue = Promise.resolve();
407
+ let activeProfileName;
408
+ let lastLoadRecoveredInvalidConfig = false;
409
+ const agentDir = options.agentDir ?? resolveAgentDir();
410
+ const configPath = options.configPath ?? getConfigPath();
411
+ const env = options.env ?? process.env;
412
+ const getEffectiveConfig = () => applyTelegramProfile(config, activeProfileName);
413
+ const setEffectiveConfig = (nextConfig) => {
414
+ config = storeTelegramEffectiveConfig(config, nextConfig, activeProfileName);
415
+ mutationVersion += 1;
416
+ };
417
+ const adoptPersistedConfig = (merged, preserveLocalChanges) => {
418
+ // Local edits are relative to the latest observation, not a queued write's older request baseline.
419
+ const nextConfig = preserveLocalChanges
420
+ ? mergeTelegramConfigDelta(persistedConfig, config, merged)
421
+ : cloneTelegramConfig(merged);
422
+ persistedConfig = cloneTelegramConfig(merged);
423
+ config = nextConfig;
424
+ };
425
+ const withPersistedPairingProfile = (profileName, tokenSha256, observe) => {
426
+ if ((profileName !== TELEGRAM_DEFAULT_PROFILE_NAME && !isValidTelegramProfileName(profileName)) ||
427
+ !/^[a-f0-9]{64}$/u.test(tokenSha256)) {
428
+ throw new Error("Invalid Telegram pairing admission identity.");
429
+ }
430
+ return withTelegramFileTransaction(`${configPath}.transaction`, () => {
431
+ const latest = readTelegramConfigForTransaction(configPath);
432
+ const profile = latest.profiles?.[profileName];
433
+ const resolvedToken = typeof profile?.botToken === "string"
434
+ ? resolveTelegramBotToken(profile.botToken, env)
435
+ : undefined;
436
+ if (!profile || !resolvedToken ||
437
+ createHash("sha256").update(resolvedToken).digest("hex") !== tokenSha256 ||
438
+ (profile.allowedUserId !== undefined &&
439
+ (!Number.isSafeInteger(profile.allowedUserId) || profile.allowedUserId <= 0))) {
440
+ throw new Error("Telegram pairing admission authority is unavailable or changed.");
441
+ }
442
+ return observe(latest, profile);
443
+ });
444
+ };
445
+ return {
446
+ get: getEffectiveConfig,
447
+ getStoredConfig: () => config,
448
+ set: setEffectiveConfig,
449
+ setProfile: (profileName, profile) => {
450
+ config = {
451
+ ...omitTelegramRootProfileFields(config),
452
+ profiles: {
453
+ ...(config.profiles ?? {}),
454
+ [profileName]: cloneTelegramConfig(profile),
455
+ },
456
+ };
457
+ mutationVersion += 1;
458
+ },
459
+ update: (mutate) => {
460
+ const nextConfig = getEffectiveConfig();
461
+ mutate(nextConfig);
462
+ setEffectiveConfig(nextConfig);
463
+ },
464
+ activateProfile: (profileName) => {
465
+ const normalizedProfileName = !profileName || profileName === TELEGRAM_DEFAULT_PROFILE_NAME
466
+ ? undefined
467
+ : profileName;
468
+ if (normalizedProfileName && !config.profiles?.[normalizedProfileName]) {
469
+ return false;
470
+ }
471
+ activeProfileName = normalizedProfileName;
472
+ return true;
473
+ },
474
+ getActiveProfileName: () => activeProfileName,
475
+ getBotToken: () => resolveTelegramBotToken(getEffectiveConfig().botToken, env),
476
+ getBotTokenDiagnostic: () => getTelegramBotTokenDiagnostic(getEffectiveConfig().botToken, env),
477
+ hasBotToken: () => !!resolveTelegramBotToken(getEffectiveConfig().botToken, env),
478
+ getAllowedUserId: () => getEffectiveConfig().allowedUserId,
479
+ getLegacyPollingCursor: () => getEffectiveConfig()
480
+ .lastUpdateId,
481
+ removeLegacyPollingCursor: () => {
482
+ const next = {
483
+ ...getEffectiveConfig(),
484
+ };
485
+ delete next.lastUpdateId;
486
+ setEffectiveConfig(next);
487
+ },
488
+ getInboundHandlers: () => [
489
+ ...(config.inboundHandlers ?? []),
490
+ ...(config.attachmentHandlers ?? []),
491
+ ],
492
+ getAttachmentHandlers: () => config.attachmentHandlers,
493
+ getOutboundHandlers: () => config.outboundHandlers,
494
+ setAllowedUserId: (userId) => {
495
+ const nextConfig = getEffectiveConfig();
496
+ nextConfig.allowedUserId = userId;
497
+ setEffectiveConfig(nextConfig);
498
+ },
499
+ withSourceSerialization: (operation) => withTelegramFileTransaction(`${configPath}.transaction`, operation),
500
+ withPairingAdmission: (profileName, tokenSha256, publish) => withPersistedPairingProfile(profileName, tokenSha256, (_latest, profile) => publish(profile.allowedUserId === undefined)),
501
+ withPairedUserAdmission: (profileName, tokenSha256, userId, publish, assertExecutionCurrent) => {
502
+ if (!Number.isSafeInteger(userId) || userId <= 0)
503
+ return { admitted: false };
504
+ assertExecutionCurrent?.();
505
+ return withPersistedPairingProfile(profileName, tokenSha256, (latest, profile) => {
506
+ if (profile.allowedUserId !== userId)
507
+ return { admitted: false };
508
+ assertExecutionCurrent?.();
509
+ const current = getEffectiveConfig();
510
+ const previousOwner = persistedConfig.profiles?.[profileName]?.allowedUserId;
511
+ if ((activeProfileName ?? TELEGRAM_DEFAULT_PROFILE_NAME) !== profileName ||
512
+ current.botToken !== profile.botToken ||
513
+ (current.allowedUserId !== undefined && current.allowedUserId !== userId) ||
514
+ (current.allowedUserId === undefined && previousOwner !== undefined)) {
515
+ throw new Error("Telegram paired admission lost local profile authority.");
516
+ }
517
+ // Observation is not a local edit; queued persistence still adopts its own fresh disk result.
518
+ adoptPersistedConfig(latest, true);
519
+ return { admitted: true, value: publish() };
520
+ });
521
+ },
522
+ persistAllowedUserId: (userId, assertExecutionCurrent, commitIfOwned) => {
523
+ const profileName = activeProfileName;
524
+ const profileKey = profileName ?? TELEGRAM_DEFAULT_PROFILE_NAME;
525
+ const botToken = getEffectiveConfig().botToken;
526
+ const previousOwner = getEffectiveConfig().allowedUserId;
527
+ const assertCurrent = () => {
528
+ assertExecutionCurrent?.();
529
+ if (activeProfileName !== profileName || getEffectiveConfig().botToken !== botToken ||
530
+ getEffectiveConfig().allowedUserId !== previousOwner) {
531
+ throw new Error("Telegram pairing lost its originating profile authority.");
532
+ }
533
+ };
534
+ const pairing = persistQueue.then(() => {
535
+ assertCurrent();
536
+ if (!Number.isSafeInteger(userId) || userId <= 0)
537
+ throw new Error("Invalid Telegram pairing user ID.");
538
+ let merged;
539
+ const publish = () => {
540
+ merged = withTelegramFileTransaction(`${configPath}.transaction`, () => {
541
+ const latest = readTelegramConfigForTransaction(configPath);
542
+ const profile = latest.profiles?.[profileKey];
543
+ if (!botToken || profile?.botToken !== botToken) {
544
+ throw new Error("Telegram pairing profile is unavailable or changed.");
545
+ }
546
+ assertCurrent();
547
+ if (profile.allowedUserId !== undefined)
548
+ return latest;
549
+ const next = { ...latest, profiles: { ...latest.profiles,
550
+ [profileKey]: { ...profile, allowedUserId: userId } } };
551
+ writeTelegramConfigInTransaction(agentDir, configPath, next);
552
+ return next;
553
+ });
554
+ };
555
+ if (commitIfOwned) {
556
+ if (!commitIfOwned(publish))
557
+ throw new Error("Telegram pairing lost transport ownership before publication.");
558
+ }
559
+ else {
560
+ publish();
561
+ }
562
+ if (!merged)
563
+ throw new Error("Telegram pairing publication did not execute.");
564
+ adoptPersistedConfig(merged, true);
565
+ return merged.profiles?.[profileKey]?.allowedUserId === userId;
566
+ });
567
+ persistQueue = pairing.then(() => undefined, () => undefined);
568
+ return pairing;
569
+ },
570
+ load: async () => {
571
+ lastLoadRecoveredInvalidConfig = false;
572
+ const loadedConfig = await readTelegramConfig(configPath, {
573
+ onInvalidConfig: (recovery) => {
574
+ lastLoadRecoveredInvalidConfig = true;
575
+ options.recordRuntimeEvent?.("config", recovery.error, {
576
+ phase: "load",
577
+ configPath: recovery.configPath,
578
+ recoveryPath: recovery.recoveryPath,
579
+ });
580
+ },
581
+ });
582
+ let normalized;
583
+ try {
584
+ normalized = normalizeTelegramDefaultProfileConfig(loadedConfig);
585
+ }
586
+ catch (error) {
587
+ options.recordRuntimeEvent?.("config", error, {
588
+ phase: "default-profile-normalize",
589
+ configPath,
590
+ });
591
+ throw error;
592
+ }
593
+ config = normalized.changed
594
+ ? withTelegramFileTransaction(`${configPath}.transaction`, () => {
595
+ const latestConfig = readTelegramConfigForTransaction(configPath);
596
+ const latestNormalized = normalizeTelegramDefaultProfileConfig(latestConfig);
597
+ if (latestNormalized.changed) {
598
+ writeTelegramConfigInTransaction(agentDir, configPath, latestNormalized.config);
599
+ }
600
+ return latestNormalized.config;
601
+ })
602
+ : normalized.config;
603
+ persistedConfig = cloneTelegramConfig(config);
604
+ mutationVersion += 1;
605
+ },
606
+ didLastLoadRecoverInvalidConfig: () => lastLoadRecoveredInvalidConfig,
607
+ persist: (nextConfig = getEffectiveConfig(), options) => {
608
+ const profileName = activeProfileName;
609
+ const desiredConfig = storeTelegramEffectiveConfig(config, cloneTelegramConfig(nextConfig), profileName);
610
+ const baseConfig = cloneTelegramConfig(persistedConfig);
611
+ const capturedMutationVersion = mutationVersion;
612
+ const persist = persistQueue.then(() => {
613
+ const mergedConfig = withTelegramFileTransaction(`${configPath}.transaction`, () => {
614
+ if (options?.isCurrent && !options.isCurrent()) {
615
+ throw new Error("Telegram config update lost its originating authority.");
616
+ }
617
+ const latestConfig = readTelegramConfigForTransaction(configPath);
618
+ const merged = mergeTelegramConfigDelta(baseConfig, desiredConfig, latestConfig);
619
+ if (!configValuesEqual(latestConfig, merged)) {
620
+ writeTelegramConfigInTransaction(agentDir, configPath, merged);
621
+ }
622
+ return merged;
623
+ });
624
+ adoptPersistedConfig(mergedConfig, mutationVersion !== capturedMutationVersion);
625
+ });
626
+ persistQueue = persist.catch(() => undefined);
627
+ return persist;
628
+ },
629
+ };
630
+ }
631
+ export function createTelegramDraftPreviewsChecker(configStore) {
632
+ return () => {
633
+ const config = configStore.get();
634
+ return (config.assistant?.draftPreviews ??
635
+ config.draftPreviews ??
636
+ config.richDraftPreviews ??
637
+ true);
638
+ };
639
+ }
640
+ export function createTelegramDraftPreviewsSetter(configStore) {
641
+ return async (enabled) => {
642
+ await loadLatestTelegramConfig(configStore);
643
+ const { draftPreviews: _legacyDraftPreviews, richDraftPreviews: _legacyRichDraftPreviews, ...current } = configStore.get();
644
+ const config = {
645
+ ...current,
646
+ assistant: { ...current.assistant, draftPreviews: enabled },
647
+ };
648
+ configStore.set(config);
649
+ await configStore.persist(config);
650
+ };
651
+ }
652
+ export function createTelegramAssistantRenderingModeGetter(configStore) {
653
+ return () => {
654
+ const config = configStore.get();
655
+ const mode = config.assistant?.rendering ?? config.assistantRendering;
656
+ return mode === "html" ? "html" : "rich";
657
+ };
658
+ }
659
+ export function createTelegramAssistantRenderingModeSetter(configStore) {
660
+ return async (mode) => {
661
+ await loadLatestTelegramConfig(configStore);
662
+ const { assistantRendering: _legacyAssistantRendering, ...current } = configStore.get();
663
+ const config = {
664
+ ...current,
665
+ assistant: { ...current.assistant, rendering: mode },
666
+ };
667
+ configStore.set(config);
668
+ await configStore.persist(config);
669
+ };
670
+ }
671
+ export function createTelegramActivityVerbosityGetter(configStore) {
672
+ return () => {
673
+ const assistant = configStore.get().assistant;
674
+ if (assistant?.activity !== undefined) {
675
+ if (assistant.activity === "thinking" ||
676
+ assistant.activity === "tools" ||
677
+ assistant.activity === "verbose") {
678
+ return assistant.activity;
679
+ }
680
+ return "quiet";
681
+ }
682
+ if (assistant?.activityVerbosity !== undefined) {
683
+ return assistant.activityVerbosity === "verbose" ? "verbose" : "quiet";
684
+ }
685
+ return "verbose";
686
+ };
687
+ }
688
+ export function createTelegramActivityVerbosityRefresher(configStore) {
689
+ return () => loadLatestTelegramConfig(configStore);
690
+ }
691
+ export function createTelegramActivityVerbositySetter(configStore) {
692
+ return async (verbosity) => {
693
+ await loadLatestTelegramConfig(configStore);
694
+ const current = configStore.get();
695
+ const { activityVerbosity: _legacyActivityVerbosity, ...assistant } = current.assistant ?? {};
696
+ const config = {
697
+ ...current,
698
+ assistant: {
699
+ ...assistant,
700
+ activity: verbosity,
701
+ },
702
+ };
703
+ configStore.set(config);
704
+ await configStore.persist(config);
705
+ };
706
+ }
707
+ export function createTelegramVoiceReplyModeGetter(configStore) {
708
+ return () => {
709
+ const mode = configStore.get().voice?.replyMode;
710
+ return mode === "mirror" || mode === "always" ? mode : "manual";
711
+ };
712
+ }
713
+ export function createTelegramVoiceReplyModeConfiguredChecker(configStore) {
714
+ return () => {
715
+ const mode = configStore.get().voice?.replyMode;
716
+ return mode === "mirror" || mode === "always";
717
+ };
718
+ }
719
+ export function createTelegramVoiceReplyModeSetter(configStore) {
720
+ return async (replyMode) => {
721
+ await loadLatestTelegramConfig(configStore);
722
+ const current = configStore.get();
723
+ if (replyMode === undefined ||
724
+ replyMode === "manual" ||
725
+ replyMode === "hidden") {
726
+ const { replyMode: _replyMode, ...remainingVoice } = current.voice ?? {};
727
+ const next = { ...current };
728
+ if (Object.keys(remainingVoice).length > 0)
729
+ next.voice = remainingVoice;
730
+ else
731
+ delete next.voice;
732
+ configStore.set(next);
733
+ await configStore.persist(next);
734
+ return;
735
+ }
736
+ const next = { ...current, voice: { ...(current.voice ?? {}), replyMode } };
737
+ configStore.set(next);
738
+ await configStore.persist(next);
739
+ };
740
+ }
741
+ function getSystemTimezone() {
742
+ try {
743
+ const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
744
+ return tz && tz.length > 0 ? tz : "UTC";
745
+ }
746
+ catch {
747
+ return "UTC";
748
+ }
749
+ }
750
+ export function resolveTelegramTimeConfig(raw, timeInjection = undefined) {
751
+ const injectionMode = timeInjection === undefined
752
+ ? "interval"
753
+ : timeInjection === "always" || timeInjection === "interval"
754
+ ? timeInjection
755
+ : "hidden";
756
+ const interval = typeof raw?.interval === "number" && raw.interval > 0
757
+ ? raw.interval
758
+ : 60 * 60 * 1000;
759
+ const timezone = getSystemTimezone();
760
+ return { injectionMode, interval, timezone };
761
+ }
762
+ export function createTelegramTimeConfigGetter(configStore) {
763
+ return () => {
764
+ const config = configStore.get();
765
+ return resolveTelegramTimeConfig(config.time, config.assistant?.timeInjection);
766
+ };
767
+ }
768
+ export function createTelegramTimeInjectionModeGetter(configStore) {
769
+ return () => {
770
+ const config = configStore.get();
771
+ return resolveTelegramTimeConfig(config.time, config.assistant?.timeInjection).injectionMode;
772
+ };
773
+ }
774
+ export function createTelegramTimeInjectionModeSetter(configStore) {
775
+ return async (injectionMode) => {
776
+ await loadLatestTelegramConfig(configStore);
777
+ const current = configStore.get();
778
+ const next = {
779
+ ...current,
780
+ assistant: {
781
+ ...(current.assistant ?? {}),
782
+ timeInjection: injectionMode,
783
+ },
784
+ };
785
+ configStore.set(next);
786
+ await configStore.persist(next);
787
+ };
788
+ }
789
+ export function createTelegramProactivePushChatIdGetter(getTarget) {
790
+ return () => getTarget()?.chatId;
791
+ }
792
+ export function createTelegramProactivePushTargetGetter(deps) {
793
+ return () => {
794
+ const activeTarget = deps.getActiveTurnTarget();
795
+ if (activeTarget)
796
+ return activeTarget;
797
+ const assignedTarget = deps.getAssignedTarget();
798
+ if (assignedTarget)
799
+ return assignedTarget;
800
+ const chatId = deps.getAllowedUserId();
801
+ return typeof chatId === "number" ? { chatId } : undefined;
802
+ };
803
+ }
804
+ export function createTelegramAutomaticThreadCleanupChecker(configStore) {
805
+ return () => configStore.get().threads?.automaticCleanup ?? true;
806
+ }
807
+ export function createTelegramAutomaticThreadCleanupResolver(configStore) {
808
+ return async () => {
809
+ await loadLatestTelegramConfig(configStore);
810
+ if (configStore.didLastLoadRecoverInvalidConfig?.()) {
811
+ throw new Error("Thread cleanup setting is unavailable after invalid Telegram config recovery.");
812
+ }
813
+ return createTelegramAutomaticThreadCleanupChecker(configStore)();
814
+ };
815
+ }
816
+ export function createTelegramAutomaticThreadCleanupSetter(configStore) {
817
+ return async (enabled) => {
818
+ await loadLatestTelegramConfig(configStore);
819
+ const current = configStore.get();
820
+ const config = {
821
+ ...current,
822
+ threads: { ...current.threads, automaticCleanup: enabled },
823
+ };
824
+ configStore.set(config);
825
+ await configStore.persist(config);
826
+ };
827
+ }
828
+ export function createTelegramConfigControls(configStore) {
829
+ return {
830
+ areDraftPreviewsEnabled: createTelegramDraftPreviewsChecker(configStore),
831
+ setDraftPreviewsEnabled: createTelegramDraftPreviewsSetter(configStore),
832
+ getAssistantRenderingMode: createTelegramAssistantRenderingModeGetter(configStore),
833
+ setAssistantRenderingMode: createTelegramAssistantRenderingModeSetter(configStore),
834
+ getActivityVerbosity: createTelegramActivityVerbosityGetter(configStore),
835
+ refreshActivityVerbosity: createTelegramActivityVerbosityRefresher(configStore),
836
+ setActivityVerbosity: createTelegramActivityVerbositySetter(configStore),
837
+ getVoiceReplyMode: createTelegramVoiceReplyModeGetter(configStore),
838
+ isVoiceReplyModeConfigured: createTelegramVoiceReplyModeConfiguredChecker(configStore),
839
+ setVoiceReplyMode: createTelegramVoiceReplyModeSetter(configStore),
840
+ getTimeInjectionMode: createTelegramTimeInjectionModeGetter(configStore),
841
+ setTimeInjectionMode: createTelegramTimeInjectionModeSetter(configStore),
842
+ isAutomaticThreadCleanupEnabled: createTelegramAutomaticThreadCleanupChecker(configStore),
843
+ resolveAutomaticThreadCleanupEnabled: createTelegramAutomaticThreadCleanupResolver(configStore),
844
+ setAutomaticThreadCleanupEnabled: createTelegramAutomaticThreadCleanupSetter(configStore),
845
+ };
846
+ }
847
+ export function getTelegramAuthorizationState(userId, allowedUserId) {
848
+ if (allowedUserId === undefined) {
849
+ return { kind: "pair", userId };
850
+ }
851
+ if (userId === allowedUserId) {
852
+ return { kind: "allow" };
853
+ }
854
+ return { kind: "deny" };
855
+ }
856
+ function isTelegramStaleContextError(error) {
857
+ return (error instanceof Error &&
858
+ (error.message.includes("stale after session") ||
859
+ error.message.includes("stale ctx")));
860
+ }
861
+ export async function pairTelegramUserIfNeeded(userId, deps) {
862
+ const authorization = getTelegramAuthorizationState(userId, deps.allowedUserId);
863
+ if (authorization.kind !== "pair")
864
+ return authorization.kind === "allow";
865
+ deps.assertExecutionCurrent?.();
866
+ const allowed = await deps.persistAllowedUserId(authorization.userId, deps.assertExecutionCurrent);
867
+ deps.assertExecutionCurrent?.();
868
+ if (!allowed)
869
+ return false;
870
+ try {
871
+ deps.updateStatus(deps.ctx);
872
+ }
873
+ catch (error) {
874
+ if (!isTelegramStaleContextError(error))
875
+ throw error;
876
+ }
877
+ return true;
878
+ }
879
+ export function createTelegramUserPairingRuntime(deps) {
880
+ return {
881
+ pairIfNeeded: (userId, ctx, assertExecutionCurrent) => pairTelegramUserIfNeeded(userId, {
882
+ allowedUserId: deps.getAllowedUserId(),
883
+ ctx,
884
+ persistAllowedUserId: deps.persistAllowedUserId,
885
+ updateStatus: deps.updateStatus,
886
+ assertExecutionCurrent,
887
+ }),
888
+ };
889
+ }