@llblab/pi-telegram 0.52.2 → 0.53.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 +9 -7
  2. package/BACKLOG.md +1 -18
  3. package/CHANGELOG.md +11 -0
  4. package/README.md +7 -11
  5. package/api/commands.ts +3 -0
  6. package/dist/api/commands.d.ts +1 -1
  7. package/dist/lib/activity-verbosity.d.ts +1 -5
  8. package/dist/lib/activity-verbosity.js +15 -42
  9. package/dist/lib/activity.d.ts +9 -0
  10. package/dist/lib/activity.js +39 -10
  11. package/dist/lib/agent-messages.js +7 -3
  12. package/dist/lib/bindings.d.ts +43 -5
  13. package/dist/lib/bindings.js +163 -20
  14. package/dist/lib/bus-api.d.ts +12 -0
  15. package/dist/lib/bus-api.js +182 -79
  16. package/dist/lib/bus-follower.d.ts +111 -31
  17. package/dist/lib/bus-follower.js +1324 -359
  18. package/dist/lib/bus-leader.d.ts +41 -91
  19. package/dist/lib/bus-leader.js +969 -637
  20. package/dist/lib/bus-transport.js +16 -9
  21. package/dist/lib/bus.d.ts +215 -18
  22. package/dist/lib/bus.js +1048 -327
  23. package/dist/lib/channel-posts.d.ts +5 -0
  24. package/dist/lib/channel-posts.js +434 -261
  25. package/dist/lib/command-templates.d.ts +26 -5
  26. package/dist/lib/command-templates.js +92 -176
  27. package/dist/lib/commands.d.ts +168 -82
  28. package/dist/lib/commands.js +1409 -291
  29. package/dist/lib/config.d.ts +1 -8
  30. package/dist/lib/config.js +34 -76
  31. package/dist/lib/delivery.d.ts +2 -2
  32. package/dist/lib/delivery.js +16 -16
  33. package/dist/lib/extension.js +639 -171
  34. package/dist/lib/generative-app-worker.mjs +19 -7
  35. package/dist/lib/generative-apps.d.ts +0 -5
  36. package/dist/lib/generative-apps.js +61 -49
  37. package/dist/lib/inbound.d.ts +5 -0
  38. package/dist/lib/inbound.js +35 -59
  39. package/dist/lib/journal.d.ts +33 -3
  40. package/dist/lib/journal.js +2063 -782
  41. package/dist/lib/lifecycle.d.ts +2 -6
  42. package/dist/lib/lifecycle.js +75 -58
  43. package/dist/lib/locks.d.ts +29 -3
  44. package/dist/lib/locks.js +289 -108
  45. package/dist/lib/logging.js +50 -14
  46. package/dist/lib/media.d.ts +22 -1
  47. package/dist/lib/media.js +46 -28
  48. package/dist/lib/menu-model.d.ts +19 -5
  49. package/dist/lib/menu-model.js +53 -11
  50. package/dist/lib/menu-queue.d.ts +5 -4
  51. package/dist/lib/menu-queue.js +45 -63
  52. package/dist/lib/menu-settings.d.ts +5 -4
  53. package/dist/lib/menu-settings.js +114 -78
  54. package/dist/lib/menu-status.d.ts +1 -1
  55. package/dist/lib/menu-status.js +8 -25
  56. package/dist/lib/menu-thinking.d.ts +6 -1
  57. package/dist/lib/menu-thinking.js +14 -21
  58. package/dist/lib/menu.d.ts +8 -61
  59. package/dist/lib/menu.js +92 -164
  60. package/dist/lib/model.d.ts +1 -1
  61. package/dist/lib/model.js +2 -16
  62. package/dist/lib/outbound-attachments.d.ts +6 -27
  63. package/dist/lib/outbound-attachments.js +45 -38
  64. package/dist/lib/outbound-buttons.js +22 -16
  65. package/dist/lib/outbound-markup.d.ts +0 -2
  66. package/dist/lib/outbound-markup.js +51 -64
  67. package/dist/lib/outbound-voice.js +6 -3
  68. package/dist/lib/outbound.d.ts +6 -6
  69. package/dist/lib/outbound.js +61 -51
  70. package/dist/lib/ownership.js +6 -2
  71. package/dist/lib/paths.d.ts +5 -0
  72. package/dist/lib/paths.js +37 -21
  73. package/dist/lib/pi.d.ts +2 -0
  74. package/dist/lib/pi.js +8 -1
  75. package/dist/lib/polling.d.ts +21 -37
  76. package/dist/lib/polling.js +59 -50
  77. package/dist/lib/preview.js +39 -19
  78. package/dist/lib/process-identity.d.ts +24 -0
  79. package/dist/lib/process-identity.js +133 -0
  80. package/dist/lib/prompts.js +2 -7
  81. package/dist/lib/queue.d.ts +14 -16
  82. package/dist/lib/queue.js +249 -126
  83. package/dist/lib/recovery.d.ts +2 -83
  84. package/dist/lib/recovery.js +20 -274
  85. package/dist/lib/replies.d.ts +16 -24
  86. package/dist/lib/replies.js +99 -116
  87. package/dist/lib/routing.d.ts +120 -7
  88. package/dist/lib/routing.js +4667 -1152
  89. package/dist/lib/runtime.d.ts +1 -11
  90. package/dist/lib/sections.d.ts +4 -29
  91. package/dist/lib/sections.js +7 -20
  92. package/dist/lib/skills.js +4 -3
  93. package/dist/lib/status.js +86 -57
  94. package/dist/lib/sync.d.ts +1 -3
  95. package/dist/lib/sync.js +67 -43
  96. package/dist/lib/target.d.ts +4 -0
  97. package/dist/lib/target.js +18 -0
  98. package/dist/lib/telegram-api.d.ts +18 -13
  99. package/dist/lib/telegram-api.js +148 -53
  100. package/dist/lib/text-groups.js +4 -39
  101. package/dist/lib/thread-cleanup-manager.d.ts +6 -27
  102. package/dist/lib/thread-cleanup-manager.js +369 -186
  103. package/dist/lib/thread-display.d.ts +1 -2
  104. package/dist/lib/thread-display.js +58 -38
  105. package/dist/lib/thread-naming.d.ts +28 -8
  106. package/dist/lib/thread-naming.js +175 -66
  107. package/dist/lib/thread-reconciler.d.ts +42 -16
  108. package/dist/lib/thread-reconciler.js +90 -50
  109. package/dist/lib/threads.d.ts +82 -20
  110. package/dist/lib/threads.js +2247 -855
  111. package/dist/lib/time-injection.js +1 -2
  112. package/dist/lib/turns.js +1 -19
  113. package/dist/lib/updates.d.ts +169 -80
  114. package/dist/lib/updates.js +2418 -641
  115. package/dist/lib/voice.d.ts +7 -22
  116. package/dist/lib/voice.js +7 -22
  117. package/dist/lib/wire.js +1 -1
  118. package/dist/lib/workspace-admission.d.ts +3 -1
  119. package/dist/lib/workspace-admission.js +425 -211
  120. package/dist/lib/workspace-identity.js +8 -13
  121. package/dist/lib/workspace-retirement.d.ts +1 -1
  122. package/dist/lib/workspace-retirement.js +328 -140
  123. package/dist/lib/workspace-slots.js +8 -4
  124. package/dist/package.json +1 -1
  125. package/docs/architecture.md +31 -29
  126. package/docs/inbound.md +1 -1
  127. package/docs/multi-instance-bus.md +15 -3
  128. package/docs/outbound.md +1 -1
  129. package/docs/public-api.md +29 -1
  130. package/docs/ui-style.md +25 -8
  131. package/lib/activity-verbosity.ts +59 -82
  132. package/lib/activity.ts +146 -102
  133. package/lib/agent-messages.ts +12 -5
  134. package/lib/bindings.ts +389 -82
  135. package/lib/bus-api.ts +263 -102
  136. package/lib/bus-follower.ts +2440 -601
  137. package/lib/bus-leader.ts +2209 -1290
  138. package/lib/bus-transport.ts +46 -12
  139. package/lib/bus.ts +1673 -444
  140. package/lib/channel-posts.ts +848 -327
  141. package/lib/command-templates.ts +152 -265
  142. package/lib/commands.ts +2398 -555
  143. package/lib/config.ts +177 -170
  144. package/lib/delivery.ts +41 -29
  145. package/lib/extension.ts +1206 -433
  146. package/lib/generative-app-worker.mjs +19 -7
  147. package/lib/generative-apps.ts +324 -211
  148. package/lib/inbound.ts +95 -97
  149. package/lib/journal.ts +5311 -1763
  150. package/lib/keyboard.ts +1 -3
  151. package/lib/lifecycle.ts +128 -78
  152. package/lib/locks.ts +677 -203
  153. package/lib/logging.ts +147 -58
  154. package/lib/media.ts +82 -43
  155. package/lib/menu-model.ts +85 -18
  156. package/lib/menu-queue.ts +88 -129
  157. package/lib/menu-settings.ts +260 -155
  158. package/lib/menu-status.ts +20 -60
  159. package/lib/menu-thinking.ts +41 -56
  160. package/lib/menu.ts +112 -285
  161. package/lib/model.ts +8 -22
  162. package/lib/outbound-attachments.ts +156 -125
  163. package/lib/outbound-buttons.ts +76 -49
  164. package/lib/outbound-markup.ts +96 -94
  165. package/lib/outbound-voice.ts +33 -27
  166. package/lib/outbound.ts +125 -125
  167. package/lib/ownership.ts +19 -10
  168. package/lib/paths.ts +148 -49
  169. package/lib/pi.ts +35 -15
  170. package/lib/polling.ts +144 -147
  171. package/lib/preview.ts +93 -40
  172. package/lib/process-identity.ts +164 -0
  173. package/lib/prompts.ts +4 -11
  174. package/lib/queue.ts +511 -300
  175. package/lib/recovery.ts +50 -454
  176. package/lib/rendering.ts +1 -3
  177. package/lib/replies.ts +248 -209
  178. package/lib/routing.ts +8509 -2186
  179. package/lib/runtime.ts +1 -12
  180. package/lib/sections.ts +24 -89
  181. package/lib/setup.ts +1 -3
  182. package/lib/skills.ts +9 -4
  183. package/lib/status.ts +215 -110
  184. package/lib/sync.ts +448 -361
  185. package/lib/target.ts +22 -3
  186. package/lib/telegram-api.ts +302 -127
  187. package/lib/text-groups.ts +5 -39
  188. package/lib/thread-cleanup-manager.ts +833 -331
  189. package/lib/thread-display.ts +213 -97
  190. package/lib/thread-naming.ts +232 -87
  191. package/lib/thread-reconciler.ts +155 -72
  192. package/lib/threads.ts +5318 -1845
  193. package/lib/time-injection.ts +1 -4
  194. package/lib/turns.ts +15 -28
  195. package/lib/updates.ts +5184 -1612
  196. package/lib/voice.ts +7 -35
  197. package/lib/wire.ts +1 -1
  198. package/lib/workspace-admission.ts +996 -416
  199. package/lib/workspace-identity.ts +14 -17
  200. package/lib/workspace-retirement.ts +878 -376
  201. package/lib/workspace-slots.ts +22 -13
  202. package/package.json +1 -1
  203. package/scripts/check-downgrade.mjs +0 -472
  204. package/scripts/measure-workspace.mjs +0 -108
@@ -5,13 +5,14 @@
5
5
  * Excludes routing, name allocation, profile mutation, and live-owner discovery.
6
6
  */
7
7
  import { isDeepStrictEqual } from "node:util";
8
+
8
9
  import type { TelegramThreadDisplayMode } from "./config.ts";
10
+ import type { TelegramApiCallOptions } from "./telegram-api.ts";
9
11
  import type {
10
12
  TelegramTopicTargetStore,
11
13
  TelegramWorkspaceDisplayBinding,
12
14
  TelegramWorkspaceThreadBinding,
13
15
  } from "./threads.ts";
14
- import type { TelegramApiCallOptions } from "./telegram-api.ts";
15
16
 
16
17
  function labelText(value: string): string {
17
18
  return value.replace(/\s+/gu, " ").trim();
@@ -30,14 +31,20 @@ function directorySegments(cwd: string): string[] {
30
31
  return cwd.split("/").filter(Boolean);
31
32
  }
32
33
 
33
- function distinguishingDirectorySegments(cwd: string, directories: readonly string[]): string[] {
34
+ function distinguishingDirectorySegments(
35
+ cwd: string,
36
+ directories: readonly string[],
37
+ ): string[] {
34
38
  const parts = directorySegments(cwd);
35
39
  if (!parts.length) return [];
36
40
  for (let depth = 1; depth <= parts.length; depth++) {
37
41
  const candidate = labelText(parts.slice(-depth).join("/"));
38
- const collides = directories.some((other) => other !== cwd &&
39
- labelText(directorySegments(other).slice(-depth).join("/")).toLowerCase() ===
40
- candidate.toLowerCase(),
42
+ const collides = directories.some(
43
+ (other) =>
44
+ other !== cwd &&
45
+ labelText(
46
+ directorySegments(other).slice(-depth).join("/"),
47
+ ).toLowerCase() === candidate.toLowerCase(),
41
48
  );
42
49
  if (!collides) return parts.slice(-depth);
43
50
  }
@@ -65,14 +72,21 @@ function formatDirectoryLabel(
65
72
  ): string {
66
73
  const segments = distinguishingDirectorySegments(cwd, directories);
67
74
  if (!segments.length) return "/";
68
- const formatted = segments.map((segment) => {
69
- const tokens = tokenizeTelegramDirectorySegment(segment);
70
- if (!tokens.length) return "";
71
- if (mode === "directory-snake") return tokens.map((token) => token.toLowerCase()).join("_");
72
- return tokens.map((token) => /\p{L}/u.test(token) && token === token.toUpperCase()
73
- ? token
74
- : `${token.slice(0, 1).toUpperCase()}${token.slice(1).toLowerCase()}`).join(" ");
75
- }).filter(Boolean);
75
+ const formatted = segments
76
+ .map((segment) => {
77
+ const tokens = tokenizeTelegramDirectorySegment(segment);
78
+ if (!tokens.length) return "";
79
+ if (mode === "directory-snake")
80
+ return tokens.map((token) => token.toLowerCase()).join("_");
81
+ return tokens
82
+ .map((token) =>
83
+ /\p{L}/u.test(token) && token === token.toUpperCase()
84
+ ? token
85
+ : `${token.slice(0, 1).toUpperCase()}${token.slice(1).toLowerCase()}`,
86
+ )
87
+ .join(" ");
88
+ })
89
+ .filter(Boolean);
76
90
  if (!formatted.length) return directoryLabel(cwd, directories);
77
91
  return formatted.join(mode === "directory-snake" ? "_" : " / ");
78
92
  }
@@ -91,27 +105,59 @@ export function resolveTelegramLiveWorkspaceBindingKeys(
91
105
  };
92
106
  add(leaderTarget);
93
107
  for (const follower of followers) add(follower.target);
94
- return new Set(bindings.filter((binding) =>
95
- targets.has(`${binding.target.chatId}:${binding.target.threadId}`),
96
- ).map((binding) => binding.bindingKey));
108
+ return new Set(
109
+ bindings
110
+ .filter((binding) =>
111
+ targets.has(`${binding.target.chatId}:${binding.target.threadId}`),
112
+ )
113
+ .map((binding) => binding.bindingKey),
114
+ );
97
115
  }
98
116
 
99
117
  /** Missing or ambiguous metadata yields no label rather than inventing identity. */
118
+ /** Case-insensitive label occurrences; directory modes count only live bindings. */
119
+ function countTelegramDisplayLabels(
120
+ bindings: readonly TelegramWorkspaceDisplayBinding[],
121
+ labels: ReadonlyMap<string, string>,
122
+ mode: TelegramThreadDisplayMode,
123
+ liveBindingKeys: ReadonlySet<string>,
124
+ ): Map<string, number> {
125
+ const counts = new Map<string, number>();
126
+ for (const binding of bindings) {
127
+ const label = labels.get(binding.bindingKey);
128
+ if (
129
+ !label ||
130
+ ((mode === "directory-snake" || mode === "directory-title") &&
131
+ !liveBindingKeys.has(binding.bindingKey))
132
+ )
133
+ continue;
134
+ const key = label.toLowerCase();
135
+ counts.set(key, (counts.get(key) ?? 0) + 1);
136
+ }
137
+ return counts;
138
+ }
139
+
100
140
  export function resolveTelegramWorkspaceDisplayNames(
101
141
  bindings: readonly TelegramWorkspaceDisplayBinding[],
102
142
  mode: TelegramThreadDisplayMode,
103
143
  liveBindingKeys: ReadonlySet<string> = new Set(),
104
144
  ): Map<string, string> {
105
145
  const labels = new Map<string, string>();
106
- const directories = Array.from(new Set(bindings.map((binding) => binding.cwd)));
146
+ const directories = Array.from(
147
+ new Set(bindings.map((binding) => binding.cwd)),
148
+ );
107
149
  const liveDirectoryCounts = new Map<string, number>();
108
150
  for (const binding of bindings) {
109
151
  if (!liveBindingKeys.has(binding.bindingKey)) continue;
110
- liveDirectoryCounts.set(binding.cwd, (liveDirectoryCounts.get(binding.cwd) ?? 0) + 1);
152
+ liveDirectoryCounts.set(
153
+ binding.cwd,
154
+ (liveDirectoryCounts.get(binding.cwd) ?? 0) + 1,
155
+ );
111
156
  }
112
157
  const bases = new Map<string, string>();
113
158
  for (const binding of bindings) {
114
- const slot = binding.slot && /^[A-Z]$/u.test(binding.slot) ? binding.slot : undefined;
159
+ const slot =
160
+ binding.slot && /^[A-Z]$/u.test(binding.slot) ? binding.slot : undefined;
115
161
  const manualName = binding.manualThreadName
116
162
  ? labelText(binding.manualThreadName)
117
163
  : undefined;
@@ -123,36 +169,50 @@ export function resolveTelegramWorkspaceDisplayNames(
123
169
  const name = binding.threadName ? labelText(binding.threadName) : slot;
124
170
  if (name) labels.set(binding.bindingKey, boundedLabel(name));
125
171
  } else {
126
- const base = mode === "directories"
127
- ? directoryLabel(binding.cwd, directories)
128
- : formatDirectoryLabel(binding.cwd, directories, mode);
172
+ const base =
173
+ mode === "directories"
174
+ ? directoryLabel(binding.cwd, directories)
175
+ : formatDirectoryLabel(binding.cwd, directories, mode);
129
176
  bases.set(binding.bindingKey, base);
130
- const showSuffix = mode === "directories"
131
- ? binding.showSlotSuffix || bindings.filter((candidate) => candidate.cwd === binding.cwd).length > 1
132
- : (liveDirectoryCounts.get(binding.cwd) ?? 0) > 1;
177
+ const showSuffix =
178
+ mode === "directories"
179
+ ? binding.showSlotSuffix ||
180
+ bindings.filter((candidate) => candidate.cwd === binding.cwd)
181
+ .length > 1
182
+ : (liveDirectoryCounts.get(binding.cwd) ?? 0) > 1;
133
183
  if (showSuffix && !slot) continue;
134
- const suffix = !showSuffix ? "" : mode === "directory-title"
135
- ? ` ${slot}`
136
- : `_${slot!.toLowerCase()}`;
184
+ const suffix = !showSuffix
185
+ ? ""
186
+ : mode === "directory-title"
187
+ ? ` ${slot}`
188
+ : `_${slot!.toLowerCase()}`;
137
189
  labels.set(binding.bindingKey, boundedLabel(base, suffix));
138
190
  }
139
191
  }
140
192
  // Long or whitespace-normalized paths can collide even after qualification.
141
- if (mode === "directories" || mode === "directory-snake" || mode === "directory-title") {
142
- const counts = new Map<string, number>();
143
- for (const binding of bindings) {
144
- const label = labels.get(binding.bindingKey);
145
- if (!label || ((mode === "directory-snake" || mode === "directory-title") &&
146
- !liveBindingKeys.has(binding.bindingKey))) continue;
147
- const key = label.toLowerCase();
148
- counts.set(key, (counts.get(key) ?? 0) + 1);
149
- }
193
+ if (
194
+ mode === "directories" ||
195
+ mode === "directory-snake" ||
196
+ mode === "directory-title"
197
+ ) {
198
+ const counts = countTelegramDisplayLabels(
199
+ bindings,
200
+ labels,
201
+ mode,
202
+ liveBindingKeys,
203
+ );
150
204
  for (const binding of bindings) {
151
205
  const label = labels.get(binding.bindingKey);
152
- if (binding.manualThreadName || !label ||
153
- (counts.get(label.toLowerCase()) ?? 0) < 2) continue;
154
- if ((mode === "directory-snake" || mode === "directory-title") &&
155
- (liveDirectoryCounts.get(binding.cwd) ?? 0) < 2) {
206
+ if (
207
+ binding.manualThreadName ||
208
+ !label ||
209
+ (counts.get(label.toLowerCase()) ?? 0) < 2
210
+ )
211
+ continue;
212
+ if (
213
+ (mode === "directory-snake" || mode === "directory-title") &&
214
+ (liveDirectoryCounts.get(binding.cwd) ?? 0) < 2
215
+ ) {
156
216
  labels.delete(binding.bindingKey);
157
217
  continue;
158
218
  }
@@ -160,18 +220,23 @@ export function resolveTelegramWorkspaceDisplayNames(
160
220
  labels.delete(binding.bindingKey);
161
221
  continue;
162
222
  }
163
- labels.set(binding.bindingKey, boundedLabel(bases.get(binding.bindingKey)!,
164
- mode === "directory-title" ? ` ${binding.slot}` : `_${binding.slot.toLowerCase()}`));
223
+ labels.set(
224
+ binding.bindingKey,
225
+ boundedLabel(
226
+ bases.get(binding.bindingKey)!,
227
+ mode === "directory-title"
228
+ ? ` ${binding.slot}`
229
+ : `_${binding.slot.toLowerCase()}`,
230
+ ),
231
+ );
165
232
  }
166
233
  }
167
- const counts = new Map<string, number>();
168
- for (const binding of bindings) {
169
- const label = labels.get(binding.bindingKey);
170
- if (!label || ((mode === "directory-snake" || mode === "directory-title") &&
171
- !liveBindingKeys.has(binding.bindingKey))) continue;
172
- const key = label.toLowerCase();
173
- counts.set(key, (counts.get(key) ?? 0) + 1);
174
- }
234
+ const counts = countTelegramDisplayLabels(
235
+ bindings,
236
+ labels,
237
+ mode,
238
+ liveBindingKeys,
239
+ );
175
240
  for (const [key, label] of labels) {
176
241
  if ((counts.get(label.toLowerCase()) ?? 0) > 1) labels.delete(key);
177
242
  }
@@ -185,24 +250,29 @@ export function resolveTelegramInitialWorkspaceDisplayName(input: {
185
250
  preserveRetainedManualName?: boolean;
186
251
  liveBindingKeys?: ReadonlySet<string>;
187
252
  }): string | undefined {
188
- const retained = input.bindings.find((binding) =>
189
- binding.bindingKey === input.binding.bindingKey,
253
+ const retained = input.bindings.find(
254
+ (binding) => binding.bindingKey === input.binding.bindingKey,
190
255
  );
191
256
  const binding = retained
192
257
  ? {
193
- ...input.binding,
194
- ...(retained.showSlotSuffix ? { showSlotSuffix: true as const } : {}),
195
- ...(input.preserveRetainedManualName !== false && retained.manualThreadName
196
- ? { manualThreadName: retained.manualThreadName }
197
- : {}),
198
- }
258
+ ...input.binding,
259
+ ...(retained.showSlotSuffix ? { showSlotSuffix: true as const } : {}),
260
+ ...(input.preserveRetainedManualName !== false &&
261
+ retained.manualThreadName
262
+ ? { manualThreadName: retained.manualThreadName }
263
+ : {}),
264
+ }
199
265
  : input.binding;
200
- return resolveTelegramWorkspaceDisplayNames([
201
- ...input.bindings.filter((candidate) =>
202
- candidate.bindingKey !== binding.bindingKey,
203
- ),
204
- binding,
205
- ], input.mode, new Set([...(input.liveBindingKeys ?? []), binding.bindingKey])).get(binding.bindingKey);
266
+ return resolveTelegramWorkspaceDisplayNames(
267
+ [
268
+ ...input.bindings.filter(
269
+ (candidate) => candidate.bindingKey !== binding.bindingKey,
270
+ ),
271
+ binding,
272
+ ],
273
+ input.mode,
274
+ new Set([...(input.liveBindingKeys ?? []), binding.bindingKey]),
275
+ ).get(binding.bindingKey);
206
276
  }
207
277
 
208
278
  export async function applyTelegramThreadDisplaySetting(
@@ -210,28 +280,43 @@ export async function applyTelegramThreadDisplaySetting(
210
280
  deps: {
211
281
  getProfileKey(): string | undefined;
212
282
  ownsLeader(): boolean;
213
- getLeaderSetter(): ((mode: TelegramThreadDisplayMode) => Promise<void>) | undefined;
214
- getFollowerSetter(): ((mode: TelegramThreadDisplayMode) => Promise<void>) | undefined;
283
+ getLeaderSetter():
284
+ ((mode: TelegramThreadDisplayMode) => Promise<void>) | undefined;
285
+ getFollowerSetter():
286
+ ((mode: TelegramThreadDisplayMode) => Promise<void>) | undefined;
215
287
  reloadConfig(): Promise<void>;
216
288
  },
217
289
  ): Promise<void> {
218
290
  const profile = deps.getProfileKey();
219
- const setter = deps.ownsLeader() ? deps.getLeaderSetter() : deps.getFollowerSetter();
220
- if (!setter) throw new Error("Thread display settings require a connected compatible instance.");
291
+ const setter = deps.ownsLeader()
292
+ ? deps.getLeaderSetter()
293
+ : deps.getFollowerSetter();
294
+ if (!setter)
295
+ throw new Error(
296
+ "Thread display settings require a connected compatible instance.",
297
+ );
221
298
  await setter(mode);
222
- if (deps.getProfileKey() !== profile) throw new Error("Telegram Thread display setting changed profile.");
299
+ if (deps.getProfileKey() !== profile)
300
+ throw new Error("Telegram Thread display setting changed profile.");
223
301
  await deps.reloadConfig();
224
- if (deps.getProfileKey() !== profile) throw new Error("Telegram Thread display setting changed profile.");
302
+ if (deps.getProfileKey() !== profile)
303
+ throw new Error("Telegram Thread display setting changed profile.");
225
304
  }
226
305
 
227
306
  export interface TelegramThreadDisplayReconcilerDeps {
228
- store: Pick<TelegramTopicTargetStore,
229
- "listWorkspaceBindings" | "setWorkspaceDisplayTitle" | "persist">;
307
+ store: Pick<
308
+ TelegramTopicTargetStore,
309
+ "listWorkspaceBindings" | "setWorkspaceDisplayTitle" | "persist"
310
+ >;
230
311
  getMode(): TelegramThreadDisplayMode;
231
312
  getProfileKey(): string;
232
313
  getLeaderEpoch(): string | number | undefined;
233
- captureBindingAuthority(binding: TelegramWorkspaceThreadBinding): (() => boolean) | undefined;
234
- captureLiveBindingKeys(bindings: readonly TelegramWorkspaceThreadBinding[]): ReadonlySet<string>;
314
+ captureBindingAuthority(
315
+ binding: TelegramWorkspaceThreadBinding,
316
+ ): (() => boolean) | undefined;
317
+ captureLiveBindingKeys(
318
+ bindings: readonly TelegramWorkspaceThreadBinding[],
319
+ ): ReadonlySet<string>;
235
320
  callApi<TResponse>(
236
321
  method: string,
237
322
  body: Record<string, unknown>,
@@ -241,11 +326,15 @@ export interface TelegramThreadDisplayReconcilerDeps {
241
326
 
242
327
  export function createTelegramThreadDisplaySettingsRuntime(deps: {
243
328
  getTarget(): { chatId: number; threadId?: number } | undefined;
244
- getBinding(target: { chatId: number; threadId?: number }):
245
- { manualThreadName?: string } | undefined;
329
+ getBinding(target: {
330
+ chatId: number;
331
+ threadId?: number;
332
+ }): { manualThreadName?: string } | undefined;
246
333
  apply(mode: TelegramThreadDisplayMode): Promise<void>;
247
- reset(target: { chatId: number; threadId?: number }):
248
- Promise<{ ok: boolean; message?: string }>;
334
+ reset(target: {
335
+ chatId: number;
336
+ threadId?: number;
337
+ }): Promise<{ ok: boolean; message?: string }>;
249
338
  }): {
250
339
  isCustom(): boolean;
251
340
  setMode(mode: TelegramThreadDisplayMode): Promise<void>;
@@ -253,17 +342,23 @@ export function createTelegramThreadDisplaySettingsRuntime(deps: {
253
342
  return {
254
343
  isCustom() {
255
344
  const target = deps.getTarget();
256
- return !!target && typeof deps.getBinding(target)?.manualThreadName === "string";
345
+ return (
346
+ !!target &&
347
+ typeof deps.getBinding(target)?.manualThreadName === "string"
348
+ );
257
349
  },
258
350
  async setMode(mode) {
259
351
  const target = deps.getTarget();
260
- const hadManualName = !!target &&
352
+ const hadManualName =
353
+ !!target &&
261
354
  typeof deps.getBinding(target)?.manualThreadName === "string";
262
355
  await deps.apply(mode);
263
356
  if (!hadManualName || !target) return;
264
357
  const reset = await deps.reset(target);
265
358
  if (!reset.ok) {
266
- throw new Error(reset.message ?? "Telegram Thread display override reset failed.");
359
+ throw new Error(
360
+ reset.message ?? "Telegram Thread display override reset failed.",
361
+ );
267
362
  }
268
363
  },
269
364
  };
@@ -279,40 +374,61 @@ export function createTelegramThreadDisplayReconciler(
279
374
  const profile = deps.getProfileKey();
280
375
  const mode = deps.getMode();
281
376
  const assertAuthority = (): void => {
282
- if (epoch === undefined || deps.getLeaderEpoch() !== epoch ||
283
- deps.getProfileKey() !== profile || deps.getMode() !== mode) {
284
- throw new Error("Telegram Thread display update lost profile, mode, or leader authority.");
377
+ if (
378
+ epoch === undefined ||
379
+ deps.getLeaderEpoch() !== epoch ||
380
+ deps.getProfileKey() !== profile ||
381
+ deps.getMode() !== mode
382
+ ) {
383
+ throw new Error(
384
+ "Telegram Thread display update lost profile, mode, or leader authority.",
385
+ );
285
386
  }
286
387
  };
287
388
  assertAuthority();
288
389
  const bindings = deps.store.listWorkspaceBindings();
289
390
  const liveBindingKeys = deps.captureLiveBindingKeys(bindings);
290
- const titles = resolveTelegramWorkspaceDisplayNames(bindings, mode, liveBindingKeys);
391
+ const titles = resolveTelegramWorkspaceDisplayNames(
392
+ bindings,
393
+ mode,
394
+ liveBindingKeys,
395
+ );
291
396
  let changed = 0;
292
397
  for (const binding of bindings) {
293
398
  const isBindingCurrent = deps.captureBindingAuthority(binding);
294
399
  if (!isBindingCurrent) continue;
295
400
  const title = titles.get(binding.bindingKey);
296
- if (!title) throw new Error("Telegram Thread display identity is missing or ambiguous.");
297
- const assertBinding = (expected: TelegramWorkspaceThreadBinding): void => {
298
- assertAuthority();
299
- const current = deps.store.listWorkspaceBindings().find((candidate) =>
300
- candidate.bindingKey === expected.bindingKey,
401
+ if (!title)
402
+ throw new Error(
403
+ "Telegram Thread display identity is missing or ambiguous.",
301
404
  );
405
+ const assertBinding = (
406
+ expected: TelegramWorkspaceThreadBinding,
407
+ ): void => {
408
+ assertAuthority();
409
+ const current = deps.store
410
+ .listWorkspaceBindings()
411
+ .find((candidate) => candidate.bindingKey === expected.bindingKey);
302
412
  if (!isDeepStrictEqual(current, expected) || !isBindingCurrent()) {
303
413
  throw new Error("Telegram Thread display binding changed.");
304
414
  }
305
415
  };
306
416
  assertBinding(binding);
307
417
  if ((binding.displayTitle ?? binding.threadName) === title) continue;
308
- await deps.callApi("editForumTopic", {
309
- chat_id: binding.target.chatId,
310
- message_thread_id: binding.target.threadId,
311
- name: title,
312
- }, { maxAttempts: 1 });
418
+ await deps.callApi(
419
+ "editForumTopic",
420
+ {
421
+ chat_id: binding.target.chatId,
422
+ message_thread_id: binding.target.threadId,
423
+ name: title,
424
+ },
425
+ { maxAttempts: 1 },
426
+ );
313
427
  assertBinding(binding);
314
428
  if (!deps.store.setWorkspaceDisplayTitle(binding, title)) {
315
- throw new Error("Telegram Thread display binding changed before title commit.");
429
+ throw new Error(
430
+ "Telegram Thread display binding changed before title commit.",
431
+ );
316
432
  }
317
433
  await deps.store.persist();
318
434
  assertBinding({ ...binding, displayTitle: title });