@llblab/pi-kit 0.29.2 → 0.30.1

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 (208) hide show
  1. package/BACKLOG.md +2 -1
  2. package/CHANGELOG.md +9 -0
  3. package/README.md +2 -2
  4. package/node_modules/@llblab/pi-telegram/AGENTS.md +9 -7
  5. package/node_modules/@llblab/pi-telegram/BACKLOG.md +1 -18
  6. package/node_modules/@llblab/pi-telegram/CHANGELOG.md +15 -0
  7. package/node_modules/@llblab/pi-telegram/README.md +7 -11
  8. package/node_modules/@llblab/pi-telegram/api/commands.ts +3 -0
  9. package/node_modules/@llblab/pi-telegram/dist/api/commands.d.ts +1 -1
  10. package/node_modules/@llblab/pi-telegram/dist/lib/activity-verbosity.d.ts +1 -5
  11. package/node_modules/@llblab/pi-telegram/dist/lib/activity-verbosity.js +15 -42
  12. package/node_modules/@llblab/pi-telegram/dist/lib/activity.d.ts +9 -0
  13. package/node_modules/@llblab/pi-telegram/dist/lib/activity.js +39 -10
  14. package/node_modules/@llblab/pi-telegram/dist/lib/agent-messages.js +7 -3
  15. package/node_modules/@llblab/pi-telegram/dist/lib/bindings.d.ts +43 -5
  16. package/node_modules/@llblab/pi-telegram/dist/lib/bindings.js +163 -20
  17. package/node_modules/@llblab/pi-telegram/dist/lib/bus-api.d.ts +12 -0
  18. package/node_modules/@llblab/pi-telegram/dist/lib/bus-api.js +182 -79
  19. package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.d.ts +111 -31
  20. package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.js +1324 -359
  21. package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.d.ts +41 -91
  22. package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.js +969 -637
  23. package/node_modules/@llblab/pi-telegram/dist/lib/bus-transport.js +16 -9
  24. package/node_modules/@llblab/pi-telegram/dist/lib/bus.d.ts +215 -18
  25. package/node_modules/@llblab/pi-telegram/dist/lib/bus.js +1048 -327
  26. package/node_modules/@llblab/pi-telegram/dist/lib/channel-posts.d.ts +5 -0
  27. package/node_modules/@llblab/pi-telegram/dist/lib/channel-posts.js +434 -261
  28. package/node_modules/@llblab/pi-telegram/dist/lib/command-templates.d.ts +26 -5
  29. package/node_modules/@llblab/pi-telegram/dist/lib/command-templates.js +92 -176
  30. package/node_modules/@llblab/pi-telegram/dist/lib/commands.d.ts +168 -82
  31. package/node_modules/@llblab/pi-telegram/dist/lib/commands.js +1409 -291
  32. package/node_modules/@llblab/pi-telegram/dist/lib/config.d.ts +1 -8
  33. package/node_modules/@llblab/pi-telegram/dist/lib/config.js +34 -76
  34. package/node_modules/@llblab/pi-telegram/dist/lib/delivery.d.ts +2 -2
  35. package/node_modules/@llblab/pi-telegram/dist/lib/delivery.js +16 -16
  36. package/node_modules/@llblab/pi-telegram/dist/lib/extension.js +639 -171
  37. package/node_modules/@llblab/pi-telegram/dist/lib/generative-app-worker.mjs +19 -7
  38. package/node_modules/@llblab/pi-telegram/dist/lib/generative-apps.d.ts +0 -5
  39. package/node_modules/@llblab/pi-telegram/dist/lib/generative-apps.js +61 -49
  40. package/node_modules/@llblab/pi-telegram/dist/lib/inbound.d.ts +5 -0
  41. package/node_modules/@llblab/pi-telegram/dist/lib/inbound.js +35 -59
  42. package/node_modules/@llblab/pi-telegram/dist/lib/journal.d.ts +33 -3
  43. package/node_modules/@llblab/pi-telegram/dist/lib/journal.js +2063 -782
  44. package/node_modules/@llblab/pi-telegram/dist/lib/lifecycle.d.ts +2 -6
  45. package/node_modules/@llblab/pi-telegram/dist/lib/lifecycle.js +75 -58
  46. package/node_modules/@llblab/pi-telegram/dist/lib/locks.d.ts +29 -3
  47. package/node_modules/@llblab/pi-telegram/dist/lib/locks.js +289 -108
  48. package/node_modules/@llblab/pi-telegram/dist/lib/logging.js +50 -14
  49. package/node_modules/@llblab/pi-telegram/dist/lib/media.d.ts +22 -1
  50. package/node_modules/@llblab/pi-telegram/dist/lib/media.js +46 -28
  51. package/node_modules/@llblab/pi-telegram/dist/lib/menu-model.d.ts +19 -5
  52. package/node_modules/@llblab/pi-telegram/dist/lib/menu-model.js +53 -11
  53. package/node_modules/@llblab/pi-telegram/dist/lib/menu-queue.d.ts +5 -4
  54. package/node_modules/@llblab/pi-telegram/dist/lib/menu-queue.js +45 -63
  55. package/node_modules/@llblab/pi-telegram/dist/lib/menu-settings.d.ts +5 -4
  56. package/node_modules/@llblab/pi-telegram/dist/lib/menu-settings.js +114 -78
  57. package/node_modules/@llblab/pi-telegram/dist/lib/menu-status.d.ts +1 -1
  58. package/node_modules/@llblab/pi-telegram/dist/lib/menu-status.js +8 -25
  59. package/node_modules/@llblab/pi-telegram/dist/lib/menu-thinking.d.ts +6 -1
  60. package/node_modules/@llblab/pi-telegram/dist/lib/menu-thinking.js +14 -21
  61. package/node_modules/@llblab/pi-telegram/dist/lib/menu.d.ts +8 -61
  62. package/node_modules/@llblab/pi-telegram/dist/lib/menu.js +92 -164
  63. package/node_modules/@llblab/pi-telegram/dist/lib/model.d.ts +1 -1
  64. package/node_modules/@llblab/pi-telegram/dist/lib/model.js +2 -16
  65. package/node_modules/@llblab/pi-telegram/dist/lib/outbound-attachments.d.ts +6 -27
  66. package/node_modules/@llblab/pi-telegram/dist/lib/outbound-attachments.js +45 -38
  67. package/node_modules/@llblab/pi-telegram/dist/lib/outbound-buttons.js +22 -16
  68. package/node_modules/@llblab/pi-telegram/dist/lib/outbound-markup.d.ts +0 -2
  69. package/node_modules/@llblab/pi-telegram/dist/lib/outbound-markup.js +51 -64
  70. package/node_modules/@llblab/pi-telegram/dist/lib/outbound-voice.js +6 -3
  71. package/node_modules/@llblab/pi-telegram/dist/lib/outbound.d.ts +6 -6
  72. package/node_modules/@llblab/pi-telegram/dist/lib/outbound.js +65 -51
  73. package/node_modules/@llblab/pi-telegram/dist/lib/ownership.js +6 -2
  74. package/node_modules/@llblab/pi-telegram/dist/lib/paths.d.ts +5 -0
  75. package/node_modules/@llblab/pi-telegram/dist/lib/paths.js +37 -21
  76. package/node_modules/@llblab/pi-telegram/dist/lib/pi.d.ts +2 -0
  77. package/node_modules/@llblab/pi-telegram/dist/lib/pi.js +8 -1
  78. package/node_modules/@llblab/pi-telegram/dist/lib/polling.d.ts +21 -37
  79. package/node_modules/@llblab/pi-telegram/dist/lib/polling.js +59 -50
  80. package/node_modules/@llblab/pi-telegram/dist/lib/preview.js +39 -19
  81. package/node_modules/@llblab/pi-telegram/dist/lib/process-identity.d.ts +24 -0
  82. package/node_modules/@llblab/pi-telegram/dist/lib/process-identity.js +133 -0
  83. package/node_modules/@llblab/pi-telegram/dist/lib/prompts.js +2 -7
  84. package/node_modules/@llblab/pi-telegram/dist/lib/queue.d.ts +14 -16
  85. package/node_modules/@llblab/pi-telegram/dist/lib/queue.js +252 -127
  86. package/node_modules/@llblab/pi-telegram/dist/lib/recovery.d.ts +2 -83
  87. package/node_modules/@llblab/pi-telegram/dist/lib/recovery.js +20 -274
  88. package/node_modules/@llblab/pi-telegram/dist/lib/replies.d.ts +16 -24
  89. package/node_modules/@llblab/pi-telegram/dist/lib/replies.js +99 -116
  90. package/node_modules/@llblab/pi-telegram/dist/lib/routing.d.ts +120 -7
  91. package/node_modules/@llblab/pi-telegram/dist/lib/routing.js +4667 -1152
  92. package/node_modules/@llblab/pi-telegram/dist/lib/runtime.d.ts +1 -11
  93. package/node_modules/@llblab/pi-telegram/dist/lib/sections.d.ts +4 -29
  94. package/node_modules/@llblab/pi-telegram/dist/lib/sections.js +7 -20
  95. package/node_modules/@llblab/pi-telegram/dist/lib/skills.js +4 -3
  96. package/node_modules/@llblab/pi-telegram/dist/lib/status.js +86 -57
  97. package/node_modules/@llblab/pi-telegram/dist/lib/sync.d.ts +1 -3
  98. package/node_modules/@llblab/pi-telegram/dist/lib/sync.js +67 -43
  99. package/node_modules/@llblab/pi-telegram/dist/lib/target.d.ts +4 -0
  100. package/node_modules/@llblab/pi-telegram/dist/lib/target.js +18 -0
  101. package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.d.ts +18 -13
  102. package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.js +148 -53
  103. package/node_modules/@llblab/pi-telegram/dist/lib/text-groups.js +4 -39
  104. package/node_modules/@llblab/pi-telegram/dist/lib/thread-cleanup-manager.d.ts +6 -27
  105. package/node_modules/@llblab/pi-telegram/dist/lib/thread-cleanup-manager.js +369 -186
  106. package/node_modules/@llblab/pi-telegram/dist/lib/thread-display.d.ts +1 -2
  107. package/node_modules/@llblab/pi-telegram/dist/lib/thread-display.js +58 -38
  108. package/node_modules/@llblab/pi-telegram/dist/lib/thread-naming.d.ts +28 -8
  109. package/node_modules/@llblab/pi-telegram/dist/lib/thread-naming.js +175 -66
  110. package/node_modules/@llblab/pi-telegram/dist/lib/thread-reconciler.d.ts +42 -16
  111. package/node_modules/@llblab/pi-telegram/dist/lib/thread-reconciler.js +90 -50
  112. package/node_modules/@llblab/pi-telegram/dist/lib/threads.d.ts +82 -20
  113. package/node_modules/@llblab/pi-telegram/dist/lib/threads.js +2247 -855
  114. package/node_modules/@llblab/pi-telegram/dist/lib/time-injection.js +1 -2
  115. package/node_modules/@llblab/pi-telegram/dist/lib/turns.js +1 -19
  116. package/node_modules/@llblab/pi-telegram/dist/lib/updates.d.ts +169 -80
  117. package/node_modules/@llblab/pi-telegram/dist/lib/updates.js +2418 -641
  118. package/node_modules/@llblab/pi-telegram/dist/lib/voice.d.ts +7 -22
  119. package/node_modules/@llblab/pi-telegram/dist/lib/voice.js +7 -22
  120. package/node_modules/@llblab/pi-telegram/dist/lib/wire.js +1 -1
  121. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-admission.d.ts +3 -1
  122. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-admission.js +425 -211
  123. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-identity.js +8 -13
  124. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-retirement.d.ts +1 -1
  125. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-retirement.js +328 -140
  126. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-slots.js +8 -4
  127. package/node_modules/@llblab/pi-telegram/dist/package.json +1 -1
  128. package/node_modules/@llblab/pi-telegram/docs/architecture.md +31 -29
  129. package/node_modules/@llblab/pi-telegram/docs/inbound.md +1 -1
  130. package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +15 -3
  131. package/node_modules/@llblab/pi-telegram/docs/outbound.md +1 -1
  132. package/node_modules/@llblab/pi-telegram/docs/public-api.md +29 -1
  133. package/node_modules/@llblab/pi-telegram/docs/ui-style.md +25 -8
  134. package/node_modules/@llblab/pi-telegram/lib/activity-verbosity.ts +59 -82
  135. package/node_modules/@llblab/pi-telegram/lib/activity.ts +146 -102
  136. package/node_modules/@llblab/pi-telegram/lib/agent-messages.ts +12 -5
  137. package/node_modules/@llblab/pi-telegram/lib/bindings.ts +389 -82
  138. package/node_modules/@llblab/pi-telegram/lib/bus-api.ts +263 -102
  139. package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +2440 -601
  140. package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +2209 -1290
  141. package/node_modules/@llblab/pi-telegram/lib/bus-transport.ts +46 -12
  142. package/node_modules/@llblab/pi-telegram/lib/bus.ts +1673 -444
  143. package/node_modules/@llblab/pi-telegram/lib/channel-posts.ts +848 -327
  144. package/node_modules/@llblab/pi-telegram/lib/command-templates.ts +152 -265
  145. package/node_modules/@llblab/pi-telegram/lib/commands.ts +2398 -555
  146. package/node_modules/@llblab/pi-telegram/lib/config.ts +177 -170
  147. package/node_modules/@llblab/pi-telegram/lib/delivery.ts +41 -29
  148. package/node_modules/@llblab/pi-telegram/lib/extension.ts +1206 -433
  149. package/node_modules/@llblab/pi-telegram/lib/generative-app-worker.mjs +19 -7
  150. package/node_modules/@llblab/pi-telegram/lib/generative-apps.ts +324 -211
  151. package/node_modules/@llblab/pi-telegram/lib/inbound.ts +95 -97
  152. package/node_modules/@llblab/pi-telegram/lib/journal.ts +5311 -1763
  153. package/node_modules/@llblab/pi-telegram/lib/keyboard.ts +1 -3
  154. package/node_modules/@llblab/pi-telegram/lib/lifecycle.ts +128 -78
  155. package/node_modules/@llblab/pi-telegram/lib/locks.ts +677 -203
  156. package/node_modules/@llblab/pi-telegram/lib/logging.ts +147 -58
  157. package/node_modules/@llblab/pi-telegram/lib/media.ts +82 -43
  158. package/node_modules/@llblab/pi-telegram/lib/menu-model.ts +85 -18
  159. package/node_modules/@llblab/pi-telegram/lib/menu-queue.ts +88 -129
  160. package/node_modules/@llblab/pi-telegram/lib/menu-settings.ts +260 -155
  161. package/node_modules/@llblab/pi-telegram/lib/menu-status.ts +20 -60
  162. package/node_modules/@llblab/pi-telegram/lib/menu-thinking.ts +41 -56
  163. package/node_modules/@llblab/pi-telegram/lib/menu.ts +112 -285
  164. package/node_modules/@llblab/pi-telegram/lib/model.ts +8 -22
  165. package/node_modules/@llblab/pi-telegram/lib/outbound-attachments.ts +156 -125
  166. package/node_modules/@llblab/pi-telegram/lib/outbound-buttons.ts +76 -49
  167. package/node_modules/@llblab/pi-telegram/lib/outbound-markup.ts +96 -94
  168. package/node_modules/@llblab/pi-telegram/lib/outbound-voice.ts +33 -27
  169. package/node_modules/@llblab/pi-telegram/lib/outbound.ts +128 -125
  170. package/node_modules/@llblab/pi-telegram/lib/ownership.ts +19 -10
  171. package/node_modules/@llblab/pi-telegram/lib/paths.ts +148 -49
  172. package/node_modules/@llblab/pi-telegram/lib/pi.ts +35 -15
  173. package/node_modules/@llblab/pi-telegram/lib/polling.ts +144 -147
  174. package/node_modules/@llblab/pi-telegram/lib/preview.ts +93 -40
  175. package/node_modules/@llblab/pi-telegram/lib/process-identity.ts +164 -0
  176. package/node_modules/@llblab/pi-telegram/lib/prompts.ts +4 -11
  177. package/node_modules/@llblab/pi-telegram/lib/queue.ts +516 -301
  178. package/node_modules/@llblab/pi-telegram/lib/recovery.ts +50 -454
  179. package/node_modules/@llblab/pi-telegram/lib/rendering.ts +1 -3
  180. package/node_modules/@llblab/pi-telegram/lib/replies.ts +248 -209
  181. package/node_modules/@llblab/pi-telegram/lib/routing.ts +8509 -2186
  182. package/node_modules/@llblab/pi-telegram/lib/runtime.ts +1 -12
  183. package/node_modules/@llblab/pi-telegram/lib/sections.ts +24 -89
  184. package/node_modules/@llblab/pi-telegram/lib/setup.ts +1 -3
  185. package/node_modules/@llblab/pi-telegram/lib/skills.ts +9 -4
  186. package/node_modules/@llblab/pi-telegram/lib/status.ts +215 -110
  187. package/node_modules/@llblab/pi-telegram/lib/sync.ts +448 -361
  188. package/node_modules/@llblab/pi-telegram/lib/target.ts +22 -3
  189. package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +302 -127
  190. package/node_modules/@llblab/pi-telegram/lib/text-groups.ts +5 -39
  191. package/node_modules/@llblab/pi-telegram/lib/thread-cleanup-manager.ts +833 -331
  192. package/node_modules/@llblab/pi-telegram/lib/thread-display.ts +213 -97
  193. package/node_modules/@llblab/pi-telegram/lib/thread-naming.ts +232 -87
  194. package/node_modules/@llblab/pi-telegram/lib/thread-reconciler.ts +155 -72
  195. package/node_modules/@llblab/pi-telegram/lib/threads.ts +5318 -1845
  196. package/node_modules/@llblab/pi-telegram/lib/time-injection.ts +1 -4
  197. package/node_modules/@llblab/pi-telegram/lib/turns.ts +15 -28
  198. package/node_modules/@llblab/pi-telegram/lib/updates.ts +5184 -1612
  199. package/node_modules/@llblab/pi-telegram/lib/voice.ts +7 -35
  200. package/node_modules/@llblab/pi-telegram/lib/wire.ts +1 -1
  201. package/node_modules/@llblab/pi-telegram/lib/workspace-admission.ts +996 -416
  202. package/node_modules/@llblab/pi-telegram/lib/workspace-identity.ts +14 -17
  203. package/node_modules/@llblab/pi-telegram/lib/workspace-retirement.ts +878 -376
  204. package/node_modules/@llblab/pi-telegram/lib/workspace-slots.ts +22 -13
  205. package/node_modules/@llblab/pi-telegram/package.json +1 -1
  206. package/package.json +2 -2
  207. package/node_modules/@llblab/pi-telegram/scripts/check-downgrade.mjs +0 -472
  208. package/node_modules/@llblab/pi-telegram/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 });