@llblab/pi-telegram 0.52.1 → 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 +10 -8
  2. package/BACKLOG.md +1 -16
  3. package/CHANGELOG.md +17 -0
  4. package/README.md +8 -10
  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 +640 -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 +34 -3
  44. package/dist/lib/locks.js +318 -119
  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 +23 -37
  76. package/dist/lib/polling.js +71 -52
  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 +6 -3
  95. package/dist/lib/sync.js +112 -45
  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 +165 -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 +36 -30
  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 +1207 -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 +703 -212
  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 +165 -149
  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 +488 -359
  185. package/lib/target.ts +22 -3
  186. package/lib/telegram-api.ts +322 -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
package/dist/lib/sync.js CHANGED
@@ -3,13 +3,13 @@
3
3
  * Zones: Telegram bot reality mirror, demand-driven reconciliation, status diagnostics
4
4
  * Owns pure contracts for deciding when local Telegram mirror state should be refreshed without querying Telegram on every action
5
5
  */
6
- import { getTelegramApiErrorRequestTarget, isTelegramStaleTargetHttpError } from "./telegram-api.js";
7
6
  import { getTelegramTargetKey } from "./target.js";
7
+ import { getTelegramApiErrorRequestTarget, isTelegramStaleTargetHttpError, } from "./telegram-api.js";
8
8
  import * as ThreadReconciler from "./thread-reconciler.js";
9
- import { TelegramWorkspaceSlotUnavailableError } from "./workspace-slots.js";
10
- import { normalizeTelegramWorkspacePath } from "./workspace-identity.js";
9
+ import { commitTelegramWorkspaceProvisionBinding, createTelegramCleanupTargetProtection, getTelegramTargetFromApiBody, getTelegramThreadOwnerKey, isSameTelegramProcessInstance, isTelegramTopicTargetStaleError, provisionOwnBusTopic, } from "./threads.js";
11
10
  import { createTelegramWorkspaceAdmissionOperationId, runWithTelegramWorkspaceAdmissionsAsync, } from "./workspace-admission.js";
12
- import { createTelegramCleanupTargetProtection, commitTelegramWorkspaceProvisionBinding, getTelegramTargetFromApiBody, getTelegramThreadOwnerKey, isSameTelegramProcessInstance, isTelegramTopicTargetStaleError, provisionOwnBusTopic, } from "./threads.js";
11
+ import { normalizeTelegramWorkspacePath } from "./workspace-identity.js";
12
+ import { TelegramWorkspaceSlotUnavailableError } from "./workspace-slots.js";
13
13
  function markTelegramConfigSyncChange(state, action, options) {
14
14
  const nowMs = options?.nowMs ?? Date.now();
15
15
  let nextState = markTelegramSyncSliceFresh(state, "pairing", {
@@ -32,10 +32,13 @@ export function createTelegramPreservedLeaderQuitHandler(deps) {
32
32
  const profile = deps.getProfileName();
33
33
  if (epoch === undefined || !isSessionCurrent())
34
34
  return undefined;
35
- const isCurrent = () => isSessionCurrent() && deps.getCurrentLeaderEpoch() === epoch &&
36
- deps.getProfileName() === profile && deps.isPollingSuspended();
35
+ const isCurrent = () => isSessionCurrent() &&
36
+ deps.getCurrentLeaderEpoch() === epoch &&
37
+ deps.getProfileName() === profile &&
38
+ deps.isPollingSuspended();
37
39
  return async () => {
38
- if (!isCurrent() || await deps.resolveAutomaticThreadCleanupEnabled() !== false)
40
+ if (!isCurrent() ||
41
+ (await deps.resolveAutomaticThreadCleanupEnabled()) !== false)
39
42
  return;
40
43
  if (!isCurrent())
41
44
  return;
@@ -49,13 +52,20 @@ export function createTelegramPreservedLeaderQuitHandler(deps) {
49
52
  await deps.topicTargetStore.load();
50
53
  if (!isCurrent())
51
54
  return;
52
- const records = deps.topicTargetStore.list().filter((record) => record.instanceId === deps.instanceId && (record.status === "active" || record.status === "starting"));
55
+ const records = deps.topicTargetStore
56
+ .list()
57
+ .filter((record) => record.instanceId === deps.instanceId &&
58
+ (record.status === "active" || record.status === "starting"));
53
59
  if (records.length !== 1)
54
60
  return;
55
61
  const record = records[0];
56
- if (deps.topicTargetStore.listPendingCleanups().some((intent) => intent.target.chatId === record.target.chatId && intent.target.threadId === record.target.threadId))
62
+ if (deps.topicTargetStore
63
+ .listPendingCleanups()
64
+ .some((intent) => intent.target.chatId === record.target.chatId &&
65
+ intent.target.threadId === record.target.threadId))
57
66
  return;
58
- if (!await deps.topicTargetStore.detachTargetOwner(record, isCurrent) && isCurrent()) {
67
+ if (!(await deps.topicTargetStore.detachTargetOwner(record, isCurrent)) &&
68
+ isCurrent()) {
59
69
  throw new Error("Telegram preserved leader detachment was not committed.");
60
70
  }
61
71
  });
@@ -74,11 +84,47 @@ function createTelegramSessionRestartThreadCleanupHandler(deps) {
74
84
  }
75
85
  export function createTelegramThreadDisconnectAssembly(deps) {
76
86
  return {
77
- disconnect: createTelegramManualThreadDisconnectHandler({
78
- ...deps,
79
- workspaceOperationKind: "workspace.disconnect-thread",
80
- stopPolling: deps.stopPolling,
81
- }),
87
+ async disconnect() {
88
+ const captured = deps.captureStopPolling?.();
89
+ const stop = captured?.stop ?? deps.stopPolling;
90
+ let stopAttempted = false;
91
+ const disconnect = createTelegramManualThreadDisconnectHandler({
92
+ ...deps,
93
+ workspaceOperationKind: "workspace.disconnect-thread",
94
+ isDisconnectCurrent: captured?.isCurrent ?? deps.isDisconnectCurrent,
95
+ stopPolling() {
96
+ stopAttempted = true;
97
+ return stop();
98
+ },
99
+ });
100
+ try {
101
+ return await disconnect();
102
+ }
103
+ catch (error) {
104
+ // Never retry a stop whose teardown/release outcome may already have committed.
105
+ if (stopAttempted) {
106
+ deps.recordRuntimeEvent("connection", error, {
107
+ phase: "disconnect-stop",
108
+ });
109
+ throw error;
110
+ }
111
+ deps.recordRuntimeEvent("connection", error, {
112
+ phase: "disconnect-cleanup",
113
+ });
114
+ let stopped;
115
+ try {
116
+ stopped = await stop();
117
+ }
118
+ catch (stopError) {
119
+ deps.recordRuntimeEvent("connection", stopError, {
120
+ phase: "disconnect-stop",
121
+ });
122
+ throw stopError;
123
+ }
124
+ const separator = /[.!?]$/u.test(stopped) ? " " : ". ";
125
+ return `${stopped}${separator}Thread cleanup was not confirmed. Check /telegram-status --debug.`;
126
+ }
127
+ },
82
128
  cleanupForSessionRestart: createTelegramSessionRestartThreadCleanupHandler({
83
129
  ...deps,
84
130
  suspendPolling: deps.suspendPolling,
@@ -86,7 +132,12 @@ export function createTelegramThreadDisconnectAssembly(deps) {
86
132
  };
87
133
  }
88
134
  export function createTelegramManualThreadDisconnectHandler(deps) {
135
+ const assertCurrent = () => {
136
+ if (deps.isDisconnectCurrent?.() === false)
137
+ throw new Error("Telegram disconnect was superseded by a new connection.");
138
+ };
89
139
  const operation = async () => {
140
+ assertCurrent();
90
141
  const currentRecord = deps.getCurrentThreadRecord();
91
142
  let cleanupPending = false;
92
143
  if (currentRecord?.target.threadId) {
@@ -98,6 +149,7 @@ export function createTelegramManualThreadDisconnectHandler(deps) {
98
149
  if (isManualFollower && !ownsLeader) {
99
150
  if (deps.disconnectFollowerThread) {
100
151
  const disconnected = await deps.disconnectFollowerThread();
152
+ assertCurrent();
101
153
  if (!disconnected) {
102
154
  throw new Error("Telegram follower thread deletion requires a live leader registration.");
103
155
  }
@@ -117,11 +169,15 @@ export function createTelegramManualThreadDisconnectHandler(deps) {
117
169
  target,
118
170
  requestedAtMs: (deps.getNowMs ?? Date.now)(),
119
171
  };
120
- const departingRecord = deps.topicTargetStore.list().find((record) => record.instanceId === currentRecord.instanceId &&
121
- record.target.chatId === target.chatId && record.target.threadId === target.threadId);
172
+ const departingRecord = deps.topicTargetStore
173
+ .list()
174
+ .find((record) => record.instanceId === currentRecord.instanceId &&
175
+ record.target.chatId === target.chatId &&
176
+ record.target.threadId === target.threadId);
122
177
  const isCleanupTargetProtected = createTelegramCleanupTargetProtection(deps.topicTargetStore, departingRecord);
123
178
  deps.topicTargetStore.upsertPendingCleanup(intent);
124
179
  await deps.topicTargetStore.persist();
180
+ assertCurrent();
125
181
  const cleanupPlan = ThreadReconciler.planThreadReconciliation({
126
182
  nowMs: (deps.getNowMs ?? Date.now)(),
127
183
  currentLeaderEpoch: leaderEpoch,
@@ -131,20 +187,25 @@ export function createTelegramManualThreadDisconnectHandler(deps) {
131
187
  const cleanup = await ThreadReconciler.applyThreadReconciliationPlan(cleanupPlan, {
132
188
  isCleanupTargetProtected,
133
189
  callApi(method, body) {
190
+ assertCurrent();
134
191
  return deps.callApi(method, body);
135
192
  },
136
193
  markStaleByTarget(targetToMark, syncStatus, lastSyncError) {
194
+ assertCurrent();
137
195
  return deps.topicTargetStore.markStaleByTarget(targetToMark, syncStatus, lastSyncError);
138
196
  },
139
197
  removeCleanupIntentById(id) {
198
+ assertCurrent();
140
199
  return deps.topicTargetStore.removePendingCleanup(id);
141
200
  },
142
201
  persist() {
202
+ assertCurrent();
143
203
  return deps.topicTargetStore.persist();
144
204
  },
145
205
  getCurrentLeaderEpoch: deps.getCurrentLeaderEpoch,
146
206
  recordRuntimeEvent: deps.recordRuntimeEvent,
147
207
  });
208
+ assertCurrent();
148
209
  if (cleanupPlan.actions.some((action) => isCleanupTargetProtected(action.target, action))) {
149
210
  return "Thread disconnect superseded by a new binding.";
150
211
  }
@@ -160,6 +221,7 @@ export function createTelegramManualThreadDisconnectHandler(deps) {
160
221
  action: "manual-disconnect",
161
222
  }));
162
223
  }
224
+ assertCurrent();
163
225
  const stopped = await deps.stopPolling();
164
226
  return cleanupPending
165
227
  ? `${stopped} Telegram thread cleanup remains pending for the next leader.`
@@ -269,23 +331,32 @@ export function captureTelegramStaleTargetRequestRecovery(body, deps) {
269
331
  if (!target || epoch === undefined)
270
332
  return undefined;
271
333
  const key = getTelegramTargetKey(target);
272
- const record = deps.topicTargetStore.list().find((candidate) => getTelegramTargetKey(candidate.target) === key);
334
+ const record = deps.topicTargetStore
335
+ .list()
336
+ .find((candidate) => getTelegramTargetKey(candidate.target) === key);
273
337
  if (!record)
274
338
  return undefined;
275
339
  const generation = deps.getSessionGeneration();
276
340
  const profile = deps.getProfileName();
277
341
  const isAuthorityCurrent = () => deps.getCurrentLeaderEpoch() === epoch &&
278
- deps.getSessionGeneration() === generation && deps.getProfileName() === profile;
342
+ deps.getSessionGeneration() === generation &&
343
+ deps.getProfileName() === profile;
279
344
  const isCurrent = () => {
280
- const current = deps.topicTargetStore.list().find((candidate) => getTelegramTargetKey(candidate.target) === key);
281
- return isAuthorityCurrent() &&
282
- current?.instanceId === record.instanceId && current?.profileKey === record.profileKey &&
283
- current?.updatedAtMs === record.updatedAtMs && current?.createdAtMs === record.createdAtMs;
345
+ const current = deps.topicTargetStore
346
+ .list()
347
+ .find((candidate) => getTelegramTargetKey(candidate.target) === key);
348
+ return (isAuthorityCurrent() &&
349
+ current?.instanceId === record.instanceId &&
350
+ current?.profileKey === record.profileKey &&
351
+ current?.updatedAtMs === record.updatedAtMs &&
352
+ current?.createdAtMs === record.createdAtMs);
284
353
  };
285
354
  return async (error) => {
286
355
  const requestTarget = getTelegramApiErrorRequestTarget(error);
287
- if (!isTelegramStaleTargetHttpError(error) || !requestTarget ||
288
- getTelegramTargetKey(requestTarget) !== key || !isCurrent())
356
+ if (!isTelegramStaleTargetHttpError(error) ||
357
+ !requestTarget ||
358
+ getTelegramTargetKey(requestTarget) !== key ||
359
+ !isCurrent())
289
360
  return;
290
361
  if (await recoverStaleTelegramTopicApiError({ chat_id: target.chatId, message_thread_id: target.threadId }, error, { ...deps, isCurrent, isAuthorityCurrent })) {
291
362
  deps.onRecovered();
@@ -311,7 +382,7 @@ export async function recoverStaleTelegramTopicApiError(apiBody, error, deps) {
311
382
  const recover = async () => {
312
383
  if (deps.isCurrent) {
313
384
  if (!deps.topicTargetStore.invalidateTarget ||
314
- !await deps.topicTargetStore.invalidateTarget(target, deps.isCurrent, String(error)))
385
+ !(await deps.topicTargetStore.invalidateTarget(target, deps.isCurrent, String(error))))
315
386
  return false;
316
387
  if (deps.isAuthorityCurrent?.() === false)
317
388
  return false;
@@ -393,7 +464,12 @@ export async function ensureTelegramLeaderThreadBinding(deps) {
393
464
  const legacyLeaderRecord = deps.topicTargetStore.getByProfileKey(leaderProfileKey);
394
465
  let capacityUnavailable = false;
395
466
  const workspaceIdentity = normalizedLeaderCwd
396
- ? deps.topicTargetStore.claimWorkspaceIdentity(normalizedLeaderCwd, deps.instanceId, legacyLeaderRecord?.instanceId, { sessionId: deps.sessionId, onCapacityUnavailable() { capacityUnavailable = true; } })
467
+ ? deps.topicTargetStore.claimWorkspaceIdentity(normalizedLeaderCwd, deps.instanceId, legacyLeaderRecord?.instanceId, {
468
+ sessionId: deps.sessionId,
469
+ onCapacityUnavailable() {
470
+ capacityUnavailable = true;
471
+ },
472
+ })
397
473
  : undefined;
398
474
  if (deps.cwd && !workspaceIdentity) {
399
475
  if (capacityUnavailable)
@@ -428,7 +504,9 @@ export async function ensureTelegramLeaderThreadBinding(deps) {
428
504
  target: { ...committed.target },
429
505
  slot: committed.slot ?? result.slot,
430
506
  ...(committed.threadName ? { threadName: committed.threadName } : {}),
431
- ...(committed.displayTitle ? { displayTitle: committed.displayTitle } : {}),
507
+ ...(committed.displayTitle
508
+ ? { displayTitle: committed.displayTitle }
509
+ : {}),
432
510
  };
433
511
  };
434
512
  try {
@@ -469,7 +547,9 @@ export async function ensureTelegramLeaderThreadBinding(deps) {
469
547
  ...(legacyLeaderRecord.threadName
470
548
  ? { threadName: legacyLeaderRecord.threadName }
471
549
  : {}),
472
- ...(legacyLeaderRecord.slot ? { slot: legacyLeaderRecord.slot } : {}),
550
+ ...(legacyLeaderRecord.slot
551
+ ? { slot: legacyLeaderRecord.slot }
552
+ : {}),
473
553
  updatedAtMs: legacyLeaderRecord.updatedAtMs,
474
554
  }
475
555
  : undefined;
@@ -557,7 +637,9 @@ export async function ensureTelegramLeaderThreadBinding(deps) {
557
637
  });
558
638
  // Legacy callers without session identity may reuse the active instance target.
559
639
  // Session-aware callers must resolve their exact Workspace binding instead.
560
- if (deps.sessionId === undefined && !deps.forceFreshUnnamed && priorTargets.length > 0) {
640
+ if (deps.sessionId === undefined &&
641
+ !deps.forceFreshUnnamed &&
642
+ priorTargets.length > 0) {
561
643
  const record = priorTargets[0];
562
644
  deps.recordEvent("telegram", "Leader thread preserved after session lifecycle change", {
563
645
  phase: "leader-thread-reused",
@@ -704,21 +786,6 @@ export function createTelegramProvisioningActivityRuntime() {
704
786
  },
705
787
  };
706
788
  }
707
- const RECONCILE_TRIGGERS = new Set([
708
- "startup",
709
- "reload",
710
- "topic-lifecycle",
711
- "stale-api-error",
712
- "setup-change",
713
- "pairing-change",
714
- "follower-register",
715
- "follower-prune",
716
- "status-request",
717
- "leader-health-tick",
718
- ]);
719
- export function shouldReconcileTelegramSync(trigger) {
720
- return RECONCILE_TRIGGERS.has(trigger);
721
- }
722
789
  export function markTelegramSyncSliceSuspect(state, slice, input) {
723
790
  return {
724
791
  ...state,
@@ -18,3 +18,7 @@ export declare function areTelegramTargetsEqual(left: TelegramTarget, right: Tel
18
18
  export declare function getTelegramTargetThreadParams(target: TelegramTarget): {
19
19
  message_thread_id?: number;
20
20
  };
21
+ /** Pure wire parser for `{ chatId, threadId? }`; unknown fields are dropped and malformed shapes refuse. */
22
+ export declare function parseTelegramTarget(value: unknown): TelegramTarget | undefined;
23
+ /** Pure Bot API integer id parser accepting integer numbers or non-blank integer strings. */
24
+ export declare function parseTelegramIntegerId(value: unknown): number | undefined;
@@ -25,3 +25,21 @@ export function getTelegramTargetThreadParams(target) {
25
25
  ? { message_thread_id: target.threadId }
26
26
  : {};
27
27
  }
28
+ /** Pure wire parser for `{ chatId, threadId? }`; unknown fields are dropped and malformed shapes refuse. */
29
+ export function parseTelegramTarget(value) {
30
+ if (typeof value !== "object" || value === null || Array.isArray(value))
31
+ return undefined;
32
+ const { chatId, threadId } = value;
33
+ if (typeof chatId !== "number")
34
+ return undefined;
35
+ return typeof threadId === "number" ? { chatId, threadId } : { chatId };
36
+ }
37
+ /** Pure Bot API integer id parser accepting integer numbers or non-blank integer strings. */
38
+ export function parseTelegramIntegerId(value) {
39
+ if (typeof value === "number" && Number.isInteger(value))
40
+ return value;
41
+ if (typeof value !== "string" || value.trim() === "")
42
+ return undefined;
43
+ const parsed = Number(value);
44
+ return Number.isInteger(parsed) ? parsed : undefined;
45
+ }
@@ -7,7 +7,8 @@
7
7
  */
8
8
  import type { TelegramAttachmentSource } from "./media.ts";
9
9
  export declare const TELEGRAM_FILE_MAX_BYTES: number;
10
- export declare function getTelegramInboundFileByteLimitFromEnv(env: NodeJS.ProcessEnv, names: string[], defaultValue?: number): number;
10
+ /** First positive safe-integer byte limit among ordered env names, else the caller's default. */
11
+ export declare function getTelegramByteLimitFromEnv(env: NodeJS.ProcessEnv, names: readonly string[], defaultValue: number): number;
11
12
  export type TelegramNetworkFamilyPolicy = "auto" | "ipv4" | "ipv6" | "ipv4-fallback";
12
13
  type TelegramNetworkFamily = 4 | 6;
13
14
  export interface TelegramUser {
@@ -288,6 +289,8 @@ export interface TelegramApiRetryWait {
288
289
  retryAfterSeconds?: number;
289
290
  }
290
291
  export interface TelegramApiCallOptions {
292
+ /** Local per-call effect authority; independent of source completion and transport ownership. */
293
+ assertAuthority?: () => void;
291
294
  signal?: AbortSignal;
292
295
  maxAttempts?: number;
293
296
  retryRateLimit?: boolean;
@@ -297,6 +300,12 @@ export interface TelegramApiCallOptions {
297
300
  /** Observability hook fired before a 429 retry wait, never for 5xx waits. */
298
301
  onRetryWait?: (wait: TelegramApiRetryWait) => void;
299
302
  }
303
+ export declare class TelegramApiAuthorityError extends Error {
304
+ readonly requestIssued: boolean;
305
+ constructor(requestIssued: boolean);
306
+ }
307
+ /** Recheck caller authority, classifying refusal by whether a Bot API request was already issued. */
308
+ export declare function assertTelegramApiCallAuthority(assertAuthority: (() => void) | undefined, requestIssued: boolean): void;
300
309
  export interface TelegramFileDownloadOptions {
301
310
  signal?: AbortSignal;
302
311
  maxFileSizeBytes?: number;
@@ -338,14 +347,14 @@ export interface TelegramEditGuestInlineMessageContent {
338
347
  richMessage?: TelegramInputRichMessage;
339
348
  parseMode?: "HTML";
340
349
  }
341
- export interface TelegramAnswerCallbackQueryOptions {
350
+ export interface TelegramAnswerCallbackQueryOptions extends Pick<TelegramApiCallOptions, "assertAuthority"> {
342
351
  recordRuntimeEvent?: (kind: "api", error: unknown, details?: Record<string, unknown>) => void;
343
352
  }
344
353
  export interface TelegramApiClient {
345
354
  call: <TResponse>(method: string, body: Record<string, unknown>, options?: TelegramApiCallOptions) => Promise<TResponse>;
346
355
  callMultipart: <TResponse>(method: string, fields: Record<string, string>, fileField: string, filePath: string, fileName: string, options?: TelegramApiCallOptions) => Promise<TResponse>;
347
356
  downloadFile: (fileId: string, suggestedName: string, tempDir: string, options?: TelegramFileDownloadOptions) => Promise<string>;
348
- answerCallbackQuery: (callbackQueryId: string, text?: string) => Promise<void>;
357
+ answerCallbackQuery: (callbackQueryId: string, text?: string, options?: Pick<TelegramApiCallOptions, "assertAuthority">) => Promise<void>;
349
358
  answerGuestQuery?: (guestQueryId: string, text?: string, options?: TelegramAnswerGuestQueryOptions) => Promise<void>;
350
359
  }
351
360
  export interface TelegramApiTargetActivityRuntime {
@@ -422,16 +431,14 @@ export interface TelegramBridgeApiRuntimeDeps {
422
431
  chatActionMinIntervalMs?: number;
423
432
  chatActionMaxGates?: number;
424
433
  }
425
- export interface TelegramBridgeApiRuntime {
426
- call: <TResponse>(method: string, body: Record<string, unknown>, options?: TelegramApiCallOptions) => Promise<TResponse>;
427
- callMultipart: <TResponse>(method: string, fields: Record<string, string>, fileField: string, filePath: string, fileName: string, options?: TelegramApiCallOptions) => Promise<TResponse>;
434
+ export interface TelegramBridgeApiRuntime extends Pick<TelegramApiClient, "call" | "callMultipart"> {
428
435
  downloadFile: (fileId: string, suggestedName: string, source?: TelegramAttachmentSource) => Promise<string>;
429
436
  deleteWebhook: (signal?: AbortSignal) => Promise<boolean>;
430
437
  getUpdates: (body: Record<string, unknown>, signal?: AbortSignal) => Promise<TelegramUpdate[]>;
431
438
  setMyCommands: (commands: readonly {
432
439
  command: string;
433
440
  description: string;
434
- }[]) => Promise<boolean>;
441
+ }[], options?: Pick<TelegramApiCallOptions, "assertAuthority">) => Promise<boolean>;
435
442
  sendChatAction: (chatId: number, action: string, options?: {
436
443
  message_thread_id?: number;
437
444
  }) => Promise<boolean>;
@@ -446,12 +453,12 @@ export interface TelegramBridgeApiRuntime {
446
453
  entities?: unknown[];
447
454
  message_thread_id?: number;
448
455
  }) => Promise<boolean>;
449
- sendMessage: (body: TelegramSendMessageBody) => Promise<TelegramSentMessage>;
450
- sendRichMessage: (body: TelegramSendRichMessageBody) => Promise<TelegramSentMessage>;
456
+ sendMessage: (body: TelegramSendMessageBody, options?: Pick<TelegramApiCallOptions, "assertAuthority">) => Promise<TelegramSentMessage>;
457
+ sendRichMessage: (body: TelegramSendRichMessageBody, options?: Pick<TelegramApiCallOptions, "assertAuthority">) => Promise<TelegramSentMessage>;
451
458
  sendRichMessageDraft: (body: TelegramSendRichMessageDraftBody) => Promise<boolean>;
452
- editMessageText: (body: TelegramEditMessageTextBody) => Promise<"edited" | "unchanged">;
459
+ editMessageText: (body: TelegramEditMessageTextBody, options?: Pick<TelegramApiCallOptions, "assertAuthority">) => Promise<"edited" | "unchanged">;
453
460
  editMessageReplyMarkup: (chatId: number, messageId: number, replyMarkup: unknown) => Promise<void>;
454
- answerCallbackQuery: (callbackQueryId: string, text?: string) => Promise<void>;
461
+ answerCallbackQuery: (callbackQueryId: string, text?: string, options?: Pick<TelegramApiCallOptions, "assertAuthority">) => Promise<void>;
455
462
  answerGuestQuery: (guestQueryId: string, text?: string, options?: TelegramAnswerGuestQueryOptions) => Promise<void>;
456
463
  /**
457
464
  * Temporary Guest Mode ACK experiment: answers the guest query and returns
@@ -514,8 +521,6 @@ export declare function fetchTelegramBotIdentity(botToken: string, fetchImpl?: t
514
521
  */
515
522
  export declare function callTelegramMultipart<TResponse>(botToken: string | undefined, method: string, fields: Record<string, string>, fileField: string, filePath: string, fileName: string, options?: TelegramApiCallOptions): Promise<TResponse>;
516
523
  export declare function downloadTelegramFile(botToken: string | undefined, fileId: string, suggestedName: string, tempDir: string, options?: TelegramFileDownloadOptions): Promise<string>;
517
- /** Tooltip-like callback answers omit a terminal sentence period; ellipses and other punctuation stay literal. */
518
- export declare function formatTelegramCallbackAnswerText(text: string | undefined): string | undefined;
519
524
  export declare function answerTelegramCallbackQuery(botToken: string | undefined, callbackQueryId: string, text?: string, options?: TelegramAnswerCallbackQueryOptions): Promise<void>;
520
525
  export declare function createTelegramChatActionSender<TAction extends string>(sendChatAction: (chatId: number, action: TAction, options?: {
521
526
  message_thread_id?: number;