@llblab/pi-telegram 0.52.2 → 0.53.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 (204) hide show
  1. package/AGENTS.md +9 -7
  2. package/BACKLOG.md +1 -18
  3. package/CHANGELOG.md +15 -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 +65 -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 +252 -127
  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 +128 -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 +516 -301
  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,7 +5,10 @@
5
5
  * Excludes occupancy/slot allocation, display-mode projection, transport and API/store effects.
6
6
  */
7
7
 
8
- import { areTelegramTargetsEqual as sameTarget, type TelegramTarget } from "./target.ts";
8
+ import {
9
+ areTelegramTargetsEqual as sameTarget,
10
+ type TelegramTarget,
11
+ } from "./target.ts";
9
12
 
10
13
  export const TELEGRAM_THREAD_NAME_DIALOG_TTL_MS = 5 * 60_000;
11
14
 
@@ -29,10 +32,39 @@ function cloneCandidate(
29
32
  return { ...candidate, target: { ...candidate.target } };
30
33
  }
31
34
 
35
+ export interface TelegramThreadNameDialogLifetime {
36
+ /** Independent recipient effect authority; never the input handle's deadline or ownership. */
37
+ readonly assertAuthority?: () => void;
38
+ isCurrent(): boolean;
39
+ publish(
40
+ dialogMessageId: number,
41
+ assertPublicationCurrent?: () => void,
42
+ ): TelegramThreadNameDialogCandidate | undefined;
43
+ select(action: TelegramThreadNameDialogAction): {
44
+ kind: "reset" | "cancel" | "expired";
45
+ };
46
+ consumeName(
47
+ text: string,
48
+ ): { kind: "name"; name: string } | { kind: "none" | "empty" };
49
+ reopen(): TelegramThreadNameDialogCandidate | undefined;
50
+ finish(): void;
51
+ }
52
+
32
53
  export function createTelegramThreadNameDialogRuntime(options?: {
33
54
  ttlMs?: number;
34
55
  nowMs?: () => number;
35
56
  }): {
57
+ prepare(input: {
58
+ scope: string;
59
+ target: TelegramTarget;
60
+ isCurrent?: () => boolean;
61
+ assertAuthority?: () => void;
62
+ }): TelegramThreadNameDialogLifetime | undefined;
63
+ capture(input: {
64
+ scope: string;
65
+ target: TelegramTarget;
66
+ dialogMessageId: number;
67
+ }): TelegramThreadNameDialogLifetime | undefined;
36
68
  open(input: {
37
69
  scope: string;
38
70
  target: TelegramTarget;
@@ -50,65 +82,231 @@ export function createTelegramThreadNameDialogRuntime(options?: {
50
82
  text: string;
51
83
  }): { kind: "name"; name: string } | { kind: "none" | "empty" };
52
84
  clearScope(scope: string): void;
53
- inspect(target: TelegramTarget): TelegramThreadNameDialogCandidate | undefined;
85
+ inspect(
86
+ target: TelegramTarget,
87
+ ): TelegramThreadNameDialogCandidate | undefined;
54
88
  } {
55
89
  const ttlMs = options?.ttlMs ?? TELEGRAM_THREAD_NAME_DIALOG_TTL_MS;
56
90
  const nowMs = options?.nowMs ?? Date.now;
57
- const candidates = new Map<string, TelegramThreadNameDialogCandidate>();
91
+ interface Entry {
92
+ scope: string;
93
+ target: TelegramTarget;
94
+ dialogMessageId?: number;
95
+ phase: "delivery" | "input" | "consumed";
96
+ expiresAtMs: number;
97
+ isCurrent?: () => boolean;
98
+ assertAuthority?: () => void;
99
+ consumedBy?: object;
100
+ }
101
+ const candidates = new Map<string, Entry>();
102
+ const recipientCurrent = (entry: Entry): boolean => {
103
+ try {
104
+ return entry.isCurrent ? entry.isCurrent() === true : true;
105
+ } catch {
106
+ return false;
107
+ }
108
+ };
109
+ const view = (entry: Entry): TelegramThreadNameDialogCandidate =>
110
+ cloneCandidate({
111
+ scope: entry.scope,
112
+ target: entry.target,
113
+ dialogMessageId: entry.dialogMessageId!,
114
+ phase: "input",
115
+ expiresAtMs: entry.expiresAtMs,
116
+ });
117
+ const createLifetime = (entry: Entry): TelegramThreadNameDialogLifetime => {
118
+ const key = targetKey(entry.target);
119
+ const claim = {};
120
+ let ended = false;
121
+ const finish = () => {
122
+ ended = true;
123
+ if (
124
+ candidates.get(key) === entry &&
125
+ (!entry.consumedBy || entry.consumedBy === claim)
126
+ )
127
+ candidates.delete(key);
128
+ };
129
+ const isCurrent = () => {
130
+ if (
131
+ ended ||
132
+ candidates.get(key) !== entry ||
133
+ (entry.consumedBy && entry.consumedBy !== claim)
134
+ ) {
135
+ ended = true;
136
+ return false;
137
+ }
138
+ const current = entry.expiresAtMs > nowMs() && recipientCurrent(entry);
139
+ // Supplied authority/clock observations may synchronously replace this dialog.
140
+ if (
141
+ !current ||
142
+ entry.expiresAtMs <= nowMs() ||
143
+ candidates.get(key) !== entry ||
144
+ (entry.consumedBy && entry.consumedBy !== claim)
145
+ ) {
146
+ finish();
147
+ return false;
148
+ }
149
+ return true;
150
+ };
151
+ return {
152
+ assertAuthority: entry.assertAuthority,
153
+ isCurrent,
154
+ publish(dialogMessageId, assertPublicationCurrent) {
155
+ if (!isCurrent() || entry.phase !== "delivery") return undefined;
156
+ // Source authority is needed for this publication only, never retained by future input/effects.
157
+ if (assertPublicationCurrent) {
158
+ assertPublicationCurrent();
159
+ if (entry.expiresAtMs <= nowMs()) return undefined;
160
+ assertPublicationCurrent();
161
+ if (
162
+ ended ||
163
+ candidates.get(key) !== entry ||
164
+ entry.phase !== "delivery"
165
+ )
166
+ return undefined;
167
+ }
168
+ if (!Number.isSafeInteger(dialogMessageId) || dialogMessageId <= 0) {
169
+ finish();
170
+ return undefined;
171
+ }
172
+ entry.dialogMessageId = dialogMessageId;
173
+ entry.phase = "input";
174
+ return view(entry);
175
+ },
176
+ select(action) {
177
+ if (!isCurrent() || entry.phase !== "input") return { kind: "expired" };
178
+ if (action === "cancel") finish();
179
+ else {
180
+ entry.phase = "consumed";
181
+ entry.consumedBy = claim;
182
+ }
183
+ return { kind: action };
184
+ },
185
+ consumeName(text) {
186
+ if (!isCurrent() || entry.phase !== "input") return { kind: "none" };
187
+ const name = text.trim();
188
+ if (!name) return { kind: "empty" };
189
+ entry.phase = "consumed";
190
+ entry.consumedBy = claim;
191
+ return { kind: "name", name };
192
+ },
193
+ reopen() {
194
+ if (
195
+ !isCurrent() ||
196
+ entry.phase !== "consumed" ||
197
+ entry.consumedBy !== claim
198
+ ) {
199
+ return undefined;
200
+ }
201
+ // A failed effect may restore input, never renew its deadline or issued handle.
202
+ const replacement: Entry = {
203
+ ...entry,
204
+ phase: "input",
205
+ consumedBy: undefined,
206
+ };
207
+ candidates.set(key, replacement);
208
+ ended = true;
209
+ return view(replacement);
210
+ },
211
+ finish,
212
+ };
213
+ };
58
214
  const current = (
59
215
  scope: string,
60
216
  target: TelegramTarget,
61
- ): TelegramThreadNameDialogCandidate | undefined => {
62
- const key = targetKey(target);
63
- const candidate = candidates.get(key);
64
- if (!candidate || candidate.scope !== scope ||
65
- !sameTarget(candidate.target, target)) return undefined;
66
- if (candidate.expiresAtMs <= nowMs()) {
67
- candidates.delete(key);
217
+ ): Entry | undefined => {
218
+ const entry = candidates.get(targetKey(target));
219
+ if (
220
+ !entry ||
221
+ entry.scope !== scope ||
222
+ entry.phase !== "input" ||
223
+ !sameTarget(entry.target, target) ||
224
+ !createLifetime(entry).isCurrent()
225
+ )
68
226
  return undefined;
69
- }
70
- return candidate;
227
+ return entry;
228
+ };
229
+ const capture = (input: {
230
+ scope: string;
231
+ target: TelegramTarget;
232
+ dialogMessageId: number;
233
+ }): TelegramThreadNameDialogLifetime | undefined => {
234
+ const { scope, dialogMessageId } = input;
235
+ const target = { ...input.target };
236
+ const entry = current(scope, target);
237
+ if (!entry || entry.dialogMessageId !== dialogMessageId) return undefined;
238
+ return createLifetime(entry);
71
239
  };
72
240
  return {
241
+ prepare(input) {
242
+ const scope = input.scope;
243
+ const target = { ...input.target };
244
+ const isCurrent = input.isCurrent,
245
+ assertAuthority = input.assertAuthority;
246
+ const key = targetKey(target);
247
+ const previous = candidates.get(key);
248
+ const entry: Entry = {
249
+ scope,
250
+ target,
251
+ phase: "delivery",
252
+ expiresAtMs: nowMs() + ttlMs,
253
+ isCurrent,
254
+ assertAuthority,
255
+ };
256
+ if (
257
+ !recipientCurrent(entry) ||
258
+ entry.expiresAtMs <= nowMs() ||
259
+ candidates.get(key) !== previous
260
+ )
261
+ return undefined;
262
+ candidates.set(key, entry);
263
+ return createLifetime(entry);
264
+ },
265
+ capture,
73
266
  open(input) {
74
- const candidate: TelegramThreadNameDialogCandidate = {
267
+ const entry: Entry = {
75
268
  scope: input.scope,
76
269
  target: { ...input.target },
77
- dialogMessageId: input.dialogMessageId,
78
270
  phase: "input",
271
+ dialogMessageId: input.dialogMessageId,
79
272
  expiresAtMs: nowMs() + ttlMs,
80
273
  };
81
- candidates.set(targetKey(input.target), candidate);
82
- return cloneCandidate(candidate);
274
+ candidates.set(targetKey(entry.target), entry);
275
+ return view(entry);
83
276
  },
84
277
  select(input) {
85
- const candidate = current(input.scope, input.target);
86
- if (!candidate || candidate.dialogMessageId !== input.dialogMessageId) {
87
- return { kind: "expired" };
88
- }
89
- candidates.delete(targetKey(input.target));
90
- return { kind: input.action };
278
+ const action = input.action;
279
+ const lifetime = capture(input);
280
+ if (!lifetime) return { kind: "expired" };
281
+ const result = lifetime.select(action);
282
+ lifetime.finish();
283
+ return result;
91
284
  },
92
285
  consumeName(input) {
93
- const candidate = current(input.scope, input.target);
94
- if (!candidate || candidate.phase !== "input") return { kind: "none" };
95
- const name = input.text.trim();
96
- if (!name) return { kind: "empty" };
97
- candidates.delete(targetKey(input.target));
98
- return { kind: "name", name };
286
+ const text = input.text;
287
+ const entry = current(input.scope, input.target);
288
+ if (!entry) return { kind: "none" };
289
+ const lifetime = createLifetime(entry);
290
+ const result = lifetime.consumeName(text);
291
+ if (result.kind === "name") lifetime.finish();
292
+ return result;
99
293
  },
100
294
  clearScope(scope) {
101
- for (const [key, candidate] of candidates) {
102
- if (candidate.scope === scope) candidates.delete(key);
295
+ for (const [key, entry] of candidates) {
296
+ if (entry.scope === scope) candidates.delete(key);
103
297
  }
104
298
  },
105
299
  inspect(target) {
106
- const candidate = candidates.get(targetKey(target));
107
- if (!candidate || candidate.expiresAtMs <= nowMs()) {
108
- candidates.delete(targetKey(target));
300
+ const key = targetKey(target);
301
+ const entry = candidates.get(key);
302
+ if (!entry) return undefined;
303
+ if (entry.expiresAtMs <= nowMs()) {
304
+ if (candidates.get(key) === entry) candidates.delete(key);
109
305
  return undefined;
110
306
  }
111
- return cloneCandidate(candidate);
307
+ if (entry.phase !== "input" || !createLifetime(entry).isCurrent())
308
+ return undefined;
309
+ return view(entry);
112
310
  },
113
311
  };
114
312
  }
@@ -119,59 +317,6 @@ export interface TelegramThreadTitleInput {
119
317
  threadName?: string;
120
318
  }
121
319
 
122
- export interface TelegramThreadNameInput {
123
- seed: string;
124
- cwd?: string;
125
- role?: "leader" | "follower";
126
- peers?: readonly string[];
127
- slot?: string;
128
- }
129
-
130
- function hashString(value: string): number {
131
- let hash = 2166136261;
132
- for (let index = 0; index < value.length; index += 1) {
133
- hash ^= value.charCodeAt(index);
134
- hash = Math.imul(hash, 16777619);
135
- }
136
- return hash >>> 0;
137
- }
138
-
139
- function getWorkspaceHint(cwd: string | undefined): string | undefined {
140
- if (!cwd) return undefined;
141
- const parts = cwd.split("/").filter(Boolean);
142
- const last = parts.at(-1)?.trim();
143
- if (!last) return undefined;
144
- return (
145
- last
146
- .replace(/[^\p{L}\p{N}._-]+/gu, " ")
147
- .trim()
148
- .slice(0, 32) || undefined
149
- );
150
- }
151
-
152
- export function createTelegramThreadName(
153
- input: TelegramThreadNameInput,
154
- ): string {
155
- const workspace = getWorkspaceHint(input.cwd);
156
- const roleMark =
157
- input.role === "leader"
158
- ? "Leader"
159
- : input.role === "follower"
160
- ? "Follower"
161
- : undefined;
162
- const slot = input.slot ? `Thread ${input.slot}` : undefined;
163
- const peerSalt = input.peers?.slice().sort().join("|") ?? "";
164
- const fallback = `Instance ${hashString(
165
- `${input.seed}|${input.cwd ?? ""}|${input.role ?? ""}|${peerSalt}|${input.slot ?? ""}`,
166
- )
167
- .toString(36)
168
- .slice(0, 4)}`;
169
- return (
170
- [slot, workspace, roleMark].filter(Boolean).join(" ").slice(0, 96) ||
171
- fallback
172
- );
173
- }
174
-
175
320
  export function normalizeTelegramTopicTargetThreadName(
176
321
  threadName: string,
177
322
  ): string {