@llblab/pi-telegram 0.45.11 → 0.47.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/BACKLOG.md +5 -1
  2. package/CHANGELOG.md +12 -0
  3. package/README.md +6 -6
  4. package/dist/api/activity.d.ts +6 -0
  5. package/dist/api/activity.js +6 -0
  6. package/dist/api/commands.d.ts +6 -0
  7. package/dist/api/commands.js +6 -0
  8. package/dist/api/delivery.d.ts +6 -0
  9. package/dist/api/delivery.js +6 -0
  10. package/dist/api/inbound.d.ts +6 -0
  11. package/dist/api/inbound.js +6 -0
  12. package/dist/api/keyboard.d.ts +6 -0
  13. package/dist/api/keyboard.js +6 -0
  14. package/dist/api/outbound.d.ts +6 -0
  15. package/dist/api/outbound.js +6 -0
  16. package/dist/api/sections.d.ts +7 -0
  17. package/dist/api/sections.js +6 -0
  18. package/dist/api/status.d.ts +6 -0
  19. package/dist/api/status.js +6 -0
  20. package/dist/api/updates.d.ts +6 -0
  21. package/dist/api/updates.js +6 -0
  22. package/dist/api/voice.d.ts +6 -0
  23. package/dist/api/voice.js +6 -0
  24. package/dist/index.d.ts +6 -0
  25. package/dist/index.js +6 -0
  26. package/dist/lib/activity-verbosity.d.ts +52 -0
  27. package/dist/lib/activity-verbosity.js +596 -0
  28. package/dist/lib/activity.d.ts +220 -0
  29. package/dist/lib/activity.js +574 -0
  30. package/dist/lib/agent-messages.d.ts +28 -0
  31. package/dist/lib/agent-messages.js +86 -0
  32. package/dist/lib/bindings.d.ts +250 -0
  33. package/dist/lib/bindings.js +936 -0
  34. package/dist/lib/bus-api.d.ts +18 -0
  35. package/dist/lib/bus-api.js +254 -0
  36. package/dist/lib/bus-follower.d.ts +464 -0
  37. package/dist/lib/bus-follower.js +1686 -0
  38. package/dist/lib/bus-leader.d.ts +292 -0
  39. package/dist/lib/bus-leader.js +2242 -0
  40. package/dist/lib/bus-transport.d.ts +64 -0
  41. package/dist/lib/bus-transport.js +140 -0
  42. package/dist/lib/bus.d.ts +518 -0
  43. package/dist/lib/bus.js +2055 -0
  44. package/dist/lib/channel-posts.d.ts +170 -0
  45. package/dist/lib/channel-posts.js +611 -0
  46. package/dist/lib/command-templates.d.ts +70 -0
  47. package/dist/lib/command-templates.js +744 -0
  48. package/dist/lib/commands.d.ts +541 -0
  49. package/dist/lib/commands.js +1155 -0
  50. package/dist/lib/config.d.ts +252 -0
  51. package/dist/lib/config.js +889 -0
  52. package/dist/lib/delivery.d.ts +163 -0
  53. package/dist/lib/delivery.js +542 -0
  54. package/dist/lib/extension.d.ts +7 -0
  55. package/dist/lib/extension.js +1608 -0
  56. package/dist/lib/generative-app-worker.mjs +104 -0
  57. package/dist/lib/generative-apps.d.ts +212 -0
  58. package/dist/lib/generative-apps.js +1006 -0
  59. package/dist/lib/inbound.d.ts +91 -0
  60. package/dist/lib/inbound.js +502 -0
  61. package/dist/lib/journal.d.ts +665 -0
  62. package/dist/lib/journal.js +3701 -0
  63. package/dist/lib/keyboard.d.ts +23 -0
  64. package/dist/lib/keyboard.js +37 -0
  65. package/dist/lib/lifecycle.d.ts +157 -0
  66. package/dist/lib/lifecycle.js +395 -0
  67. package/dist/lib/locks.d.ts +164 -0
  68. package/dist/lib/locks.js +1208 -0
  69. package/dist/lib/logging.d.ts +50 -0
  70. package/dist/lib/logging.js +274 -0
  71. package/dist/lib/media.d.ts +181 -0
  72. package/dist/lib/media.js +602 -0
  73. package/dist/lib/menu-model.d.ts +217 -0
  74. package/dist/lib/menu-model.js +663 -0
  75. package/dist/lib/menu-queue.d.ts +37 -0
  76. package/dist/lib/menu-queue.js +408 -0
  77. package/dist/lib/menu-settings.d.ts +116 -0
  78. package/dist/lib/menu-settings.js +595 -0
  79. package/dist/lib/menu-status.d.ts +32 -0
  80. package/dist/lib/menu-status.js +112 -0
  81. package/dist/lib/menu-thinking.d.ts +29 -0
  82. package/dist/lib/menu-thinking.js +82 -0
  83. package/dist/lib/menu.d.ts +171 -0
  84. package/dist/lib/menu.js +323 -0
  85. package/dist/lib/model.d.ts +127 -0
  86. package/dist/lib/model.js +409 -0
  87. package/dist/lib/outbound-attachments.d.ts +241 -0
  88. package/dist/lib/outbound-attachments.js +639 -0
  89. package/dist/lib/outbound-buttons.d.ts +69 -0
  90. package/dist/lib/outbound-buttons.js +248 -0
  91. package/dist/lib/outbound-markup.d.ts +42 -0
  92. package/dist/lib/outbound-markup.js +678 -0
  93. package/dist/lib/outbound-voice.d.ts +55 -0
  94. package/dist/lib/outbound-voice.js +152 -0
  95. package/dist/lib/outbound.d.ts +185 -0
  96. package/dist/lib/outbound.js +516 -0
  97. package/dist/lib/ownership.d.ts +77 -0
  98. package/dist/lib/ownership.js +174 -0
  99. package/dist/lib/paths.d.ts +40 -0
  100. package/dist/lib/paths.js +94 -0
  101. package/dist/lib/pi.d.ts +72 -0
  102. package/dist/lib/pi.js +121 -0
  103. package/dist/lib/polling.d.ts +351 -0
  104. package/dist/lib/polling.js +1196 -0
  105. package/dist/lib/preview.d.ts +192 -0
  106. package/dist/lib/preview.js +614 -0
  107. package/dist/lib/prompt-templates.d.ts +24 -0
  108. package/dist/lib/prompt-templates.js +118 -0
  109. package/dist/lib/prompts.d.ts +57 -0
  110. package/dist/lib/prompts.js +167 -0
  111. package/dist/lib/queue.d.ts +766 -0
  112. package/dist/lib/queue.js +1965 -0
  113. package/dist/lib/recovery.d.ts +86 -0
  114. package/dist/lib/recovery.js +285 -0
  115. package/dist/lib/rendering.d.ts +20 -0
  116. package/dist/lib/rendering.js +983 -0
  117. package/dist/lib/replies.d.ts +214 -0
  118. package/dist/lib/replies.js +737 -0
  119. package/dist/lib/routing.d.ts +207 -0
  120. package/dist/lib/routing.js +2282 -0
  121. package/dist/lib/runtime.d.ts +172 -0
  122. package/dist/lib/runtime.js +402 -0
  123. package/dist/lib/sections.d.ts +165 -0
  124. package/dist/lib/sections.js +327 -0
  125. package/dist/lib/setup.d.ts +82 -0
  126. package/dist/lib/setup.js +150 -0
  127. package/dist/lib/skills.d.ts +8 -0
  128. package/dist/lib/skills.js +12 -0
  129. package/dist/lib/status.d.ts +442 -0
  130. package/dist/lib/status.js +1006 -0
  131. package/dist/lib/sync.d.ts +179 -0
  132. package/dist/lib/sync.js +782 -0
  133. package/dist/lib/target.d.ts +20 -0
  134. package/dist/lib/target.js +27 -0
  135. package/dist/lib/telegram-api.d.ts +541 -0
  136. package/dist/lib/telegram-api.js +1159 -0
  137. package/dist/lib/text-groups.d.ts +89 -0
  138. package/dist/lib/text-groups.js +317 -0
  139. package/dist/lib/thread-cleanup-manager.d.ts +288 -0
  140. package/dist/lib/thread-cleanup-manager.js +560 -0
  141. package/dist/lib/thread-display.d.ts +46 -0
  142. package/dist/lib/thread-display.js +257 -0
  143. package/dist/lib/thread-naming.d.ts +46 -0
  144. package/dist/lib/thread-naming.js +78 -0
  145. package/dist/lib/thread-reconciler.d.ts +239 -0
  146. package/dist/lib/thread-reconciler.js +644 -0
  147. package/dist/lib/threads.d.ts +621 -0
  148. package/dist/lib/threads.js +3679 -0
  149. package/dist/lib/time-injection.d.ts +15 -0
  150. package/dist/lib/time-injection.js +56 -0
  151. package/dist/lib/turns.d.ts +109 -0
  152. package/dist/lib/turns.js +500 -0
  153. package/dist/lib/updates.d.ts +1290 -0
  154. package/dist/lib/updates.js +3634 -0
  155. package/dist/lib/voice.d.ts +117 -0
  156. package/dist/lib/voice.js +174 -0
  157. package/dist/lib/workspace-admission.d.ts +264 -0
  158. package/dist/lib/workspace-admission.js +1136 -0
  159. package/dist/lib/workspace-retirement.d.ts +219 -0
  160. package/dist/lib/workspace-retirement.js +587 -0
  161. package/dist/lib/workspace-slots.d.ts +30 -0
  162. package/dist/lib/workspace-slots.js +54 -0
  163. package/dist/package.json +126 -0
  164. package/dist/pi-telegram/index.js +1 -0
  165. package/dist/skills/generated-control-surface/SKILL.md +107 -0
  166. package/dist/skills/generated-control-surface/references/capability-adapters.md +27 -0
  167. package/dist/skills/generated-control-surface/references/layout-and-state.md +37 -0
  168. package/dist/skills/generative-apps/SKILL.md +115 -0
  169. package/dist/skills/show-me/SKILL.md +166 -0
  170. package/dist/skills/show-me/references/telegram-surfaces.md +43 -0
  171. package/dist/skills/telegram-bridge/SKILL.md +129 -0
  172. package/dist/skills/telegram-bridge/references/configuration.md +15 -0
  173. package/dist/skills/telegram-bridge/references/delivery-and-threads.md +27 -0
  174. package/dist/skills/telegram-bridge/references/diagnosis.md +18 -0
  175. package/docs/architecture.md +1 -1
  176. package/docs/compact-matrix-literal.md +10 -3
  177. package/docs/generative-apps.md +15 -13
  178. package/docs/multi-instance-bus.md +17 -3
  179. package/docs/outbound.md +1 -1
  180. package/docs/public-api.md +2 -2
  181. package/lib/bindings.ts +89 -6
  182. package/lib/bus-follower.ts +11 -3
  183. package/lib/bus-leader.ts +57 -20
  184. package/lib/bus.ts +12 -2
  185. package/lib/commands.ts +2 -2
  186. package/lib/config.ts +14 -7
  187. package/lib/delivery.ts +38 -6
  188. package/lib/extension.ts +26 -1
  189. package/lib/generative-apps.ts +379 -13
  190. package/lib/menu-settings.ts +21 -5
  191. package/lib/outbound-markup.ts +16 -7
  192. package/lib/pi.ts +4 -0
  193. package/lib/sync.ts +3 -0
  194. package/lib/telegram-api.ts +12 -1
  195. package/lib/thread-cleanup-manager.ts +36 -6
  196. package/lib/thread-display.ts +96 -18
  197. package/lib/threads.ts +150 -30
  198. package/package.json +56 -13
  199. package/scripts/build-dist.mjs +44 -0
  200. package/scripts/measure-bus.mjs +8 -1
  201. package/scripts/measure-workspace.mjs +8 -1
  202. package/skills/generative-apps/SKILL.md +1 -1
  203. package/skills/show-me/SKILL.md +1 -1
  204. package/skills/telegram-bridge/SKILL.md +1 -1
package/lib/threads.ts CHANGED
@@ -52,6 +52,10 @@ export interface TelegramThreadNameInput {
52
52
  export interface TelegramWorkspaceBindingIdentity {
53
53
  cwd: string;
54
54
  workspaceKey: string;
55
+ /** Exact durable Pi session identity; absent only on legacy cwd-only bindings. */
56
+ sessionId?: string;
57
+ /** Full SHA-256 index component for session-qualified bindings. */
58
+ sessionKey?: string;
55
59
  /** Immutable legacy binding-key component, not the displayed global letter. */
56
60
  instanceSlot: string;
57
61
  bindingKey: string;
@@ -60,6 +64,27 @@ export interface TelegramWorkspaceBindingIdentity {
60
64
  }
61
65
 
62
66
  const TELEGRAM_WORKSPACE_KEY_MAX_LENGTH = 180;
67
+ const TELEGRAM_SESSION_ID_MAX_LENGTH = 256;
68
+
69
+ export function normalizeTelegramSessionId(
70
+ sessionId: string,
71
+ ): string | undefined {
72
+ if (typeof sessionId !== "string") return undefined;
73
+ const normalized = sessionId.trim();
74
+ return normalized && Buffer.byteLength(normalized, "utf8") <=
75
+ TELEGRAM_SESSION_ID_MAX_LENGTH
76
+ ? normalized
77
+ : undefined;
78
+ }
79
+
80
+ export function createTelegramSessionKey(
81
+ sessionId: string,
82
+ ): string | undefined {
83
+ const normalized = normalizeTelegramSessionId(sessionId);
84
+ return normalized
85
+ ? createHash("sha256").update(normalized).digest("hex")
86
+ : undefined;
87
+ }
63
88
 
64
89
  export function normalizeTelegramWorkspacePath(
65
90
  cwd: string,
@@ -116,21 +141,39 @@ function createTelegramWorkspaceBindingIdentityWithKey(
116
141
  cwd: string,
117
142
  workspaceKey: string,
118
143
  ordinal: number,
144
+ sessionId?: string,
119
145
  ): TelegramWorkspaceBindingIdentity | undefined {
120
146
  const instanceSlot = createTelegramWorkspaceInstanceSlot(ordinal);
121
147
  if (!instanceSlot) return undefined;
148
+ const normalizedSessionId = sessionId === undefined
149
+ ? undefined
150
+ : normalizeTelegramSessionId(sessionId);
151
+ const sessionKey = normalizedSessionId
152
+ ? createTelegramSessionKey(normalizedSessionId)
153
+ : undefined;
154
+ if (sessionId !== undefined && (!normalizedSessionId || !sessionKey)) {
155
+ return undefined;
156
+ }
157
+ const legacyBindingKey = instanceSlot === "a"
158
+ ? workspaceKey
159
+ : `${workspaceKey}${instanceSlot}`;
122
160
  return {
123
161
  cwd,
124
162
  workspaceKey,
163
+ ...(normalizedSessionId && sessionKey
164
+ ? { sessionId: normalizedSessionId, sessionKey }
165
+ : {}),
125
166
  instanceSlot,
126
- bindingKey:
127
- instanceSlot === "a" ? workspaceKey : `${workspaceKey}${instanceSlot}`,
167
+ bindingKey: sessionKey
168
+ ? `${legacyBindingKey}-s-${sessionKey}`
169
+ : legacyBindingKey,
128
170
  };
129
171
  }
130
172
 
131
173
  export function createTelegramWorkspaceBindingIdentity(
132
174
  cwd: string,
133
175
  ordinal = 0,
176
+ sessionId?: string,
134
177
  ): TelegramWorkspaceBindingIdentity | undefined {
135
178
  const normalized = normalizeTelegramWorkspacePath(cwd);
136
179
  const workspaceKey = normalized
@@ -141,6 +184,7 @@ export function createTelegramWorkspaceBindingIdentity(
141
184
  normalized,
142
185
  workspaceKey,
143
186
  ordinal,
187
+ sessionId,
144
188
  );
145
189
  }
146
190
 
@@ -249,6 +293,10 @@ export interface TelegramThreadIdentityRecord {
249
293
  export interface TelegramWorkspaceThreadBinding {
250
294
  cwd: string;
251
295
  workspaceKey: string;
296
+ /** Exact durable Pi session identity; absent only on legacy cwd-only bindings. */
297
+ sessionId?: string;
298
+ /** Full SHA-256 index component for session-qualified bindings. */
299
+ sessionKey?: string;
252
300
  instanceSlot: string;
253
301
  bindingKey: string;
254
302
  target: TelegramTarget & { threadId: number };
@@ -453,7 +501,8 @@ export interface TelegramTopicTargetStore {
453
501
  ) => Promise<boolean>;
454
502
  commitInactiveWorkspaceCleanup: (
455
503
  expected: TelegramWorkspaceThreadBinding | {
456
- cwd: string; workspaceKey: string; instanceSlot: string; slot: string; bindingKey: string;
504
+ cwd: string; workspaceKey: string; sessionId?: string; sessionKey?: string;
505
+ instanceSlot: string; slot: string; bindingKey: string;
457
506
  target: { chatId: number; threadId: number }; inactiveSinceMs: number; bindingUpdatedAtMs: number;
458
507
  },
459
508
  isCurrent: () => boolean,
@@ -465,7 +514,7 @@ export interface TelegramTopicTargetStore {
465
514
  ) => TelegramWorkspaceExternalProtectionEvidence,
466
515
  options?: { expectedRetirement?: TelegramWorkspaceRetirementIntent },
467
516
  ) => TelegramWorkspaceSlotOccupancySnapshot;
468
- hasWorkspaceBinding: (cwd: string) => boolean;
517
+ hasWorkspaceBinding: (cwd: string, sessionId?: string) => boolean;
469
518
  setWorkspaceDisplayTitle: (
470
519
  expected: TelegramWorkspaceThreadBinding,
471
520
  title: string,
@@ -478,6 +527,7 @@ export interface TelegramTopicTargetStore {
478
527
  getWorkspaceBinding: (
479
528
  cwd: string,
480
529
  instanceSlot?: string,
530
+ sessionId?: string,
481
531
  ) => TelegramWorkspaceThreadBinding | undefined;
482
532
  claimWorkspaceIdentity: (
483
533
  cwd: string,
@@ -485,6 +535,7 @@ export interface TelegramTopicTargetStore {
485
535
  previousInstanceId?: string,
486
536
  options?: {
487
537
  existingBindingOnly?: boolean;
538
+ sessionId?: string;
488
539
  onCapacityUnavailable?: () => void;
489
540
  },
490
541
  ) => TelegramWorkspaceBindingIdentity | undefined;
@@ -1124,9 +1175,12 @@ function cloneIdentityRecord(
1124
1175
  }
1125
1176
 
1126
1177
  function getWorkspaceBindingMapKey(
1127
- binding: Pick<TelegramWorkspaceThreadBinding, "cwd" | "instanceSlot">,
1178
+ binding: Pick<
1179
+ TelegramWorkspaceThreadBinding,
1180
+ "cwd" | "instanceSlot" | "sessionId"
1181
+ >,
1128
1182
  ): string {
1129
- return `${binding.cwd}\u0000${binding.instanceSlot}`;
1183
+ return `${binding.cwd}\u0000${binding.sessionId ?? ""}\u0000${binding.instanceSlot}`;
1130
1184
  }
1131
1185
 
1132
1186
  function normalizeWorkspaceBindingRecord(
@@ -1145,15 +1199,30 @@ function normalizeWorkspaceBindingRecord(
1145
1199
  return undefined;
1146
1200
  }
1147
1201
  const cwd = normalizeTelegramWorkspacePath(record.cwd);
1202
+ const sessionId = typeof record.sessionId === "string"
1203
+ ? normalizeTelegramSessionId(record.sessionId)
1204
+ : undefined;
1205
+ const sessionKey = typeof record.sessionKey === "string"
1206
+ ? record.sessionKey
1207
+ : undefined;
1208
+ const hasSessionFields = record.sessionId !== undefined ||
1209
+ record.sessionKey !== undefined;
1210
+ const expectedSessionKey = sessionId
1211
+ ? createTelegramSessionKey(sessionId)
1212
+ : undefined;
1213
+ const legacyBindingKey = record.instanceSlot === "a"
1214
+ ? record.workspaceKey
1215
+ : `${record.workspaceKey}${record.instanceSlot}`;
1216
+ const expectedBindingKey = expectedSessionKey
1217
+ ? `${legacyBindingKey}-s-${expectedSessionKey}`
1218
+ : legacyBindingKey;
1148
1219
  if (
1149
1220
  !cwd ||
1150
1221
  cwd !== record.cwd ||
1151
1222
  !record.workspaceKey ||
1152
1223
  !/^[a-z]+$/u.test(record.instanceSlot) ||
1153
- record.bindingKey !==
1154
- (record.instanceSlot === "a"
1155
- ? record.workspaceKey
1156
- : `${record.workspaceKey}${record.instanceSlot}`)
1224
+ (hasSessionFields && (!sessionId || sessionKey !== expectedSessionKey)) ||
1225
+ record.bindingKey !== expectedBindingKey
1157
1226
  ) {
1158
1227
  return undefined;
1159
1228
  }
@@ -1191,6 +1260,7 @@ function normalizeWorkspaceBindingRecord(
1191
1260
  return {
1192
1261
  cwd,
1193
1262
  workspaceKey: record.workspaceKey,
1263
+ ...(sessionId && sessionKey ? { sessionId, sessionKey } : {}),
1194
1264
  instanceSlot: record.instanceSlot,
1195
1265
  bindingKey: record.bindingKey,
1196
1266
  ...(record.showSlotSuffix === true ? { showSlotSuffix: true } : {}),
@@ -1684,7 +1754,10 @@ export function createTelegramTopicTargetStore(
1684
1754
  );
1685
1755
  let workspaceClaims = new Map<
1686
1756
  string,
1687
- { identity: TelegramWorkspaceBindingIdentity; instanceId: string }
1757
+ {
1758
+ identity: TelegramWorkspaceBindingIdentity;
1759
+ instanceId: string;
1760
+ }
1688
1761
  >();
1689
1762
  let reservations: TelegramThreadReservation[] = [];
1690
1763
  let pendingProvisions: TelegramThreadPendingProvision[] = [];
@@ -2404,8 +2477,16 @@ export function createTelegramTopicTargetStore(
2404
2477
  if (workspaceRetirementCommitInFlight || !isCurrent()) return false;
2405
2478
  const cleanupSnapshot = "bindingUpdatedAtMs" in expected ? expected : undefined;
2406
2479
  const normalized = cleanupSnapshot ? undefined : normalizeWorkspaceBindingRecord(expected);
2480
+ const cleanupSessionId = cleanupSnapshot?.sessionId === undefined
2481
+ ? undefined
2482
+ : normalizeTelegramSessionId(cleanupSnapshot.sessionId);
2483
+ const cleanupSessionKey = cleanupSessionId
2484
+ ? createTelegramSessionKey(cleanupSessionId)
2485
+ : undefined;
2407
2486
  if (cleanupSnapshot && (!cleanupSnapshot.cwd || !cleanupSnapshot.workspaceKey ||
2408
2487
  !cleanupSnapshot.instanceSlot || !cleanupSnapshot.slot || !cleanupSnapshot.bindingKey ||
2488
+ ((cleanupSnapshot.sessionId !== undefined || cleanupSnapshot.sessionKey !== undefined) &&
2489
+ (!cleanupSessionId || cleanupSnapshot.sessionKey !== cleanupSessionKey)) ||
2409
2490
  !Number.isSafeInteger(cleanupSnapshot.inactiveSinceMs) ||
2410
2491
  !Number.isSafeInteger(cleanupSnapshot.bindingUpdatedAtMs) ||
2411
2492
  !Number.isSafeInteger(cleanupSnapshot.target.chatId) ||
@@ -2416,6 +2497,7 @@ export function createTelegramTopicTargetStore(
2416
2497
  if (!binding) return isCurrent();
2417
2498
  const exact = cleanupSnapshot ? binding.cwd === cleanupSnapshot.cwd &&
2418
2499
  binding.workspaceKey === cleanupSnapshot.workspaceKey &&
2500
+ binding.sessionId === cleanupSessionId && binding.sessionKey === cleanupSessionKey &&
2419
2501
  binding.instanceSlot === cleanupSnapshot.instanceSlot && binding.slot === cleanupSnapshot.slot &&
2420
2502
  binding.bindingKey === cleanupSnapshot.bindingKey && targetMatches(binding.target, cleanupSnapshot.target) &&
2421
2503
  binding.inactiveSinceMs === cleanupSnapshot.inactiveSinceMs &&
@@ -2612,10 +2694,17 @@ export function createTelegramTopicTargetStore(
2612
2694
  : [...localReservedSlots, "invalid"];
2613
2695
  return { bindings, reservedSlots };
2614
2696
  },
2615
- hasWorkspaceBinding(cwd) {
2697
+ hasWorkspaceBinding(cwd, sessionId) {
2616
2698
  const normalizedCwd = normalizeTelegramWorkspacePath(cwd);
2617
- return !!normalizedCwd && Array.from(workspaceBindings.values()).some(
2618
- (binding) => binding.cwd === normalizedCwd,
2699
+ const normalizedSessionId = sessionId === undefined
2700
+ ? undefined
2701
+ : normalizeTelegramSessionId(sessionId);
2702
+ if (!normalizedCwd || (sessionId !== undefined && !normalizedSessionId)) {
2703
+ return false;
2704
+ }
2705
+ return Array.from(workspaceBindings.values()).some(
2706
+ (binding) => binding.cwd === normalizedCwd &&
2707
+ binding.sessionId === normalizedSessionId,
2619
2708
  );
2620
2709
  },
2621
2710
  setWorkspaceDisplayTitle(expected, title) {
@@ -2651,18 +2740,29 @@ export function createTelegramTopicTargetStore(
2651
2740
  }
2652
2741
  return false;
2653
2742
  },
2654
- getWorkspaceBinding(cwd, instanceSlot = "a") {
2743
+ getWorkspaceBinding(cwd, instanceSlot = "a", sessionId) {
2655
2744
  const normalizedCwd = normalizeTelegramWorkspacePath(cwd);
2656
- if (!normalizedCwd || !/^[a-z]+$/u.test(instanceSlot)) return undefined;
2657
- const binding = workspaceBindings.get(
2658
- getWorkspaceBindingMapKey({ cwd: normalizedCwd, instanceSlot }),
2659
- );
2745
+ const normalizedSessionId = sessionId === undefined
2746
+ ? undefined
2747
+ : normalizeTelegramSessionId(sessionId);
2748
+ if (!normalizedCwd || !/^[a-z]+$/u.test(instanceSlot) ||
2749
+ (sessionId !== undefined && !normalizedSessionId)) return undefined;
2750
+ const mapKey = getWorkspaceBindingMapKey({
2751
+ cwd: normalizedCwd,
2752
+ instanceSlot,
2753
+ ...(normalizedSessionId ? { sessionId: normalizedSessionId } : {}),
2754
+ });
2755
+ const binding = workspaceBindings.get(mapKey);
2660
2756
  return binding ? cloneWorkspaceBinding(binding) : undefined;
2661
2757
  },
2662
2758
  claimWorkspaceIdentity(cwd, instanceId, previousInstanceId, options) {
2663
2759
  if (workspaceRetirementCommitInFlight) return undefined;
2664
2760
  const normalizedCwd = normalizeTelegramWorkspacePath(cwd);
2761
+ const normalizedSessionId = options?.sessionId === undefined
2762
+ ? undefined
2763
+ : normalizeTelegramSessionId(options.sessionId);
2665
2764
  if (!normalizedCwd || !instanceId ||
2765
+ (options?.sessionId !== undefined && !normalizedSessionId) ||
2666
2766
  hasWorkspaceRetirementConflict({ cwd: normalizedCwd })) return undefined;
2667
2767
  const externalReservedSlots = captureExternalReservedSlots();
2668
2768
  if (!externalReservedSlots) {
@@ -2674,7 +2774,9 @@ export function createTelegramTopicTargetStore(
2674
2774
  );
2675
2775
  for (const claim of workspaceClaims.values()) {
2676
2776
  if (claim.instanceId !== instanceId) continue;
2677
- if (claim.identity.cwd !== normalizedCwd || !claim.identity.slot ||
2777
+ if (claim.identity.cwd !== normalizedCwd ||
2778
+ claim.identity.sessionId !== normalizedSessionId ||
2779
+ !claim.identity.slot ||
2678
2780
  externalReservedSlotKeys.includes(claim.identity.slot.toLowerCase())) {
2679
2781
  return undefined;
2680
2782
  }
@@ -2686,11 +2788,13 @@ export function createTelegramTopicTargetStore(
2686
2788
  }
2687
2789
  const workspaceKey = resolveWorkspaceKey(normalizedCwd);
2688
2790
  if (!workspaceKey) return undefined;
2689
- let legacyRecord = findLegacyWorkspaceMigrationRecord(
2690
- normalizedCwd,
2691
- instanceId,
2692
- previousInstanceId,
2693
- );
2791
+ let legacyRecord = normalizedSessionId
2792
+ ? undefined
2793
+ : findLegacyWorkspaceMigrationRecord(
2794
+ normalizedCwd,
2795
+ instanceId,
2796
+ previousInstanceId,
2797
+ );
2694
2798
  const legacyTarget = legacyRecord?.target;
2695
2799
  const targetBinding = legacyTarget
2696
2800
  ? Array.from(workspaceBindings.values()).find(
@@ -2772,12 +2876,16 @@ export function createTelegramTopicTargetStore(
2772
2876
  }
2773
2877
  if (!slot || !/^[A-Z]$/u.test(slot)) return undefined;
2774
2878
  const claimedIdentity = { ...identity, slot };
2775
- workspaceClaims.set(mapKey, { identity: claimedIdentity, instanceId });
2879
+ workspaceClaims.set(mapKey, {
2880
+ identity: claimedIdentity,
2881
+ instanceId,
2882
+ });
2776
2883
  return { ...claimedIdentity };
2777
2884
  };
2778
2885
  if (options?.existingBindingOnly) {
2779
2886
  const candidates = Array.from(workspaceBindings.values())
2780
- .filter((binding) => binding.cwd === normalizedCwd)
2887
+ .filter((binding) => binding.cwd === normalizedCwd &&
2888
+ binding.sessionId === normalizedSessionId)
2781
2889
  .sort((left, right) =>
2782
2890
  left.instanceSlot.length - right.instanceSlot.length ||
2783
2891
  left.instanceSlot.localeCompare(right.instanceSlot),
@@ -2786,6 +2894,9 @@ export function createTelegramTopicTargetStore(
2786
2894
  const claimed = claimIdentity({
2787
2895
  cwd: binding.cwd,
2788
2896
  workspaceKey: binding.workspaceKey,
2897
+ ...(binding.sessionId && binding.sessionKey
2898
+ ? { sessionId: binding.sessionId, sessionKey: binding.sessionKey }
2899
+ : {}),
2789
2900
  instanceSlot: binding.instanceSlot,
2790
2901
  bindingKey: binding.bindingKey,
2791
2902
  });
@@ -2811,6 +2922,7 @@ export function createTelegramTopicTargetStore(
2811
2922
  normalizedCwd,
2812
2923
  workspaceKey,
2813
2924
  ordinal,
2925
+ normalizedSessionId,
2814
2926
  );
2815
2927
  if (!identity) return undefined;
2816
2928
  const mapKey = getWorkspaceBindingMapKey(identity);
@@ -2850,6 +2962,8 @@ export function createTelegramTopicTargetStore(
2850
2962
  const next = normalizeWorkspaceBindingRecord(binding);
2851
2963
  if (next && hasWorkspaceRetirementConflict(next)) return undefined;
2852
2964
  if (!next) return undefined;
2965
+ const nextMapKey = getWorkspaceBindingMapKey(next);
2966
+ const claim = workspaceClaims.get(nextMapKey);
2853
2967
  if (next.slot && Array.from(workspaceBindings.values()).some((existing) =>
2854
2968
  existing.bindingKey !== next.bindingKey && existing.slot === next.slot,
2855
2969
  )) return undefined;
@@ -2861,9 +2975,11 @@ export function createTelegramTopicTargetStore(
2861
2975
  return undefined;
2862
2976
  }
2863
2977
  }
2864
- const nextMapKey = getWorkspaceBindingMapKey(next);
2865
2978
  const previous = workspaceBindings.get(nextMapKey);
2866
2979
  if (previous?.showSlotSuffix) next.showSlotSuffix = true;
2980
+ if (previous?.threadName && !next.threadName) {
2981
+ next.threadName = previous.threadName;
2982
+ }
2867
2983
  if (previous?.manualThreadName && !next.manualThreadName) {
2868
2984
  next.manualThreadName = previous.manualThreadName;
2869
2985
  }
@@ -2884,7 +3000,6 @@ export function createTelegramTopicTargetStore(
2884
3000
  if (previous.displayTitle) next.displayTitle = previous.displayTitle;
2885
3001
  if (previous.inactiveSinceMs !== undefined) next.inactiveSinceMs = previous.inactiveSinceMs;
2886
3002
  }
2887
- const claim = workspaceClaims.get(nextMapKey);
2888
3003
  if (claimInstanceId) {
2889
3004
  if (claim?.instanceId !== claimInstanceId) return undefined;
2890
3005
  if (next.slot !== undefined && claim.identity.slot !== next.slot) return undefined;
@@ -3460,6 +3575,7 @@ export interface TelegramPromoteFollowerBindingToLeaderDeps {
3460
3575
  store: TelegramTopicTargetStore;
3461
3576
  instanceId: string;
3462
3577
  cwd?: string;
3578
+ sessionId?: string;
3463
3579
  telegramProfile?: string;
3464
3580
  target?: TelegramTarget;
3465
3581
  slot?: string;
@@ -3483,11 +3599,15 @@ export async function promoteTelegramFollowerBindingToLeader(
3483
3599
  );
3484
3600
  const workspaceIdentity = deps.cwd
3485
3601
  ? deps.store.claimWorkspaceIdentity(deps.cwd, deps.instanceId,
3486
- existing?.instanceId, { existingBindingOnly: true })
3602
+ existing?.instanceId, {
3603
+ existingBindingOnly: true,
3604
+ sessionId: deps.sessionId,
3605
+ })
3487
3606
  : undefined;
3488
3607
  const workspaceBinding = workspaceIdentity
3489
3608
  ? deps.store.getWorkspaceBinding(
3490
3609
  workspaceIdentity.cwd, workspaceIdentity.instanceSlot,
3610
+ workspaceIdentity.sessionId,
3491
3611
  )
3492
3612
  : undefined;
3493
3613
  const exactWorkspaceBinding = workspaceBinding &&
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-telegram",
3
- "version": "0.45.11",
3
+ "version": "0.47.0",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -30,15 +30,19 @@
30
30
  "test": "node --experimental-strip-types --test --test-concurrency=4 --test-reporter=dot tests/*.test.ts",
31
31
  "test:verbose": "node --experimental-strip-types --test --test-reporter=spec tests/*.test.ts",
32
32
  "typecheck": "tsc --noEmit",
33
+ "build": "node scripts/build-dist.mjs",
34
+ "prepack": "npm run build",
35
+ "check": "node -e \"await import('./dist/pi-telegram/index.js'); console.log('pi-telegram: extension import ok')\"",
33
36
  "audit": "npm audit --omit=peer",
34
37
  "audit:host": "npm audit",
35
38
  "pack:check": "npm pack --dry-run",
36
- "validate": "npm run typecheck && npm test && npm run audit && npm run pack:check"
39
+ "validate": "npm run build && npm run typecheck && npm test && npm run audit && npm run check && npm run pack:check"
37
40
  },
38
41
  "files": [
39
42
  "index.ts",
40
43
  "api/",
41
44
  "lib/",
45
+ "dist/",
42
46
  "README.md",
43
47
  "AGENTS.md",
44
48
  "BACKLOG.md",
@@ -49,23 +53,62 @@
49
53
  "screenshot.png"
50
54
  ],
51
55
  "exports": {
52
- ".": "./index.ts",
53
- "./inbound": "./api/inbound.ts",
54
- "./outbound": "./api/outbound.ts",
55
- "./delivery": "./api/delivery.ts",
56
- "./activity": "./api/activity.ts",
57
- "./updates": "./api/updates.ts",
58
- "./commands": "./api/commands.ts",
59
- "./sections": "./api/sections.ts",
60
- "./status": "./api/status.ts",
61
- "./voice": "./api/voice.ts",
62
- "./keyboard": "./api/keyboard.ts"
56
+ ".": {
57
+ "types": "./dist/index.d.ts",
58
+ "default": "./dist/index.js"
59
+ },
60
+ "./inbound": {
61
+ "types": "./dist/api/inbound.d.ts",
62
+ "default": "./dist/api/inbound.js"
63
+ },
64
+ "./outbound": {
65
+ "types": "./dist/api/outbound.d.ts",
66
+ "default": "./dist/api/outbound.js"
67
+ },
68
+ "./delivery": {
69
+ "types": "./dist/api/delivery.d.ts",
70
+ "default": "./dist/api/delivery.js"
71
+ },
72
+ "./activity": {
73
+ "types": "./dist/api/activity.d.ts",
74
+ "default": "./dist/api/activity.js"
75
+ },
76
+ "./updates": {
77
+ "types": "./dist/api/updates.d.ts",
78
+ "default": "./dist/api/updates.js"
79
+ },
80
+ "./commands": {
81
+ "types": "./dist/api/commands.d.ts",
82
+ "default": "./dist/api/commands.js"
83
+ },
84
+ "./sections": {
85
+ "types": "./dist/api/sections.d.ts",
86
+ "default": "./dist/api/sections.js"
87
+ },
88
+ "./status": {
89
+ "types": "./dist/api/status.d.ts",
90
+ "default": "./dist/api/status.js"
91
+ },
92
+ "./voice": {
93
+ "types": "./dist/api/voice.d.ts",
94
+ "default": "./dist/api/voice.js"
95
+ },
96
+ "./keyboard": {
97
+ "types": "./dist/api/keyboard.d.ts",
98
+ "default": "./dist/api/keyboard.js"
99
+ }
63
100
  },
64
101
  "pi": {
65
102
  "extensions": [
103
+ "./dist/pi-telegram/index.js"
104
+ ],
105
+ "sourceExtensions": [
66
106
  "./index.ts"
67
107
  ],
68
108
  "skills": [
109
+ "./dist/skills"
110
+ ],
111
+ "sourceSkills": [
69
112
  "./skills"
70
113
  ],
71
114
  "image": "https://raw.githubusercontent.com/llblab/pi-telegram/main/screenshot.png"
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Builds the distributive JavaScript, declarations, Pi entrypoint, and Skills.
5
+ * Usage: node scripts/build-dist.mjs
6
+ *
7
+ * The build replaces `dist`, compiles with `tsconfig.build.json`, then copies
8
+ * package-owned runtime assets. It does not publish, pack, or modify sources.
9
+ */
10
+
11
+ import { spawnSync } from "node:child_process";
12
+ import { cpSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
13
+ import { join } from "node:path";
14
+
15
+ function run(command, args) {
16
+ const result = spawnSync(command, args, { stdio: "inherit" });
17
+ if (result.error) throw result.error;
18
+ if (result.status !== 0) process.exit(result.status ?? 1);
19
+ }
20
+
21
+ rmSync("dist", { recursive: true, force: true });
22
+ mkdirSync("dist", { recursive: true });
23
+
24
+ run(process.execPath, [
25
+ join("node_modules", "typescript", "bin", "tsc"),
26
+ "-p",
27
+ "tsconfig.build.json",
28
+ ]);
29
+
30
+ mkdirSync(join("dist", "pi-telegram"), { recursive: true });
31
+ writeFileSync(
32
+ join("dist", "pi-telegram", "index.js"),
33
+ 'export { default } from "../index.js";\n',
34
+ "utf8",
35
+ );
36
+
37
+ cpSync("skills", join("dist", "skills"), { recursive: true });
38
+ cpSync("package.json", join("dist", "package.json"));
39
+ cpSync(
40
+ join("lib", "generative-app-worker.mjs"),
41
+ join("dist", "lib", "generative-app-worker.mjs"),
42
+ );
43
+
44
+ run(process.execPath, ["--check", join("dist", "pi-telegram", "index.js")]);
@@ -1,4 +1,11 @@
1
- // Counts isolated registry work without sockets, configured profiles, or Telegram calls.
1
+ /**
2
+ * Counts isolated synchronous bus-registry work at representative sizes.
3
+ * Usage: node --experimental-strip-types scripts/measure-bus.mjs
4
+ *
5
+ * This assertion-backed fixture instruments Map operations only. It does not
6
+ * open sockets, read configured profiles, call Telegram, or measure latency.
7
+ */
8
+
2
9
  import assert from "node:assert/strict";
3
10
  import { createTelegramBusFollowerRegistry, createTelegramBusProtocolIdentity } from "../lib/bus.ts";
4
11
 
@@ -1,4 +1,11 @@
1
- // Measures isolated Thread-store work; never reads configured profiles or calls Telegram.
1
+ /**
2
+ * Measures isolated Thread-store filesystem work at representative sizes.
3
+ * Usage: node --experimental-strip-types scripts/measure-workspace.mjs
4
+ *
5
+ * This assertion-backed fixture uses and removes its own temporary directory.
6
+ * It never reads configured profiles, calls Telegram, or measures latency.
7
+ */
8
+
2
9
  import assert from "node:assert/strict";
3
10
  import fs from "node:fs/promises";
4
11
  import { syncBuiltinESMExports } from "node:module";
@@ -97,7 +97,7 @@ ordinary prompt → model-mediated interpretation or explanation
97
97
  - Keep credentials and unrelated private state out of source, state, output, and diagnostics.
98
98
  - Route consequences requiring contextual judgment through an ordinary model prompt.
99
99
  - Fail closed on unavailable owners, stale actions, malformed state, absent methods, process failures, or uncertain effects.
100
- - Do not claim automatic refresh, removal, voice output, or other behavior still marked incomplete in the runtime document.
100
+ - Treat `refreshAfterMs` validation, output-only `refresh`, scheduling after a successful hinted explicit `telegram_bind` delivery, same-handle rescheduling after a current-surface bound action, Delivery edit-failure classification, known unavailable-message invalidation with bounded classified diagnostics, session/runtime live-surface cancellation, and exact direct classic/leader/authenticated-follower target retention as implemented. The Bot API has no ordinary private-message deletion update; `deleted_business_messages` is limited to connected business accounts. Do not claim removal, voice output, proactive deletion-update invalidation, or other behavior still marked incomplete in the runtime document.
101
101
 
102
102
  ## Validation
103
103
 
@@ -10,7 +10,7 @@ Help the user understand the current topic. Skip the preamble and keep prose bri
10
10
  - Infer the subject from the conversation and honor an explicitly requested format. With plain `show me`, choose the form that best explains the subject.
11
11
  - Markdown in the reply is a complete output format for both chat and terminal surfaces. Use headings, emphasis, lists, and focused code blocks to explain outcomes, comparisons, and reasoning; add a diagram when relationships need one.
12
12
  - `Show me markdown` requests a rendered Markdown reply. Create a Markdown file only when the user asks for a saved document or file artifact.
13
- - `Show me html` requests a focused HTML file. Save file artifacts in the project or filesystem as appropriate and deliver them through the active environment's file-delivery mechanism; open locally when that is the requested surface.
13
+ - `Show me html` requests a focused HTML file. Prefer an accessible, art-directed interactive infographic when timelines, comparisons, layers, or progressive disclosure clarify the subject; otherwise stay static. Preserve truthful labels and reduced motion. Save and deliver the artifact through the active environment; open it locally when requested.
14
14
 
15
15
  ### Surface Routing
16
16
 
@@ -60,7 +60,7 @@ Every enabled button has a self-contained prompt and an optional selection style
60
60
  - `{label|prompt}` separates visible label from queued prompt.
61
61
  - `{label|prompt|selected_style}` and `{|prompt|selected_style}` accept `primary`, `success`, or `danger`.
62
62
  - Fourth-position `1`/`true` disables, `0`/`false` enables; omission means enabled. JSON uses boolean `disabled`. `{|Next||1}` omits label/style; `{Next|||1}` omits prompt/style; `{|||1}` is blank (Telegram receives a non-breaking space). Prefer meaningful labels and retain a useful enabled action. Enabled CML requires a prompt. Disabled controls stay visible but have no callback, queued prompt, or bound-method invocation.
63
- - Top-level cells form vertical rows; one nested row groups horizontal peers.
63
+ - Top-level cells form vertical rows; inside a fenced block, adjacent top-level JSON/CML objects may omit the outer array and commas. One nested row groups horizontal peers.
64
64
  - Prefer one matrix per related group. Fenced blocks stay in place in Rich mode; HTML compatibility moves them to the footer. Native rows allow eight buttons. Malformed/oversized/unclosed blocks activate nothing; drafts hide them. Outer code fences and quoted/indented examples remain literal.
65
65
  - Both placements share prompt/app routing. In-body clicks acknowledge without recoloring; selected-style highlighting remains footer-only.
66
66