@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
@@ -1,6 +1,6 @@
1
1
  import type { TelegramThreadDisplayMode } from "./config.ts";
2
- import type { TelegramTopicTargetStore, TelegramWorkspaceDisplayBinding, TelegramWorkspaceThreadBinding } from "./threads.ts";
3
2
  import type { TelegramApiCallOptions } from "./telegram-api.ts";
3
+ import type { TelegramTopicTargetStore, TelegramWorkspaceDisplayBinding, TelegramWorkspaceThreadBinding } from "./threads.ts";
4
4
  /** Pure directory tokenization shared by previews, initial titles, and reconciliation. */
5
5
  export declare function tokenizeTelegramDirectorySegment(segment: string): string[];
6
6
  /** Maps one leader-captured authenticated owner roster onto retained binding identities. */
@@ -13,7 +13,6 @@ export declare function resolveTelegramLiveWorkspaceBindingKeys(bindings: readon
13
13
  threadId?: number;
14
14
  };
15
15
  }[]): ReadonlySet<string>;
16
- /** Missing or ambiguous metadata yields no label rather than inventing identity. */
17
16
  export declare function resolveTelegramWorkspaceDisplayNames(bindings: readonly TelegramWorkspaceDisplayBinding[], mode: TelegramThreadDisplayMode, liveBindingKeys?: ReadonlySet<string>): Map<string, string>;
18
17
  export declare function resolveTelegramInitialWorkspaceDisplayName(input: {
19
18
  bindings: readonly TelegramWorkspaceDisplayBinding[];
@@ -27,8 +27,7 @@ function distinguishingDirectorySegments(cwd, directories) {
27
27
  for (let depth = 1; depth <= parts.length; depth++) {
28
28
  const candidate = labelText(parts.slice(-depth).join("/"));
29
29
  const collides = directories.some((other) => other !== cwd &&
30
- labelText(directorySegments(other).slice(-depth).join("/")).toLowerCase() ===
31
- candidate.toLowerCase());
30
+ labelText(directorySegments(other).slice(-depth).join("/")).toLowerCase() === candidate.toLowerCase());
32
31
  if (!collides)
33
32
  return parts.slice(-depth);
34
33
  }
@@ -50,16 +49,20 @@ function formatDirectoryLabel(cwd, directories, mode) {
50
49
  const segments = distinguishingDirectorySegments(cwd, directories);
51
50
  if (!segments.length)
52
51
  return "/";
53
- const formatted = segments.map((segment) => {
52
+ const formatted = segments
53
+ .map((segment) => {
54
54
  const tokens = tokenizeTelegramDirectorySegment(segment);
55
55
  if (!tokens.length)
56
56
  return "";
57
57
  if (mode === "directory-snake")
58
58
  return tokens.map((token) => token.toLowerCase()).join("_");
59
- return tokens.map((token) => /\p{L}/u.test(token) && token === token.toUpperCase()
59
+ return tokens
60
+ .map((token) => /\p{L}/u.test(token) && token === token.toUpperCase()
60
61
  ? token
61
- : `${token.slice(0, 1).toUpperCase()}${token.slice(1).toLowerCase()}`).join(" ");
62
- }).filter(Boolean);
62
+ : `${token.slice(0, 1).toUpperCase()}${token.slice(1).toLowerCase()}`)
63
+ .join(" ");
64
+ })
65
+ .filter(Boolean);
63
66
  if (!formatted.length)
64
67
  return directoryLabel(cwd, directories);
65
68
  return formatted.join(mode === "directory-snake" ? "_" : " / ");
@@ -75,9 +78,25 @@ export function resolveTelegramLiveWorkspaceBindingKeys(bindings, leaderTarget,
75
78
  add(leaderTarget);
76
79
  for (const follower of followers)
77
80
  add(follower.target);
78
- return new Set(bindings.filter((binding) => targets.has(`${binding.target.chatId}:${binding.target.threadId}`)).map((binding) => binding.bindingKey));
81
+ return new Set(bindings
82
+ .filter((binding) => targets.has(`${binding.target.chatId}:${binding.target.threadId}`))
83
+ .map((binding) => binding.bindingKey));
79
84
  }
80
85
  /** Missing or ambiguous metadata yields no label rather than inventing identity. */
86
+ /** Case-insensitive label occurrences; directory modes count only live bindings. */
87
+ function countTelegramDisplayLabels(bindings, labels, mode, liveBindingKeys) {
88
+ const counts = new Map();
89
+ for (const binding of bindings) {
90
+ const label = labels.get(binding.bindingKey);
91
+ if (!label ||
92
+ ((mode === "directory-snake" || mode === "directory-title") &&
93
+ !liveBindingKeys.has(binding.bindingKey)))
94
+ continue;
95
+ const key = label.toLowerCase();
96
+ counts.set(key, (counts.get(key) ?? 0) + 1);
97
+ }
98
+ return counts;
99
+ }
81
100
  export function resolveTelegramWorkspaceDisplayNames(bindings, mode, liveBindingKeys = new Set()) {
82
101
  const labels = new Map();
83
102
  const directories = Array.from(new Set(bindings.map((binding) => binding.cwd)));
@@ -111,30 +130,29 @@ export function resolveTelegramWorkspaceDisplayNames(bindings, mode, liveBinding
111
130
  : formatDirectoryLabel(binding.cwd, directories, mode);
112
131
  bases.set(binding.bindingKey, base);
113
132
  const showSuffix = mode === "directories"
114
- ? binding.showSlotSuffix || bindings.filter((candidate) => candidate.cwd === binding.cwd).length > 1
133
+ ? binding.showSlotSuffix ||
134
+ bindings.filter((candidate) => candidate.cwd === binding.cwd)
135
+ .length > 1
115
136
  : (liveDirectoryCounts.get(binding.cwd) ?? 0) > 1;
116
137
  if (showSuffix && !slot)
117
138
  continue;
118
- const suffix = !showSuffix ? "" : mode === "directory-title"
119
- ? ` ${slot}`
120
- : `_${slot.toLowerCase()}`;
139
+ const suffix = !showSuffix
140
+ ? ""
141
+ : mode === "directory-title"
142
+ ? ` ${slot}`
143
+ : `_${slot.toLowerCase()}`;
121
144
  labels.set(binding.bindingKey, boundedLabel(base, suffix));
122
145
  }
123
146
  }
124
147
  // Long or whitespace-normalized paths can collide even after qualification.
125
- if (mode === "directories" || mode === "directory-snake" || mode === "directory-title") {
126
- const counts = new Map();
127
- for (const binding of bindings) {
128
- const label = labels.get(binding.bindingKey);
129
- if (!label || ((mode === "directory-snake" || mode === "directory-title") &&
130
- !liveBindingKeys.has(binding.bindingKey)))
131
- continue;
132
- const key = label.toLowerCase();
133
- counts.set(key, (counts.get(key) ?? 0) + 1);
134
- }
148
+ if (mode === "directories" ||
149
+ mode === "directory-snake" ||
150
+ mode === "directory-title") {
151
+ const counts = countTelegramDisplayLabels(bindings, labels, mode, liveBindingKeys);
135
152
  for (const binding of bindings) {
136
153
  const label = labels.get(binding.bindingKey);
137
- if (binding.manualThreadName || !label ||
154
+ if (binding.manualThreadName ||
155
+ !label ||
138
156
  (counts.get(label.toLowerCase()) ?? 0) < 2)
139
157
  continue;
140
158
  if ((mode === "directory-snake" || mode === "directory-title") &&
@@ -146,18 +164,12 @@ export function resolveTelegramWorkspaceDisplayNames(bindings, mode, liveBinding
146
164
  labels.delete(binding.bindingKey);
147
165
  continue;
148
166
  }
149
- labels.set(binding.bindingKey, boundedLabel(bases.get(binding.bindingKey), mode === "directory-title" ? ` ${binding.slot}` : `_${binding.slot.toLowerCase()}`));
167
+ labels.set(binding.bindingKey, boundedLabel(bases.get(binding.bindingKey), mode === "directory-title"
168
+ ? ` ${binding.slot}`
169
+ : `_${binding.slot.toLowerCase()}`));
150
170
  }
151
171
  }
152
- const counts = new Map();
153
- for (const binding of bindings) {
154
- const label = labels.get(binding.bindingKey);
155
- if (!label || ((mode === "directory-snake" || mode === "directory-title") &&
156
- !liveBindingKeys.has(binding.bindingKey)))
157
- continue;
158
- const key = label.toLowerCase();
159
- counts.set(key, (counts.get(key) ?? 0) + 1);
160
- }
172
+ const counts = countTelegramDisplayLabels(bindings, labels, mode, liveBindingKeys);
161
173
  for (const [key, label] of labels) {
162
174
  if ((counts.get(label.toLowerCase()) ?? 0) > 1)
163
175
  labels.delete(key);
@@ -170,7 +182,8 @@ export function resolveTelegramInitialWorkspaceDisplayName(input) {
170
182
  ? {
171
183
  ...input.binding,
172
184
  ...(retained.showSlotSuffix ? { showSlotSuffix: true } : {}),
173
- ...(input.preserveRetainedManualName !== false && retained.manualThreadName
185
+ ...(input.preserveRetainedManualName !== false &&
186
+ retained.manualThreadName
174
187
  ? { manualThreadName: retained.manualThreadName }
175
188
  : {}),
176
189
  }
@@ -182,7 +195,9 @@ export function resolveTelegramInitialWorkspaceDisplayName(input) {
182
195
  }
183
196
  export async function applyTelegramThreadDisplaySetting(mode, deps) {
184
197
  const profile = deps.getProfileKey();
185
- const setter = deps.ownsLeader() ? deps.getLeaderSetter() : deps.getFollowerSetter();
198
+ const setter = deps.ownsLeader()
199
+ ? deps.getLeaderSetter()
200
+ : deps.getFollowerSetter();
186
201
  if (!setter)
187
202
  throw new Error("Thread display settings require a connected compatible instance.");
188
203
  await setter(mode);
@@ -196,7 +211,8 @@ export function createTelegramThreadDisplaySettingsRuntime(deps) {
196
211
  return {
197
212
  isCustom() {
198
213
  const target = deps.getTarget();
199
- return !!target && typeof deps.getBinding(target)?.manualThreadName === "string";
214
+ return (!!target &&
215
+ typeof deps.getBinding(target)?.manualThreadName === "string");
200
216
  },
201
217
  async setMode(mode) {
202
218
  const target = deps.getTarget();
@@ -220,8 +236,10 @@ export function createTelegramThreadDisplayReconciler(deps) {
220
236
  const profile = deps.getProfileKey();
221
237
  const mode = deps.getMode();
222
238
  const assertAuthority = () => {
223
- if (epoch === undefined || deps.getLeaderEpoch() !== epoch ||
224
- deps.getProfileKey() !== profile || deps.getMode() !== mode) {
239
+ if (epoch === undefined ||
240
+ deps.getLeaderEpoch() !== epoch ||
241
+ deps.getProfileKey() !== profile ||
242
+ deps.getMode() !== mode) {
225
243
  throw new Error("Telegram Thread display update lost profile, mode, or leader authority.");
226
244
  }
227
245
  };
@@ -239,7 +257,9 @@ export function createTelegramThreadDisplayReconciler(deps) {
239
257
  throw new Error("Telegram Thread display identity is missing or ambiguous.");
240
258
  const assertBinding = (expected) => {
241
259
  assertAuthority();
242
- const current = deps.store.listWorkspaceBindings().find((candidate) => candidate.bindingKey === expected.bindingKey);
260
+ const current = deps.store
261
+ .listWorkspaceBindings()
262
+ .find((candidate) => candidate.bindingKey === expected.bindingKey);
243
263
  if (!isDeepStrictEqual(current, expected) || !isBindingCurrent()) {
244
264
  throw new Error("Telegram Thread display binding changed.");
245
265
  }
@@ -14,10 +14,38 @@ export interface TelegramThreadNameDialogCandidate {
14
14
  phase: "input";
15
15
  expiresAtMs: number;
16
16
  }
17
+ export interface TelegramThreadNameDialogLifetime {
18
+ /** Independent recipient effect authority; never the input handle's deadline or ownership. */
19
+ readonly assertAuthority?: () => void;
20
+ isCurrent(): boolean;
21
+ publish(dialogMessageId: number, assertPublicationCurrent?: () => void): TelegramThreadNameDialogCandidate | undefined;
22
+ select(action: TelegramThreadNameDialogAction): {
23
+ kind: "reset" | "cancel" | "expired";
24
+ };
25
+ consumeName(text: string): {
26
+ kind: "name";
27
+ name: string;
28
+ } | {
29
+ kind: "none" | "empty";
30
+ };
31
+ reopen(): TelegramThreadNameDialogCandidate | undefined;
32
+ finish(): void;
33
+ }
17
34
  export declare function createTelegramThreadNameDialogRuntime(options?: {
18
35
  ttlMs?: number;
19
36
  nowMs?: () => number;
20
37
  }): {
38
+ prepare(input: {
39
+ scope: string;
40
+ target: TelegramTarget;
41
+ isCurrent?: () => boolean;
42
+ assertAuthority?: () => void;
43
+ }): TelegramThreadNameDialogLifetime | undefined;
44
+ capture(input: {
45
+ scope: string;
46
+ target: TelegramTarget;
47
+ dialogMessageId: number;
48
+ }): TelegramThreadNameDialogLifetime | undefined;
21
49
  open(input: {
22
50
  scope: string;
23
51
  target: TelegramTarget;
@@ -49,14 +77,6 @@ export interface TelegramThreadTitleInput {
49
77
  profileKey: string;
50
78
  threadName?: string;
51
79
  }
52
- export interface TelegramThreadNameInput {
53
- seed: string;
54
- cwd?: string;
55
- role?: "leader" | "follower";
56
- peers?: readonly string[];
57
- slot?: string;
58
- }
59
- export declare function createTelegramThreadName(input: TelegramThreadNameInput): string;
60
80
  export declare function normalizeTelegramTopicTargetThreadName(threadName: string): string;
61
81
  export declare function getTelegramTopicIdentityName(threadName: string): string;
62
82
  export declare function chooseTelegramThreadName(input: {
@@ -4,7 +4,7 @@
4
4
  * Owns name/title value policy and one expiring exact-target dialog per session scope.
5
5
  * Excludes occupancy/slot allocation, display-mode projection, transport and API/store effects.
6
6
  */
7
- import { areTelegramTargetsEqual as sameTarget } from "./target.js";
7
+ import { areTelegramTargetsEqual as sameTarget, } from "./target.js";
8
8
  export const TELEGRAM_THREAD_NAME_DIALOG_TTL_MS = 5 * 60_000;
9
9
  function targetKey(target) {
10
10
  return `${target.chatId}:${target.threadId ?? "chat"}`;
@@ -16,99 +16,208 @@ export function createTelegramThreadNameDialogRuntime(options) {
16
16
  const ttlMs = options?.ttlMs ?? TELEGRAM_THREAD_NAME_DIALOG_TTL_MS;
17
17
  const nowMs = options?.nowMs ?? Date.now;
18
18
  const candidates = new Map();
19
+ const recipientCurrent = (entry) => {
20
+ try {
21
+ return entry.isCurrent ? entry.isCurrent() === true : true;
22
+ }
23
+ catch {
24
+ return false;
25
+ }
26
+ };
27
+ const view = (entry) => cloneCandidate({
28
+ scope: entry.scope,
29
+ target: entry.target,
30
+ dialogMessageId: entry.dialogMessageId,
31
+ phase: "input",
32
+ expiresAtMs: entry.expiresAtMs,
33
+ });
34
+ const createLifetime = (entry) => {
35
+ const key = targetKey(entry.target);
36
+ const claim = {};
37
+ let ended = false;
38
+ const finish = () => {
39
+ ended = true;
40
+ if (candidates.get(key) === entry &&
41
+ (!entry.consumedBy || entry.consumedBy === claim))
42
+ candidates.delete(key);
43
+ };
44
+ const isCurrent = () => {
45
+ if (ended ||
46
+ candidates.get(key) !== entry ||
47
+ (entry.consumedBy && entry.consumedBy !== claim)) {
48
+ ended = true;
49
+ return false;
50
+ }
51
+ const current = entry.expiresAtMs > nowMs() && recipientCurrent(entry);
52
+ // Supplied authority/clock observations may synchronously replace this dialog.
53
+ if (!current ||
54
+ entry.expiresAtMs <= nowMs() ||
55
+ candidates.get(key) !== entry ||
56
+ (entry.consumedBy && entry.consumedBy !== claim)) {
57
+ finish();
58
+ return false;
59
+ }
60
+ return true;
61
+ };
62
+ return {
63
+ assertAuthority: entry.assertAuthority,
64
+ isCurrent,
65
+ publish(dialogMessageId, assertPublicationCurrent) {
66
+ if (!isCurrent() || entry.phase !== "delivery")
67
+ return undefined;
68
+ // Source authority is needed for this publication only, never retained by future input/effects.
69
+ if (assertPublicationCurrent) {
70
+ assertPublicationCurrent();
71
+ if (entry.expiresAtMs <= nowMs())
72
+ return undefined;
73
+ assertPublicationCurrent();
74
+ if (ended ||
75
+ candidates.get(key) !== entry ||
76
+ entry.phase !== "delivery")
77
+ return undefined;
78
+ }
79
+ if (!Number.isSafeInteger(dialogMessageId) || dialogMessageId <= 0) {
80
+ finish();
81
+ return undefined;
82
+ }
83
+ entry.dialogMessageId = dialogMessageId;
84
+ entry.phase = "input";
85
+ return view(entry);
86
+ },
87
+ select(action) {
88
+ if (!isCurrent() || entry.phase !== "input")
89
+ return { kind: "expired" };
90
+ if (action === "cancel")
91
+ finish();
92
+ else {
93
+ entry.phase = "consumed";
94
+ entry.consumedBy = claim;
95
+ }
96
+ return { kind: action };
97
+ },
98
+ consumeName(text) {
99
+ if (!isCurrent() || entry.phase !== "input")
100
+ return { kind: "none" };
101
+ const name = text.trim();
102
+ if (!name)
103
+ return { kind: "empty" };
104
+ entry.phase = "consumed";
105
+ entry.consumedBy = claim;
106
+ return { kind: "name", name };
107
+ },
108
+ reopen() {
109
+ if (!isCurrent() ||
110
+ entry.phase !== "consumed" ||
111
+ entry.consumedBy !== claim) {
112
+ return undefined;
113
+ }
114
+ // A failed effect may restore input, never renew its deadline or issued handle.
115
+ const replacement = {
116
+ ...entry,
117
+ phase: "input",
118
+ consumedBy: undefined,
119
+ };
120
+ candidates.set(key, replacement);
121
+ ended = true;
122
+ return view(replacement);
123
+ },
124
+ finish,
125
+ };
126
+ };
19
127
  const current = (scope, target) => {
20
- const key = targetKey(target);
21
- const candidate = candidates.get(key);
22
- if (!candidate || candidate.scope !== scope ||
23
- !sameTarget(candidate.target, target))
128
+ const entry = candidates.get(targetKey(target));
129
+ if (!entry ||
130
+ entry.scope !== scope ||
131
+ entry.phase !== "input" ||
132
+ !sameTarget(entry.target, target) ||
133
+ !createLifetime(entry).isCurrent())
24
134
  return undefined;
25
- if (candidate.expiresAtMs <= nowMs()) {
26
- candidates.delete(key);
135
+ return entry;
136
+ };
137
+ const capture = (input) => {
138
+ const { scope, dialogMessageId } = input;
139
+ const target = { ...input.target };
140
+ const entry = current(scope, target);
141
+ if (!entry || entry.dialogMessageId !== dialogMessageId)
27
142
  return undefined;
28
- }
29
- return candidate;
143
+ return createLifetime(entry);
30
144
  };
31
145
  return {
146
+ prepare(input) {
147
+ const scope = input.scope;
148
+ const target = { ...input.target };
149
+ const isCurrent = input.isCurrent, assertAuthority = input.assertAuthority;
150
+ const key = targetKey(target);
151
+ const previous = candidates.get(key);
152
+ const entry = {
153
+ scope,
154
+ target,
155
+ phase: "delivery",
156
+ expiresAtMs: nowMs() + ttlMs,
157
+ isCurrent,
158
+ assertAuthority,
159
+ };
160
+ if (!recipientCurrent(entry) ||
161
+ entry.expiresAtMs <= nowMs() ||
162
+ candidates.get(key) !== previous)
163
+ return undefined;
164
+ candidates.set(key, entry);
165
+ return createLifetime(entry);
166
+ },
167
+ capture,
32
168
  open(input) {
33
- const candidate = {
169
+ const entry = {
34
170
  scope: input.scope,
35
171
  target: { ...input.target },
36
- dialogMessageId: input.dialogMessageId,
37
172
  phase: "input",
173
+ dialogMessageId: input.dialogMessageId,
38
174
  expiresAtMs: nowMs() + ttlMs,
39
175
  };
40
- candidates.set(targetKey(input.target), candidate);
41
- return cloneCandidate(candidate);
176
+ candidates.set(targetKey(entry.target), entry);
177
+ return view(entry);
42
178
  },
43
179
  select(input) {
44
- const candidate = current(input.scope, input.target);
45
- if (!candidate || candidate.dialogMessageId !== input.dialogMessageId) {
180
+ const action = input.action;
181
+ const lifetime = capture(input);
182
+ if (!lifetime)
46
183
  return { kind: "expired" };
47
- }
48
- candidates.delete(targetKey(input.target));
49
- return { kind: input.action };
184
+ const result = lifetime.select(action);
185
+ lifetime.finish();
186
+ return result;
50
187
  },
51
188
  consumeName(input) {
52
- const candidate = current(input.scope, input.target);
53
- if (!candidate || candidate.phase !== "input")
189
+ const text = input.text;
190
+ const entry = current(input.scope, input.target);
191
+ if (!entry)
54
192
  return { kind: "none" };
55
- const name = input.text.trim();
56
- if (!name)
57
- return { kind: "empty" };
58
- candidates.delete(targetKey(input.target));
59
- return { kind: "name", name };
193
+ const lifetime = createLifetime(entry);
194
+ const result = lifetime.consumeName(text);
195
+ if (result.kind === "name")
196
+ lifetime.finish();
197
+ return result;
60
198
  },
61
199
  clearScope(scope) {
62
- for (const [key, candidate] of candidates) {
63
- if (candidate.scope === scope)
200
+ for (const [key, entry] of candidates) {
201
+ if (entry.scope === scope)
64
202
  candidates.delete(key);
65
203
  }
66
204
  },
67
205
  inspect(target) {
68
- const candidate = candidates.get(targetKey(target));
69
- if (!candidate || candidate.expiresAtMs <= nowMs()) {
70
- candidates.delete(targetKey(target));
206
+ const key = targetKey(target);
207
+ const entry = candidates.get(key);
208
+ if (!entry)
209
+ return undefined;
210
+ if (entry.expiresAtMs <= nowMs()) {
211
+ if (candidates.get(key) === entry)
212
+ candidates.delete(key);
71
213
  return undefined;
72
214
  }
73
- return cloneCandidate(candidate);
215
+ if (entry.phase !== "input" || !createLifetime(entry).isCurrent())
216
+ return undefined;
217
+ return view(entry);
74
218
  },
75
219
  };
76
220
  }
77
- function hashString(value) {
78
- let hash = 2166136261;
79
- for (let index = 0; index < value.length; index += 1) {
80
- hash ^= value.charCodeAt(index);
81
- hash = Math.imul(hash, 16777619);
82
- }
83
- return hash >>> 0;
84
- }
85
- function getWorkspaceHint(cwd) {
86
- if (!cwd)
87
- return undefined;
88
- const parts = cwd.split("/").filter(Boolean);
89
- const last = parts.at(-1)?.trim();
90
- if (!last)
91
- return undefined;
92
- return (last
93
- .replace(/[^\p{L}\p{N}._-]+/gu, " ")
94
- .trim()
95
- .slice(0, 32) || undefined);
96
- }
97
- export function createTelegramThreadName(input) {
98
- const workspace = getWorkspaceHint(input.cwd);
99
- const roleMark = input.role === "leader"
100
- ? "Leader"
101
- : input.role === "follower"
102
- ? "Follower"
103
- : undefined;
104
- const slot = input.slot ? `Thread ${input.slot}` : undefined;
105
- const peerSalt = input.peers?.slice().sort().join("|") ?? "";
106
- const fallback = `Instance ${hashString(`${input.seed}|${input.cwd ?? ""}|${input.role ?? ""}|${peerSalt}|${input.slot ?? ""}`)
107
- .toString(36)
108
- .slice(0, 4)}`;
109
- return ([slot, workspace, roleMark].filter(Boolean).join(" ").slice(0, 96) ||
110
- fallback);
111
- }
112
221
  export function normalizeTelegramTopicTargetThreadName(threadName) {
113
222
  return threadName.replace(/\s+/g, " ").trim().slice(0, 96);
114
223
  }
@@ -126,15 +126,6 @@ export type ThreadReconciliationAction = {
126
126
  instanceId?: string;
127
127
  messageId?: number;
128
128
  leaderEpoch?: number | string;
129
- } | {
130
- kind: "close-delete-disconnected-instance-topic";
131
- target: TelegramTarget & {
132
- threadId: number;
133
- };
134
- reason: "manual-disconnect";
135
- instanceId?: string;
136
- messageId?: number;
137
- leaderEpoch?: number | string;
138
129
  } | {
139
130
  kind: "close-delete-graceful-shutdown-topic";
140
131
  target: TelegramTarget & {
@@ -220,6 +211,14 @@ export interface ThreadReconciliationInput {
220
211
  previousState?: ThreadReconciliationMachineState;
221
212
  freshCreationGraceMs?: number;
222
213
  }
214
+ /** A Thread record still claims its slot/target while active, starting or pending. */
215
+ export declare function isCurrentThreadRecord(record: {
216
+ readonly status: string;
217
+ }): boolean;
218
+ /** Pure Bot API vocabulary for a deleted/missing Thread; callers own their HTTP-status policy. */
219
+ export declare function isTelegramTopicDeletedErrorMessage(message: string): boolean;
220
+ /** Pure Bot API vocabulary for a closed Thread; closure is not deletion evidence. */
221
+ export declare function isTelegramTopicClosedErrorMessage(message: string): boolean;
223
222
  export interface ThreadReconciliationRuntime {
224
223
  getState: () => ThreadReconciliationMachineState | undefined;
225
224
  recordPlan: (plan: ThreadReconciliationPlan) => void;
@@ -229,13 +228,40 @@ export declare function createThreadReconciliationRuntime(deps: {
229
228
  recordRuntimeEvent: (category: string, message: unknown, details?: Record<string, unknown>) => void;
230
229
  scheduleSnapshotPersist: () => void;
231
230
  }): ThreadReconciliationRuntime;
232
- export declare function planDisconnectedInstanceThreadCleanup(input: {
233
- target: TelegramTarget & {
234
- threadId: number;
235
- };
236
- instanceId?: string;
237
- leaderEpoch?: number | string;
238
- }): ThreadReconciliationPlan;
231
+ /** Momentary preparation evidence only: not an action, lease, issued effect or reusable deletion grant. */
232
+ export interface TelegramLiveRebindCleanupPreparation {
233
+ status: "prepared";
234
+ operationId: string;
235
+ target: ThreadTarget;
236
+ recipientTarget: ThreadTarget;
237
+ leaderEpoch: string;
238
+ }
239
+ export declare function prepareLiveRebindThreadCleanup(input: {
240
+ operationId: string;
241
+ oldTarget: ThreadTarget;
242
+ recipientTarget: ThreadTarget;
243
+ leaderEpoch: string;
244
+ targetProtected: boolean;
245
+ /** Narrow body-free current-work projection; no cached idle or historical disposition proof. */
246
+ work: Readonly<{
247
+ sessionBusy: boolean;
248
+ targetWork: boolean;
249
+ deliveryPending: boolean;
250
+ unknown: boolean;
251
+ }> | undefined;
252
+ }): TelegramLiveRebindCleanupPreparation | undefined;
253
+ export type TelegramLiveRebindCleanupOutcome = "confirmed" | "failed" | "unknown";
254
+ /**
255
+ * One unretried deletion for an already issued live-rebind cleanup grant. Success or confirmed absence is `confirmed`;
256
+ * protection or authority loss before the request and a definite request rejection are `failed`; every other outcome is
257
+ * `unknown`. The caller owns the durable issue marker and terminal record; this never repeats or rolls back.
258
+ */
259
+ export declare function issueLiveRebindThreadCleanup(prepared: TelegramLiveRebindCleanupPreparation, ports: {
260
+ /** Throws when the issuer's current recipient/work/ownership authority no longer holds. */
261
+ assertCurrent(): void;
262
+ deleteTopic(target: ThreadTarget): Promise<unknown>;
263
+ classifyFailure(error: unknown): "absent" | "rejected" | "unknown";
264
+ }): Promise<TelegramLiveRebindCleanupOutcome>;
239
265
  export declare function applyThreadReconciliationPlan(plan: ThreadReconciliationPlan, ports: ThreadReconciliationApplyPorts): Promise<ThreadReconciliationApplyResult>;
240
266
  export declare function planThreadReconciliation(input: ThreadReconciliationInput): ThreadReconciliationPlan;
241
267
  export {};