@llblab/pi-kit 0.1.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 (523) hide show
  1. package/AGENTS.md +35 -0
  2. package/BACKLOG.md +3 -0
  3. package/CHANGELOG.md +9 -0
  4. package/LICENSE +21 -0
  5. package/README.md +43 -0
  6. package/node_modules/@llblab/pi-actors/AGENTS.md +150 -0
  7. package/node_modules/@llblab/pi-actors/BACKLOG.md +6 -0
  8. package/node_modules/@llblab/pi-actors/CHANGELOG.md +865 -0
  9. package/node_modules/@llblab/pi-actors/README.md +231 -0
  10. package/node_modules/@llblab/pi-actors/banner.jpg +0 -0
  11. package/node_modules/@llblab/pi-actors/dist/fixtures/protocol/artifact-manifest.json +9 -0
  12. package/node_modules/@llblab/pi-actors/dist/fixtures/protocol/control-endpoint.json +6 -0
  13. package/node_modules/@llblab/pi-actors/dist/fixtures/protocol/control-record.json +9 -0
  14. package/node_modules/@llblab/pi-actors/dist/fixtures/protocol/recipe-summary.json +8 -0
  15. package/node_modules/@llblab/pi-actors/dist/fixtures/protocol/run-state.json +10 -0
  16. package/node_modules/@llblab/pi-actors/dist/fixtures/protocol/trace-event.json +9 -0
  17. package/node_modules/@llblab/pi-actors/dist/index.d.ts +7 -0
  18. package/node_modules/@llblab/pi-actors/dist/index.js +17 -0
  19. package/node_modules/@llblab/pi-actors/dist/lib/async-runs.d.ts +146 -0
  20. package/node_modules/@llblab/pi-actors/dist/lib/async-runs.js +800 -0
  21. package/node_modules/@llblab/pi-actors/dist/lib/automatic-review-runtime.d.ts +20 -0
  22. package/node_modules/@llblab/pi-actors/dist/lib/automatic-review-runtime.js +103 -0
  23. package/node_modules/@llblab/pi-actors/dist/lib/command-templates.d.ts +85 -0
  24. package/node_modules/@llblab/pi-actors/dist/lib/command-templates.js +804 -0
  25. package/node_modules/@llblab/pi-actors/dist/lib/config.d.ts +34 -0
  26. package/node_modules/@llblab/pi-actors/dist/lib/config.js +216 -0
  27. package/node_modules/@llblab/pi-actors/dist/lib/control-projection.d.ts +20 -0
  28. package/node_modules/@llblab/pi-actors/dist/lib/control-projection.js +66 -0
  29. package/node_modules/@llblab/pi-actors/dist/lib/control.d.ts +15 -0
  30. package/node_modules/@llblab/pi-actors/dist/lib/control.js +97 -0
  31. package/node_modules/@llblab/pi-actors/dist/lib/draft-consolidation-transaction.d.ts +65 -0
  32. package/node_modules/@llblab/pi-actors/dist/lib/draft-consolidation-transaction.js +610 -0
  33. package/node_modules/@llblab/pi-actors/dist/lib/draft-consolidation.d.ts +35 -0
  34. package/node_modules/@llblab/pi-actors/dist/lib/draft-consolidation.js +126 -0
  35. package/node_modules/@llblab/pi-actors/dist/lib/draft-review.d.ts +56 -0
  36. package/node_modules/@llblab/pi-actors/dist/lib/draft-review.js +254 -0
  37. package/node_modules/@llblab/pi-actors/dist/lib/draft-sleep.d.ts +65 -0
  38. package/node_modules/@llblab/pi-actors/dist/lib/draft-sleep.js +467 -0
  39. package/node_modules/@llblab/pi-actors/dist/lib/execution-output.d.ts +25 -0
  40. package/node_modules/@llblab/pi-actors/dist/lib/execution-output.js +88 -0
  41. package/node_modules/@llblab/pi-actors/dist/lib/execution-sessions.d.ts +17 -0
  42. package/node_modules/@llblab/pi-actors/dist/lib/execution-sessions.js +85 -0
  43. package/node_modules/@llblab/pi-actors/dist/lib/execution.d.ts +99 -0
  44. package/node_modules/@llblab/pi-actors/dist/lib/execution.js +725 -0
  45. package/node_modules/@llblab/pi-actors/dist/lib/extension-runtime.d.ts +21 -0
  46. package/node_modules/@llblab/pi-actors/dist/lib/extension-runtime.js +141 -0
  47. package/node_modules/@llblab/pi-actors/dist/lib/file-state.d.ts +18 -0
  48. package/node_modules/@llblab/pi-actors/dist/lib/file-state.js +310 -0
  49. package/node_modules/@llblab/pi-actors/dist/lib/identity.d.ts +9 -0
  50. package/node_modules/@llblab/pi-actors/dist/lib/identity.js +27 -0
  51. package/node_modules/@llblab/pi-actors/dist/lib/inspector-actions.d.ts +16 -0
  52. package/node_modules/@llblab/pi-actors/dist/lib/inspector-actions.js +57 -0
  53. package/node_modules/@llblab/pi-actors/dist/lib/inspector-command.d.ts +7 -0
  54. package/node_modules/@llblab/pi-actors/dist/lib/inspector-command.js +37 -0
  55. package/node_modules/@llblab/pi-actors/dist/lib/inspector-overlay.d.ts +100 -0
  56. package/node_modules/@llblab/pi-actors/dist/lib/inspector-overlay.js +824 -0
  57. package/node_modules/@llblab/pi-actors/dist/lib/inspector.d.ts +20 -0
  58. package/node_modules/@llblab/pi-actors/dist/lib/inspector.js +136 -0
  59. package/node_modules/@llblab/pi-actors/dist/lib/limits.d.ts +28 -0
  60. package/node_modules/@llblab/pi-actors/dist/lib/limits.js +28 -0
  61. package/node_modules/@llblab/pi-actors/dist/lib/model-context.d.ts +56 -0
  62. package/node_modules/@llblab/pi-actors/dist/lib/model-context.js +220 -0
  63. package/node_modules/@llblab/pi-actors/dist/lib/observability.d.ts +186 -0
  64. package/node_modules/@llblab/pi-actors/dist/lib/observability.js +951 -0
  65. package/node_modules/@llblab/pi-actors/dist/lib/paths.d.ts +27 -0
  66. package/node_modules/@llblab/pi-actors/dist/lib/paths.js +72 -0
  67. package/node_modules/@llblab/pi-actors/dist/lib/pi.d.ts +19 -0
  68. package/node_modules/@llblab/pi-actors/dist/lib/pi.js +21 -0
  69. package/node_modules/@llblab/pi-actors/dist/lib/preflight-diagnostics.d.ts +27 -0
  70. package/node_modules/@llblab/pi-actors/dist/lib/preflight-diagnostics.js +86 -0
  71. package/node_modules/@llblab/pi-actors/dist/lib/prompts.d.ts +24 -0
  72. package/node_modules/@llblab/pi-actors/dist/lib/prompts.js +49 -0
  73. package/node_modules/@llblab/pi-actors/dist/lib/recipe-control.d.ts +7 -0
  74. package/node_modules/@llblab/pi-actors/dist/lib/recipe-control.js +43 -0
  75. package/node_modules/@llblab/pi-actors/dist/lib/recipes-context.d.ts +36 -0
  76. package/node_modules/@llblab/pi-actors/dist/lib/recipes-context.js +197 -0
  77. package/node_modules/@llblab/pi-actors/dist/lib/recipes-discovery.d.ts +77 -0
  78. package/node_modules/@llblab/pi-actors/dist/lib/recipes-discovery.js +783 -0
  79. package/node_modules/@llblab/pi-actors/dist/lib/recipes-references.d.ts +113 -0
  80. package/node_modules/@llblab/pi-actors/dist/lib/recipes-references.js +1108 -0
  81. package/node_modules/@llblab/pi-actors/dist/lib/recipes-usage.d.ts +21 -0
  82. package/node_modules/@llblab/pi-actors/dist/lib/recipes-usage.js +494 -0
  83. package/node_modules/@llblab/pi-actors/dist/lib/registry.d.ts +67 -0
  84. package/node_modules/@llblab/pi-actors/dist/lib/registry.js +504 -0
  85. package/node_modules/@llblab/pi-actors/dist/lib/review-control.d.ts +14 -0
  86. package/node_modules/@llblab/pi-actors/dist/lib/review-control.js +110 -0
  87. package/node_modules/@llblab/pi-actors/dist/lib/review-diagnostics.d.ts +11 -0
  88. package/node_modules/@llblab/pi-actors/dist/lib/review-diagnostics.js +148 -0
  89. package/node_modules/@llblab/pi-actors/dist/lib/review-projection.d.ts +14 -0
  90. package/node_modules/@llblab/pi-actors/dist/lib/review-projection.js +166 -0
  91. package/node_modules/@llblab/pi-actors/dist/lib/run-evidence-policy.d.ts +95 -0
  92. package/node_modules/@llblab/pi-actors/dist/lib/run-evidence-policy.js +177 -0
  93. package/node_modules/@llblab/pi-actors/dist/lib/run-ui-runtime.d.ts +18 -0
  94. package/node_modules/@llblab/pi-actors/dist/lib/run-ui-runtime.js +125 -0
  95. package/node_modules/@llblab/pi-actors/dist/lib/runs-artifacts.d.ts +21 -0
  96. package/node_modules/@llblab/pi-actors/dist/lib/runs-artifacts.js +68 -0
  97. package/node_modules/@llblab/pi-actors/dist/lib/runs-control-delivery.d.ts +28 -0
  98. package/node_modules/@llblab/pi-actors/dist/lib/runs-control-delivery.js +150 -0
  99. package/node_modules/@llblab/pi-actors/dist/lib/runs-control.d.ts +21 -0
  100. package/node_modules/@llblab/pi-actors/dist/lib/runs-control.js +89 -0
  101. package/node_modules/@llblab/pi-actors/dist/lib/runs-controls.d.ts +39 -0
  102. package/node_modules/@llblab/pi-actors/dist/lib/runs-controls.js +283 -0
  103. package/node_modules/@llblab/pi-actors/dist/lib/runs-identity.d.ts +5 -0
  104. package/node_modules/@llblab/pi-actors/dist/lib/runs-identity.js +11 -0
  105. package/node_modules/@llblab/pi-actors/dist/lib/runs-index.d.ts +27 -0
  106. package/node_modules/@llblab/pi-actors/dist/lib/runs-index.js +142 -0
  107. package/node_modules/@llblab/pi-actors/dist/lib/runs-ownership.d.ts +7 -0
  108. package/node_modules/@llblab/pi-actors/dist/lib/runs-ownership.js +96 -0
  109. package/node_modules/@llblab/pi-actors/dist/lib/runs-parent-teardown.d.ts +51 -0
  110. package/node_modules/@llblab/pi-actors/dist/lib/runs-parent-teardown.js +172 -0
  111. package/node_modules/@llblab/pi-actors/dist/lib/runs-process.d.ts +22 -0
  112. package/node_modules/@llblab/pi-actors/dist/lib/runs-process.js +125 -0
  113. package/node_modules/@llblab/pi-actors/dist/lib/runs-retention.d.ts +18 -0
  114. package/node_modules/@llblab/pi-actors/dist/lib/runs-retention.js +108 -0
  115. package/node_modules/@llblab/pi-actors/dist/lib/runs-start.d.ts +12 -0
  116. package/node_modules/@llblab/pi-actors/dist/lib/runs-start.js +68 -0
  117. package/node_modules/@llblab/pi-actors/dist/lib/runs-status.d.ts +10 -0
  118. package/node_modules/@llblab/pi-actors/dist/lib/runs-status.js +68 -0
  119. package/node_modules/@llblab/pi-actors/dist/lib/runs-trace.d.ts +48 -0
  120. package/node_modules/@llblab/pi-actors/dist/lib/runs-trace.js +491 -0
  121. package/node_modules/@llblab/pi-actors/dist/lib/runtime-identity.d.ts +7 -0
  122. package/node_modules/@llblab/pi-actors/dist/lib/runtime-identity.js +35 -0
  123. package/node_modules/@llblab/pi-actors/dist/lib/runtime-triage.d.ts +29 -0
  124. package/node_modules/@llblab/pi-actors/dist/lib/runtime-triage.js +60 -0
  125. package/node_modules/@llblab/pi-actors/dist/lib/runtime.d.ts +69 -0
  126. package/node_modules/@llblab/pi-actors/dist/lib/runtime.js +339 -0
  127. package/node_modules/@llblab/pi-actors/dist/lib/schema.d.ts +60 -0
  128. package/node_modules/@llblab/pi-actors/dist/lib/schema.js +428 -0
  129. package/node_modules/@llblab/pi-actors/dist/lib/session-evidence.d.ts +45 -0
  130. package/node_modules/@llblab/pi-actors/dist/lib/session-evidence.js +204 -0
  131. package/node_modules/@llblab/pi-actors/dist/lib/state-readers.d.ts +21 -0
  132. package/node_modules/@llblab/pi-actors/dist/lib/state-readers.js +74 -0
  133. package/node_modules/@llblab/pi-actors/dist/lib/temp.d.ts +10 -0
  134. package/node_modules/@llblab/pi-actors/dist/lib/temp.js +90 -0
  135. package/node_modules/@llblab/pi-actors/dist/lib/tool-review-lineage-transaction.d.ts +27 -0
  136. package/node_modules/@llblab/pi-actors/dist/lib/tool-review-lineage-transaction.js +597 -0
  137. package/node_modules/@llblab/pi-actors/dist/lib/tool-review-lineage.d.ts +24 -0
  138. package/node_modules/@llblab/pi-actors/dist/lib/tool-review-lineage.js +98 -0
  139. package/node_modules/@llblab/pi-actors/dist/lib/tool-review-scheduler.d.ts +80 -0
  140. package/node_modules/@llblab/pi-actors/dist/lib/tool-review-scheduler.js +494 -0
  141. package/node_modules/@llblab/pi-actors/dist/lib/tool-review-transaction.d.ts +50 -0
  142. package/node_modules/@llblab/pi-actors/dist/lib/tool-review-transaction.js +362 -0
  143. package/node_modules/@llblab/pi-actors/dist/lib/tool-review.d.ts +56 -0
  144. package/node_modules/@llblab/pi-actors/dist/lib/tool-review.js +197 -0
  145. package/node_modules/@llblab/pi-actors/dist/lib/tools-access.d.ts +19 -0
  146. package/node_modules/@llblab/pi-actors/dist/lib/tools-access.js +44 -0
  147. package/node_modules/@llblab/pi-actors/dist/lib/tools-inspect.d.ts +15 -0
  148. package/node_modules/@llblab/pi-actors/dist/lib/tools-inspect.js +532 -0
  149. package/node_modules/@llblab/pi-actors/dist/lib/tools-local.d.ts +17 -0
  150. package/node_modules/@llblab/pi-actors/dist/lib/tools-local.js +191 -0
  151. package/node_modules/@llblab/pi-actors/dist/lib/tools-message.d.ts +9 -0
  152. package/node_modules/@llblab/pi-actors/dist/lib/tools-message.js +139 -0
  153. package/node_modules/@llblab/pi-actors/dist/lib/tools-register.d.ts +18 -0
  154. package/node_modules/@llblab/pi-actors/dist/lib/tools-register.js +41 -0
  155. package/node_modules/@llblab/pi-actors/dist/lib/tools-response.d.ts +20 -0
  156. package/node_modules/@llblab/pi-actors/dist/lib/tools-response.js +243 -0
  157. package/node_modules/@llblab/pi-actors/dist/lib/tools-spawn.d.ts +15 -0
  158. package/node_modules/@llblab/pi-actors/dist/lib/tools-spawn.js +133 -0
  159. package/node_modules/@llblab/pi-actors/dist/lib/tools.d.ts +28 -0
  160. package/node_modules/@llblab/pi-actors/dist/lib/tools.js +49 -0
  161. package/node_modules/@llblab/pi-actors/dist/lib/trace-projection.d.ts +22 -0
  162. package/node_modules/@llblab/pi-actors/dist/lib/trace-projection.js +231 -0
  163. package/node_modules/@llblab/pi-actors/dist/pi-actors/index.js +1 -0
  164. package/node_modules/@llblab/pi-actors/dist/scripts/async-runner.mjs +555 -0
  165. package/node_modules/@llblab/pi-actors/dist/scripts/build-dist.mjs +60 -0
  166. package/node_modules/@llblab/pi-actors/dist/scripts/conformance.mjs +63 -0
  167. package/node_modules/@llblab/pi-actors/dist/skills/actors/SKILL.md +126 -0
  168. package/node_modules/@llblab/pi-actors/dist/skills/actors/recipes/command-validate.json +13 -0
  169. package/node_modules/@llblab/pi-actors/dist/skills/actors/recipes/jsonl-tail.json +10 -0
  170. package/node_modules/@llblab/pi-actors/dist/skills/actors/recipes/recipe-validate.json +11 -0
  171. package/node_modules/@llblab/pi-actors/dist/skills/actors/recipes/resource-locker-snapshot.json +11 -0
  172. package/node_modules/@llblab/pi-actors/dist/skills/actors/recipes/resource-locker.json +25 -0
  173. package/node_modules/@llblab/pi-actors/dist/skills/actors/recipes/run-ops-snapshot.json +15 -0
  174. package/node_modules/@llblab/pi-actors/dist/skills/actors/recipes/run-state-files.json +13 -0
  175. package/node_modules/@llblab/pi-actors/dist/skills/actors/recipes/run-summary.json +9 -0
  176. package/node_modules/@llblab/pi-actors/dist/skills/actors/references/diagnostics.md +44 -0
  177. package/node_modules/@llblab/pi-actors/dist/skills/actors/references/persistent-tools.md +76 -0
  178. package/node_modules/@llblab/pi-actors/dist/skills/actors/references/recipes.md +51 -0
  179. package/node_modules/@llblab/pi-actors/dist/skills/actors/references/runs.md +41 -0
  180. package/node_modules/@llblab/pi-actors/dist/skills/actors/scripts/resource-locker.mjs +425 -0
  181. package/node_modules/@llblab/pi-actors/dist/skills/actors/scripts/run-utils.mjs +164 -0
  182. package/node_modules/@llblab/pi-actors/dist/skills/actors/scripts/validate-recipe.mjs +370 -0
  183. package/node_modules/@llblab/pi-actors/dist/skills/artifacts/SKILL.md +33 -0
  184. package/node_modules/@llblab/pi-actors/dist/skills/artifacts/recipes/bundle.json +77 -0
  185. package/node_modules/@llblab/pi-actors/dist/skills/artifacts/recipes/file-write.json +14 -0
  186. package/node_modules/@llblab/pi-actors/dist/skills/artifacts/recipes/manifest.json +14 -0
  187. package/node_modules/@llblab/pi-actors/dist/skills/artifacts/recipes/report.json +39 -0
  188. package/node_modules/@llblab/pi-actors/dist/skills/artifacts/recipes/write.json +53 -0
  189. package/node_modules/@llblab/pi-actors/dist/skills/artifacts/scripts/artifact-utils.mjs +63 -0
  190. package/node_modules/@llblab/pi-actors/dist/skills/music-player/SKILL.md +57 -0
  191. package/node_modules/@llblab/pi-actors/dist/skills/music-player/genapps/music-player.mjs +360 -0
  192. package/node_modules/@llblab/pi-actors/dist/skills/music-player/recipes/playback.json +29 -0
  193. package/node_modules/@llblab/pi-actors/dist/skills/music-player/scripts/playback-client.mjs +143 -0
  194. package/node_modules/@llblab/pi-actors/dist/skills/music-player/scripts/playback.mjs +1446 -0
  195. package/node_modules/@llblab/pi-actors/dist/skills/project-work/SKILL.md +37 -0
  196. package/node_modules/@llblab/pi-actors/dist/skills/project-work/recipes/changelog-head.json +11 -0
  197. package/node_modules/@llblab/pi-actors/dist/skills/project-work/recipes/changelog-section.json +11 -0
  198. package/node_modules/@llblab/pi-actors/dist/skills/project-work/recipes/docs-maintenance.json +66 -0
  199. package/node_modules/@llblab/pi-actors/dist/skills/project-work/recipes/git-log.json +11 -0
  200. package/node_modules/@llblab/pi-actors/dist/skills/project-work/recipes/git-status.json +9 -0
  201. package/node_modules/@llblab/pi-actors/dist/skills/project-work/recipes/markdown-index.json +14 -0
  202. package/node_modules/@llblab/pi-actors/dist/skills/project-work/recipes/package-summary.json +9 -0
  203. package/node_modules/@llblab/pi-actors/dist/skills/project-work/recipes/release-readiness.json +115 -0
  204. package/node_modules/@llblab/pi-actors/dist/skills/project-work/recipes/release-summary.json +74 -0
  205. package/node_modules/@llblab/pi-actors/dist/skills/project-work/recipes/repo-health.json +75 -0
  206. package/node_modules/@llblab/pi-actors/dist/skills/project-work/recipes/run-ops.json +56 -0
  207. package/node_modules/@llblab/pi-actors/dist/skills/project-work/recipes/skill-summary.json +11 -0
  208. package/node_modules/@llblab/pi-actors/dist/skills/project-work/scripts/project-utils.mjs +89 -0
  209. package/node_modules/@llblab/pi-actors/dist/skills/recipe-memory/SKILL.md +36 -0
  210. package/node_modules/@llblab/pi-actors/dist/skills/recipe-memory/recipes/draft-review.json +14 -0
  211. package/node_modules/@llblab/pi-actors/dist/skills/recipe-memory/recipes/tool-review.json +14 -0
  212. package/node_modules/@llblab/pi-actors/dist/skills/swarm/SKILL.md +84 -0
  213. package/node_modules/@llblab/pi-actors/dist/skills/swarm/recipes/architect.json +82 -0
  214. package/node_modules/@llblab/pi-actors/dist/skills/swarm/recipes/checkpoint-continuation.json +51 -0
  215. package/node_modules/@llblab/pi-actors/dist/skills/swarm/recipes/development-tasking.json +67 -0
  216. package/node_modules/@llblab/pi-actors/dist/skills/swarm/recipes/lens-review.json +63 -0
  217. package/node_modules/@llblab/pi-actors/dist/skills/swarm/recipes/quorum-review.json +65 -0
  218. package/node_modules/@llblab/pi-actors/dist/skills/swarm/recipes/research-synthesis.json +81 -0
  219. package/node_modules/@llblab/pi-actors/dist/skills/swarm/recipes/review-readiness.json +59 -0
  220. package/node_modules/@llblab/pi-actors/dist/skills/swarm/recipes/subagent-artifact.json +21 -0
  221. package/node_modules/@llblab/pi-actors/dist/skills/swarm/recipes/subagent-checkpoint.json +21 -0
  222. package/node_modules/@llblab/pi-actors/dist/skills/swarm/recipes/subagent-conflict-report.json +19 -0
  223. package/node_modules/@llblab/pi-actors/dist/skills/swarm/recipes/subagent-contradiction-map.json +20 -0
  224. package/node_modules/@llblab/pi-actors/dist/skills/swarm/recipes/subagent-critic.json +22 -0
  225. package/node_modules/@llblab/pi-actors/dist/skills/swarm/recipes/subagent-evidence-map.json +20 -0
  226. package/node_modules/@llblab/pi-actors/dist/skills/swarm/recipes/subagent-followup.json +21 -0
  227. package/node_modules/@llblab/pi-actors/dist/skills/swarm/recipes/subagent-judge.json +22 -0
  228. package/node_modules/@llblab/pi-actors/dist/skills/swarm/recipes/subagent-merge.json +24 -0
  229. package/node_modules/@llblab/pi-actors/dist/skills/swarm/recipes/subagent-normalize.json +20 -0
  230. package/node_modules/@llblab/pi-actors/dist/skills/swarm/recipes/subagent-plan.json +20 -0
  231. package/node_modules/@llblab/pi-actors/dist/skills/swarm/recipes/subagent-preflight.json +18 -0
  232. package/node_modules/@llblab/pi-actors/dist/skills/swarm/recipes/subagent-prompt.json +16 -0
  233. package/node_modules/@llblab/pi-actors/dist/skills/swarm/recipes/subagent-prompts.json +39 -0
  234. package/node_modules/@llblab/pi-actors/dist/skills/swarm/recipes/subagent-quorum.json +31 -0
  235. package/node_modules/@llblab/pi-actors/dist/skills/swarm/recipes/subagent-review-coordinator.json +170 -0
  236. package/node_modules/@llblab/pi-actors/dist/skills/swarm/recipes/subagent-review.json +26 -0
  237. package/node_modules/@llblab/pi-actors/dist/skills/swarm/recipes/subagent-task-card.json +22 -0
  238. package/node_modules/@llblab/pi-actors/dist/skills/swarm/recipes/subagent-tools.json +15 -0
  239. package/node_modules/@llblab/pi-actors/dist/skills/swarm/recipes/subagent-verify.json +23 -0
  240. package/node_modules/@llblab/pi-actors/dist/skills/swarm/references/development-swarm.md +189 -0
  241. package/node_modules/@llblab/pi-actors/dist/skills/swarm/references/review-swarms.md +115 -0
  242. package/node_modules/@llblab/pi-actors/docs/README.md +20 -0
  243. package/node_modules/@llblab/pi-actors/docs/actor-inspector.md +50 -0
  244. package/node_modules/@llblab/pi-actors/docs/async-runs.md +141 -0
  245. package/node_modules/@llblab/pi-actors/docs/command-templates.md +321 -0
  246. package/node_modules/@llblab/pi-actors/docs/recipe-library.md +89 -0
  247. package/node_modules/@llblab/pi-actors/docs/releasing.md +28 -0
  248. package/node_modules/@llblab/pi-actors/docs/template-recipes.md +175 -0
  249. package/node_modules/@llblab/pi-actors/docs/tool-registry.md +90 -0
  250. package/node_modules/@llblab/pi-actors/fixtures/protocol/artifact-manifest.json +9 -0
  251. package/node_modules/@llblab/pi-actors/fixtures/protocol/control-endpoint.json +6 -0
  252. package/node_modules/@llblab/pi-actors/fixtures/protocol/control-record.json +9 -0
  253. package/node_modules/@llblab/pi-actors/fixtures/protocol/recipe-summary.json +8 -0
  254. package/node_modules/@llblab/pi-actors/fixtures/protocol/run-state.json +10 -0
  255. package/node_modules/@llblab/pi-actors/fixtures/protocol/trace-event.json +9 -0
  256. package/node_modules/@llblab/pi-actors/index.ts +30 -0
  257. package/node_modules/@llblab/pi-actors/lib/async-runs.ts +1245 -0
  258. package/node_modules/@llblab/pi-actors/lib/automatic-review-runtime.ts +148 -0
  259. package/node_modules/@llblab/pi-actors/lib/command-templates.ts +1116 -0
  260. package/node_modules/@llblab/pi-actors/lib/config.ts +261 -0
  261. package/node_modules/@llblab/pi-actors/lib/control-projection.ts +105 -0
  262. package/node_modules/@llblab/pi-actors/lib/control.ts +117 -0
  263. package/node_modules/@llblab/pi-actors/lib/draft-consolidation-transaction.ts +821 -0
  264. package/node_modules/@llblab/pi-actors/lib/draft-consolidation.ts +181 -0
  265. package/node_modules/@llblab/pi-actors/lib/draft-review.ts +325 -0
  266. package/node_modules/@llblab/pi-actors/lib/draft-sleep.ts +575 -0
  267. package/node_modules/@llblab/pi-actors/lib/execution-output.ts +123 -0
  268. package/node_modules/@llblab/pi-actors/lib/execution-sessions.ts +111 -0
  269. package/node_modules/@llblab/pi-actors/lib/execution.ts +1153 -0
  270. package/node_modules/@llblab/pi-actors/lib/extension-runtime.ts +181 -0
  271. package/node_modules/@llblab/pi-actors/lib/file-state.ts +306 -0
  272. package/node_modules/@llblab/pi-actors/lib/identity.ts +29 -0
  273. package/node_modules/@llblab/pi-actors/lib/inspector-actions.ts +79 -0
  274. package/node_modules/@llblab/pi-actors/lib/inspector-command.ts +54 -0
  275. package/node_modules/@llblab/pi-actors/lib/inspector-overlay.ts +901 -0
  276. package/node_modules/@llblab/pi-actors/lib/inspector.ts +204 -0
  277. package/node_modules/@llblab/pi-actors/lib/limits.ts +29 -0
  278. package/node_modules/@llblab/pi-actors/lib/model-context.ts +359 -0
  279. package/node_modules/@llblab/pi-actors/lib/observability.ts +1337 -0
  280. package/node_modules/@llblab/pi-actors/lib/paths.ts +111 -0
  281. package/node_modules/@llblab/pi-actors/lib/pi.ts +47 -0
  282. package/node_modules/@llblab/pi-actors/lib/preflight-diagnostics.ts +132 -0
  283. package/node_modules/@llblab/pi-actors/lib/prompts.ts +69 -0
  284. package/node_modules/@llblab/pi-actors/lib/recipe-control.ts +52 -0
  285. package/node_modules/@llblab/pi-actors/lib/recipes-context.ts +268 -0
  286. package/node_modules/@llblab/pi-actors/lib/recipes-discovery.ts +1060 -0
  287. package/node_modules/@llblab/pi-actors/lib/recipes-references.ts +1598 -0
  288. package/node_modules/@llblab/pi-actors/lib/recipes-usage.ts +638 -0
  289. package/node_modules/@llblab/pi-actors/lib/registry.ts +806 -0
  290. package/node_modules/@llblab/pi-actors/lib/review-control.ts +136 -0
  291. package/node_modules/@llblab/pi-actors/lib/review-diagnostics.ts +164 -0
  292. package/node_modules/@llblab/pi-actors/lib/review-projection.ts +196 -0
  293. package/node_modules/@llblab/pi-actors/lib/run-evidence-policy.ts +242 -0
  294. package/node_modules/@llblab/pi-actors/lib/run-ui-runtime.ts +155 -0
  295. package/node_modules/@llblab/pi-actors/lib/runs-artifacts.ts +96 -0
  296. package/node_modules/@llblab/pi-actors/lib/runs-control-delivery.ts +209 -0
  297. package/node_modules/@llblab/pi-actors/lib/runs-control.ts +148 -0
  298. package/node_modules/@llblab/pi-actors/lib/runs-controls.ts +282 -0
  299. package/node_modules/@llblab/pi-actors/lib/runs-identity.ts +14 -0
  300. package/node_modules/@llblab/pi-actors/lib/runs-index.ts +192 -0
  301. package/node_modules/@llblab/pi-actors/lib/runs-ownership.ts +136 -0
  302. package/node_modules/@llblab/pi-actors/lib/runs-parent-teardown.ts +257 -0
  303. package/node_modules/@llblab/pi-actors/lib/runs-process.ts +174 -0
  304. package/node_modules/@llblab/pi-actors/lib/runs-retention.ts +140 -0
  305. package/node_modules/@llblab/pi-actors/lib/runs-start.ts +113 -0
  306. package/node_modules/@llblab/pi-actors/lib/runs-status.ts +101 -0
  307. package/node_modules/@llblab/pi-actors/lib/runs-trace.ts +611 -0
  308. package/node_modules/@llblab/pi-actors/lib/runtime-identity.ts +39 -0
  309. package/node_modules/@llblab/pi-actors/lib/runtime-triage.ts +106 -0
  310. package/node_modules/@llblab/pi-actors/lib/runtime.ts +430 -0
  311. package/node_modules/@llblab/pi-actors/lib/schema.ts +525 -0
  312. package/node_modules/@llblab/pi-actors/lib/session-evidence.ts +302 -0
  313. package/node_modules/@llblab/pi-actors/lib/state-readers.ts +93 -0
  314. package/node_modules/@llblab/pi-actors/lib/temp.ts +99 -0
  315. package/node_modules/@llblab/pi-actors/lib/tool-review-lineage-transaction.ts +881 -0
  316. package/node_modules/@llblab/pi-actors/lib/tool-review-lineage.ts +145 -0
  317. package/node_modules/@llblab/pi-actors/lib/tool-review-scheduler.ts +635 -0
  318. package/node_modules/@llblab/pi-actors/lib/tool-review-transaction.ts +563 -0
  319. package/node_modules/@llblab/pi-actors/lib/tool-review.ts +270 -0
  320. package/node_modules/@llblab/pi-actors/lib/tools-access.ts +68 -0
  321. package/node_modules/@llblab/pi-actors/lib/tools-inspect.ts +673 -0
  322. package/node_modules/@llblab/pi-actors/lib/tools-local.ts +303 -0
  323. package/node_modules/@llblab/pi-actors/lib/tools-message.ts +176 -0
  324. package/node_modules/@llblab/pi-actors/lib/tools-register.ts +63 -0
  325. package/node_modules/@llblab/pi-actors/lib/tools-response.ts +285 -0
  326. package/node_modules/@llblab/pi-actors/lib/tools-spawn.ts +212 -0
  327. package/node_modules/@llblab/pi-actors/lib/tools.ts +88 -0
  328. package/node_modules/@llblab/pi-actors/lib/trace-projection.ts +306 -0
  329. package/node_modules/@llblab/pi-actors/package.json +76 -0
  330. package/node_modules/@llblab/pi-actors/scripts/async-runner.mjs +555 -0
  331. package/node_modules/@llblab/pi-actors/scripts/build-dist.mjs +60 -0
  332. package/node_modules/@llblab/pi-actors/scripts/conformance.mjs +63 -0
  333. package/node_modules/@llblab/pi-actors/skills/actors/SKILL.md +126 -0
  334. package/node_modules/@llblab/pi-actors/skills/actors/recipes/command-validate.json +13 -0
  335. package/node_modules/@llblab/pi-actors/skills/actors/recipes/jsonl-tail.json +10 -0
  336. package/node_modules/@llblab/pi-actors/skills/actors/recipes/recipe-validate.json +11 -0
  337. package/node_modules/@llblab/pi-actors/skills/actors/recipes/resource-locker-snapshot.json +11 -0
  338. package/node_modules/@llblab/pi-actors/skills/actors/recipes/resource-locker.json +25 -0
  339. package/node_modules/@llblab/pi-actors/skills/actors/recipes/run-ops-snapshot.json +15 -0
  340. package/node_modules/@llblab/pi-actors/skills/actors/recipes/run-state-files.json +13 -0
  341. package/node_modules/@llblab/pi-actors/skills/actors/recipes/run-summary.json +9 -0
  342. package/node_modules/@llblab/pi-actors/skills/actors/references/diagnostics.md +44 -0
  343. package/node_modules/@llblab/pi-actors/skills/actors/references/persistent-tools.md +76 -0
  344. package/node_modules/@llblab/pi-actors/skills/actors/references/recipes.md +51 -0
  345. package/node_modules/@llblab/pi-actors/skills/actors/references/runs.md +41 -0
  346. package/node_modules/@llblab/pi-actors/skills/actors/scripts/resource-locker.mjs +425 -0
  347. package/node_modules/@llblab/pi-actors/skills/actors/scripts/run-utils.mjs +164 -0
  348. package/node_modules/@llblab/pi-actors/skills/actors/scripts/validate-recipe.mjs +370 -0
  349. package/node_modules/@llblab/pi-actors/skills/artifacts/SKILL.md +33 -0
  350. package/node_modules/@llblab/pi-actors/skills/artifacts/recipes/bundle.json +77 -0
  351. package/node_modules/@llblab/pi-actors/skills/artifacts/recipes/file-write.json +14 -0
  352. package/node_modules/@llblab/pi-actors/skills/artifacts/recipes/manifest.json +14 -0
  353. package/node_modules/@llblab/pi-actors/skills/artifacts/recipes/report.json +39 -0
  354. package/node_modules/@llblab/pi-actors/skills/artifacts/recipes/write.json +53 -0
  355. package/node_modules/@llblab/pi-actors/skills/artifacts/scripts/artifact-utils.mjs +63 -0
  356. package/node_modules/@llblab/pi-actors/skills/music-player/SKILL.md +57 -0
  357. package/node_modules/@llblab/pi-actors/skills/music-player/genapps/music-player.mjs +360 -0
  358. package/node_modules/@llblab/pi-actors/skills/music-player/recipes/playback.json +29 -0
  359. package/node_modules/@llblab/pi-actors/skills/music-player/scripts/playback-client.mjs +143 -0
  360. package/node_modules/@llblab/pi-actors/skills/music-player/scripts/playback.mjs +1446 -0
  361. package/node_modules/@llblab/pi-actors/skills/project-work/SKILL.md +37 -0
  362. package/node_modules/@llblab/pi-actors/skills/project-work/recipes/changelog-head.json +11 -0
  363. package/node_modules/@llblab/pi-actors/skills/project-work/recipes/changelog-section.json +11 -0
  364. package/node_modules/@llblab/pi-actors/skills/project-work/recipes/docs-maintenance.json +66 -0
  365. package/node_modules/@llblab/pi-actors/skills/project-work/recipes/git-log.json +11 -0
  366. package/node_modules/@llblab/pi-actors/skills/project-work/recipes/git-status.json +9 -0
  367. package/node_modules/@llblab/pi-actors/skills/project-work/recipes/markdown-index.json +14 -0
  368. package/node_modules/@llblab/pi-actors/skills/project-work/recipes/package-summary.json +9 -0
  369. package/node_modules/@llblab/pi-actors/skills/project-work/recipes/release-readiness.json +115 -0
  370. package/node_modules/@llblab/pi-actors/skills/project-work/recipes/release-summary.json +74 -0
  371. package/node_modules/@llblab/pi-actors/skills/project-work/recipes/repo-health.json +75 -0
  372. package/node_modules/@llblab/pi-actors/skills/project-work/recipes/run-ops.json +56 -0
  373. package/node_modules/@llblab/pi-actors/skills/project-work/recipes/skill-summary.json +11 -0
  374. package/node_modules/@llblab/pi-actors/skills/project-work/scripts/project-utils.mjs +89 -0
  375. package/node_modules/@llblab/pi-actors/skills/recipe-memory/SKILL.md +36 -0
  376. package/node_modules/@llblab/pi-actors/skills/recipe-memory/recipes/draft-review.json +14 -0
  377. package/node_modules/@llblab/pi-actors/skills/recipe-memory/recipes/tool-review.json +14 -0
  378. package/node_modules/@llblab/pi-actors/skills/swarm/SKILL.md +84 -0
  379. package/node_modules/@llblab/pi-actors/skills/swarm/recipes/architect.json +82 -0
  380. package/node_modules/@llblab/pi-actors/skills/swarm/recipes/checkpoint-continuation.json +51 -0
  381. package/node_modules/@llblab/pi-actors/skills/swarm/recipes/development-tasking.json +67 -0
  382. package/node_modules/@llblab/pi-actors/skills/swarm/recipes/lens-review.json +63 -0
  383. package/node_modules/@llblab/pi-actors/skills/swarm/recipes/quorum-review.json +65 -0
  384. package/node_modules/@llblab/pi-actors/skills/swarm/recipes/research-synthesis.json +81 -0
  385. package/node_modules/@llblab/pi-actors/skills/swarm/recipes/review-readiness.json +59 -0
  386. package/node_modules/@llblab/pi-actors/skills/swarm/recipes/subagent-artifact.json +21 -0
  387. package/node_modules/@llblab/pi-actors/skills/swarm/recipes/subagent-checkpoint.json +21 -0
  388. package/node_modules/@llblab/pi-actors/skills/swarm/recipes/subagent-conflict-report.json +19 -0
  389. package/node_modules/@llblab/pi-actors/skills/swarm/recipes/subagent-contradiction-map.json +20 -0
  390. package/node_modules/@llblab/pi-actors/skills/swarm/recipes/subagent-critic.json +22 -0
  391. package/node_modules/@llblab/pi-actors/skills/swarm/recipes/subagent-evidence-map.json +20 -0
  392. package/node_modules/@llblab/pi-actors/skills/swarm/recipes/subagent-followup.json +21 -0
  393. package/node_modules/@llblab/pi-actors/skills/swarm/recipes/subagent-judge.json +22 -0
  394. package/node_modules/@llblab/pi-actors/skills/swarm/recipes/subagent-merge.json +24 -0
  395. package/node_modules/@llblab/pi-actors/skills/swarm/recipes/subagent-normalize.json +20 -0
  396. package/node_modules/@llblab/pi-actors/skills/swarm/recipes/subagent-plan.json +20 -0
  397. package/node_modules/@llblab/pi-actors/skills/swarm/recipes/subagent-preflight.json +18 -0
  398. package/node_modules/@llblab/pi-actors/skills/swarm/recipes/subagent-prompt.json +16 -0
  399. package/node_modules/@llblab/pi-actors/skills/swarm/recipes/subagent-prompts.json +39 -0
  400. package/node_modules/@llblab/pi-actors/skills/swarm/recipes/subagent-quorum.json +31 -0
  401. package/node_modules/@llblab/pi-actors/skills/swarm/recipes/subagent-review-coordinator.json +170 -0
  402. package/node_modules/@llblab/pi-actors/skills/swarm/recipes/subagent-review.json +26 -0
  403. package/node_modules/@llblab/pi-actors/skills/swarm/recipes/subagent-task-card.json +22 -0
  404. package/node_modules/@llblab/pi-actors/skills/swarm/recipes/subagent-tools.json +15 -0
  405. package/node_modules/@llblab/pi-actors/skills/swarm/recipes/subagent-verify.json +23 -0
  406. package/node_modules/@llblab/pi-actors/skills/swarm/references/development-swarm.md +189 -0
  407. package/node_modules/@llblab/pi-actors/skills/swarm/references/review-swarms.md +115 -0
  408. package/node_modules/@llblab/pi-codex-usage/AGENTS.md +17 -0
  409. package/node_modules/@llblab/pi-codex-usage/BACKLOG.md +8 -0
  410. package/node_modules/@llblab/pi-codex-usage/CHANGELOG.md +80 -0
  411. package/node_modules/@llblab/pi-codex-usage/LICENSE +22 -0
  412. package/node_modules/@llblab/pi-codex-usage/README.md +113 -0
  413. package/node_modules/@llblab/pi-codex-usage/banner.jpg +0 -0
  414. package/node_modules/@llblab/pi-codex-usage/index.ts +1525 -0
  415. package/node_modules/@llblab/pi-codex-usage/package.json +59 -0
  416. package/node_modules/@llblab/pi-grow-loop/AGENTS.md +56 -0
  417. package/node_modules/@llblab/pi-grow-loop/BACKLOG.md +3 -0
  418. package/node_modules/@llblab/pi-grow-loop/CHANGELOG.md +126 -0
  419. package/node_modules/@llblab/pi-grow-loop/README.md +216 -0
  420. package/node_modules/@llblab/pi-grow-loop/banner.jpg +0 -0
  421. package/node_modules/@llblab/pi-grow-loop/index.ts +219 -0
  422. package/node_modules/@llblab/pi-grow-loop/package.json +64 -0
  423. package/node_modules/@llblab/pi-grow-loop/skills/grow-loop/SKILL.md +131 -0
  424. package/node_modules/@llblab/pi-grow-loop/skills/while-true/SKILL.md +233 -0
  425. package/node_modules/@llblab/pi-telegram/AGENTS.md +157 -0
  426. package/node_modules/@llblab/pi-telegram/BACKLOG.md +15 -0
  427. package/node_modules/@llblab/pi-telegram/CHANGELOG.md +886 -0
  428. package/node_modules/@llblab/pi-telegram/README.md +330 -0
  429. package/node_modules/@llblab/pi-telegram/api/activity.ts +15 -0
  430. package/node_modules/@llblab/pi-telegram/api/commands.ts +11 -0
  431. package/node_modules/@llblab/pi-telegram/api/delivery.ts +21 -0
  432. package/node_modules/@llblab/pi-telegram/api/inbound.ts +14 -0
  433. package/node_modules/@llblab/pi-telegram/api/keyboard.ts +11 -0
  434. package/node_modules/@llblab/pi-telegram/api/outbound.ts +11 -0
  435. package/node_modules/@llblab/pi-telegram/api/sections.ts +17 -0
  436. package/node_modules/@llblab/pi-telegram/api/status.ts +12 -0
  437. package/node_modules/@llblab/pi-telegram/api/updates.ts +16 -0
  438. package/node_modules/@llblab/pi-telegram/api/voice.ts +24 -0
  439. package/node_modules/@llblab/pi-telegram/docs/README.md +25 -0
  440. package/node_modules/@llblab/pi-telegram/docs/activity.md +304 -0
  441. package/node_modules/@llblab/pi-telegram/docs/architecture.md +418 -0
  442. package/node_modules/@llblab/pi-telegram/docs/callback-namespaces.md +48 -0
  443. package/node_modules/@llblab/pi-telegram/docs/command-templates.md +365 -0
  444. package/node_modules/@llblab/pi-telegram/docs/compact-matrix-literal.md +176 -0
  445. package/node_modules/@llblab/pi-telegram/docs/delivery.md +225 -0
  446. package/node_modules/@llblab/pi-telegram/docs/generative-apps.md +310 -0
  447. package/node_modules/@llblab/pi-telegram/docs/inbound.md +136 -0
  448. package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +467 -0
  449. package/node_modules/@llblab/pi-telegram/docs/outbound.md +193 -0
  450. package/node_modules/@llblab/pi-telegram/docs/public-api.md +599 -0
  451. package/node_modules/@llblab/pi-telegram/docs/sections.md +452 -0
  452. package/node_modules/@llblab/pi-telegram/docs/ui-style.md +256 -0
  453. package/node_modules/@llblab/pi-telegram/docs/updates.md +171 -0
  454. package/node_modules/@llblab/pi-telegram/docs/voice.md +217 -0
  455. package/node_modules/@llblab/pi-telegram/index.ts +1336 -0
  456. package/node_modules/@llblab/pi-telegram/lib/activity-verbosity.ts +714 -0
  457. package/node_modules/@llblab/pi-telegram/lib/activity.ts +782 -0
  458. package/node_modules/@llblab/pi-telegram/lib/agent-messages.ts +121 -0
  459. package/node_modules/@llblab/pi-telegram/lib/bindings.ts +1129 -0
  460. package/node_modules/@llblab/pi-telegram/lib/bus-api.ts +379 -0
  461. package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +1941 -0
  462. package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +2119 -0
  463. package/node_modules/@llblab/pi-telegram/lib/bus-transport.ts +244 -0
  464. package/node_modules/@llblab/pi-telegram/lib/bus.ts +2645 -0
  465. package/node_modules/@llblab/pi-telegram/lib/command-templates.ts +954 -0
  466. package/node_modules/@llblab/pi-telegram/lib/commands.ts +1854 -0
  467. package/node_modules/@llblab/pi-telegram/lib/config.ts +1143 -0
  468. package/node_modules/@llblab/pi-telegram/lib/delivery.ts +891 -0
  469. package/node_modules/@llblab/pi-telegram/lib/generative-app-worker.mjs +104 -0
  470. package/node_modules/@llblab/pi-telegram/lib/generative-apps.ts +958 -0
  471. package/node_modules/@llblab/pi-telegram/lib/inbound.ts +824 -0
  472. package/node_modules/@llblab/pi-telegram/lib/journal.ts +3140 -0
  473. package/node_modules/@llblab/pi-telegram/lib/keyboard.ts +59 -0
  474. package/node_modules/@llblab/pi-telegram/lib/lifecycle.ts +623 -0
  475. package/node_modules/@llblab/pi-telegram/lib/locks.ts +1422 -0
  476. package/node_modules/@llblab/pi-telegram/lib/logs.ts +393 -0
  477. package/node_modules/@llblab/pi-telegram/lib/media.ts +921 -0
  478. package/node_modules/@llblab/pi-telegram/lib/menu-model.ts +1182 -0
  479. package/node_modules/@llblab/pi-telegram/lib/menu-queue.ts +926 -0
  480. package/node_modules/@llblab/pi-telegram/lib/menu-settings.ts +876 -0
  481. package/node_modules/@llblab/pi-telegram/lib/menu-status.ts +274 -0
  482. package/node_modules/@llblab/pi-telegram/lib/menu-thinking.ts +185 -0
  483. package/node_modules/@llblab/pi-telegram/lib/menu.ts +867 -0
  484. package/node_modules/@llblab/pi-telegram/lib/model.ts +672 -0
  485. package/node_modules/@llblab/pi-telegram/lib/outbound-attachments.ts +981 -0
  486. package/node_modules/@llblab/pi-telegram/lib/outbound-buttons.ts +331 -0
  487. package/node_modules/@llblab/pi-telegram/lib/outbound-markup.ts +515 -0
  488. package/node_modules/@llblab/pi-telegram/lib/outbound-voice.ts +285 -0
  489. package/node_modules/@llblab/pi-telegram/lib/outbound.ts +1007 -0
  490. package/node_modules/@llblab/pi-telegram/lib/ownership.ts +298 -0
  491. package/node_modules/@llblab/pi-telegram/lib/paths.ts +127 -0
  492. package/node_modules/@llblab/pi-telegram/lib/pi.ts +197 -0
  493. package/node_modules/@llblab/pi-telegram/lib/polling.ts +1588 -0
  494. package/node_modules/@llblab/pi-telegram/lib/preview.ts +846 -0
  495. package/node_modules/@llblab/pi-telegram/lib/prompt-templates.ts +158 -0
  496. package/node_modules/@llblab/pi-telegram/lib/prompts.ts +218 -0
  497. package/node_modules/@llblab/pi-telegram/lib/queue.ts +3075 -0
  498. package/node_modules/@llblab/pi-telegram/lib/recovery.ts +464 -0
  499. package/node_modules/@llblab/pi-telegram/lib/rendering.ts +1213 -0
  500. package/node_modules/@llblab/pi-telegram/lib/replies.ts +892 -0
  501. package/node_modules/@llblab/pi-telegram/lib/routing.ts +2889 -0
  502. package/node_modules/@llblab/pi-telegram/lib/runtime.ts +628 -0
  503. package/node_modules/@llblab/pi-telegram/lib/sections.ts +710 -0
  504. package/node_modules/@llblab/pi-telegram/lib/setup.ts +220 -0
  505. package/node_modules/@llblab/pi-telegram/lib/skills.ts +21 -0
  506. package/node_modules/@llblab/pi-telegram/lib/status.ts +1550 -0
  507. package/node_modules/@llblab/pi-telegram/lib/sync.ts +883 -0
  508. package/node_modules/@llblab/pi-telegram/lib/target.ts +49 -0
  509. package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +1747 -0
  510. package/node_modules/@llblab/pi-telegram/lib/text-groups.ts +487 -0
  511. package/node_modules/@llblab/pi-telegram/lib/thread-reconciler.ts +1002 -0
  512. package/node_modules/@llblab/pi-telegram/lib/threads.ts +3183 -0
  513. package/node_modules/@llblab/pi-telegram/lib/time-injection.ts +78 -0
  514. package/node_modules/@llblab/pi-telegram/lib/turns.ts +753 -0
  515. package/node_modules/@llblab/pi-telegram/lib/updates.ts +4950 -0
  516. package/node_modules/@llblab/pi-telegram/lib/voice.ts +340 -0
  517. package/node_modules/@llblab/pi-telegram/package.json +83 -0
  518. package/node_modules/@llblab/pi-telegram/screenshot.png +0 -0
  519. package/node_modules/@llblab/pi-telegram/scripts/check-downgrade.mjs +435 -0
  520. package/node_modules/@llblab/pi-telegram/skills/generated-control-surface/SKILL.md +241 -0
  521. package/node_modules/@llblab/pi-telegram/skills/generative-apps/SKILL.md +114 -0
  522. package/node_modules/@llblab/pi-telegram/skills/telegram-bridge/SKILL.md +149 -0
  523. package/package.json +67 -0
@@ -0,0 +1,2889 @@
1
+ /**
2
+ * Telegram inbound routing composition
3
+ * Zones: telegram inbound, orchestration, queue/menu/command composition
4
+ * Wires authorized updates into menus, commands, media grouping, and prompt queueing, and owns exact assistant-output target/route authority capture
5
+ */
6
+
7
+ import { readFile } from "node:fs/promises";
8
+ import { basename, dirname } from "node:path";
9
+ import * as Bus from "./bus.ts";
10
+ import * as Commands from "./commands.ts";
11
+ import type { TelegramConfigStore } from "./config.ts";
12
+ import type { TelegramInboundHandlerRuntime } from "./inbound.ts";
13
+ import * as Media from "./media.ts";
14
+ import * as Menu from "./menu.ts";
15
+ import * as Model from "./model.ts";
16
+ import * as OutboundHandlers from "./outbound.ts";
17
+ import * as PromptTemplates from "./prompt-templates.ts";
18
+ import * as Queue from "./queue.ts";
19
+ import type { TelegramBridgeRuntime } from "./runtime.ts";
20
+ import type { TelegramSectionRegistry } from "./sections.ts";
21
+ import * as TextGroups from "./text-groups.ts";
22
+ import * as ThreadReconciler from "./thread-reconciler.ts";
23
+ import type {
24
+ TelegramInstanceThreadIdentityCandidate,
25
+ TelegramTopicTargetRecord,
26
+ } from "./threads.ts";
27
+ import * as Turns from "./turns.ts";
28
+
29
+ interface TelegramPromptPeerView {
30
+ id?: unknown;
31
+ is_bot?: unknown;
32
+ username?: unknown;
33
+ first_name?: unknown;
34
+ last_name?: unknown;
35
+ title?: unknown;
36
+ }
37
+
38
+ function formatTelegramPromptPeer(
39
+ peer: TelegramPromptPeerView | undefined,
40
+ ): string | undefined {
41
+ if (!peer) return undefined;
42
+ if (typeof peer.username === "string" && peer.username.length > 0) {
43
+ return peer.username;
44
+ }
45
+ const displayName = [peer.first_name, peer.last_name]
46
+ .filter(
47
+ (part): part is string => typeof part === "string" && part.length > 0,
48
+ )
49
+ .join(" ");
50
+ if (displayName) return displayName;
51
+ if (typeof peer.title === "string" && peer.title.length > 0) {
52
+ return peer.title;
53
+ }
54
+ return typeof peer.id === "number" ? String(peer.id) : undefined;
55
+ }
56
+
57
+ function isTelegramPromptOwnerPeer(
58
+ peer: TelegramPromptPeerView | undefined,
59
+ ownerUserId: number | undefined,
60
+ ): boolean {
61
+ return ownerUserId !== undefined && peer?.id === ownerUserId;
62
+ }
63
+
64
+ function isTelegramPromptBotPeer(
65
+ peer: TelegramPromptPeerView | undefined,
66
+ ): boolean {
67
+ return peer?.is_bot === true;
68
+ }
69
+
70
+ export function resolveTelegramGuestPromptPeer(input: {
71
+ chatType?: string;
72
+ chat?: TelegramPromptPeerView;
73
+ from?: TelegramPromptPeerView;
74
+ replyFrom?: TelegramPromptPeerView;
75
+ guestBotCallerUser?: TelegramPromptPeerView;
76
+ guestBotCallerChat?: TelegramPromptPeerView;
77
+ ownerUserId?: number;
78
+ }): string | undefined {
79
+ if (input.chatType !== "private") {
80
+ return formatTelegramPromptPeer(input.chat);
81
+ }
82
+ if (
83
+ !isTelegramPromptOwnerPeer(input.from, input.ownerUserId) &&
84
+ !isTelegramPromptBotPeer(input.from)
85
+ ) {
86
+ return formatTelegramPromptPeer(input.from);
87
+ }
88
+ for (const candidate of [
89
+ input.chat,
90
+ input.guestBotCallerUser,
91
+ input.guestBotCallerChat,
92
+ input.replyFrom,
93
+ ]) {
94
+ if (
95
+ isTelegramPromptOwnerPeer(candidate, input.ownerUserId) ||
96
+ isTelegramPromptBotPeer(candidate)
97
+ ) {
98
+ continue;
99
+ }
100
+ const peer = formatTelegramPromptPeer(candidate);
101
+ if (peer) return peer;
102
+ }
103
+ return undefined;
104
+ }
105
+
106
+ function appendTelegramSourceAttachmentSection(
107
+ text: string,
108
+ from: string | undefined,
109
+ files: Pick<Media.DownloadedTelegramFile, "path">[],
110
+ ): string {
111
+ if (files.length === 0) return text;
112
+ const dirs = [...new Set(files.map((file) => dirname(file.path)))];
113
+ const sameDir = dirs.length === 1;
114
+ const source = from ? `|from:${from}` : "";
115
+ const header = sameDir
116
+ ? `[attachments${source}] ${dirs[0]}`
117
+ : `[attachments${source}]`;
118
+ const items = sameDir
119
+ ? files.map((file) => `/${basename(file.path)}`)
120
+ : files.map((file) => file.path);
121
+ const prefix = text.length > 0 ? `${text}\n\n` : "";
122
+ return `${prefix}${header}\n${items.map((item) => `- ${item}`).join("\n")}`;
123
+ }
124
+
125
+ function getContextCwd(ctx: unknown): string | undefined {
126
+ if (!ctx || typeof ctx !== "object") return undefined;
127
+ const cwd = (ctx as { cwd?: unknown }).cwd;
128
+ return typeof cwd === "string" && cwd.length > 0 ? cwd : undefined;
129
+ }
130
+
131
+ function getLeaderTopicProfileKey(
132
+ ctx: unknown,
133
+ instanceId: string | undefined,
134
+ ): string | undefined {
135
+ const cwd = getContextCwd(ctx);
136
+ if (cwd) return `cwd:${cwd}`;
137
+ return instanceId ? `leader:${instanceId}` : undefined;
138
+ }
139
+
140
+ function isCurrentLeaderTopicRecord(
141
+ record: Threads.TelegramTopicTargetRecord,
142
+ profileKey: string | undefined,
143
+ instanceId: string | undefined,
144
+ ): boolean {
145
+ if (instanceId && record.instanceId === instanceId) return true;
146
+ return !!profileKey && record.profileKey === profileKey;
147
+ }
148
+
149
+ function hasActiveLeaderTopic(
150
+ records: Threads.TelegramTopicTargetRecord[],
151
+ profileKey: string | undefined,
152
+ instanceId: string | undefined,
153
+ ): boolean {
154
+ return records.some((record) => {
155
+ if (record.status !== "active") return false;
156
+ return isCurrentLeaderTopicRecord(record, profileKey, instanceId);
157
+ });
158
+ }
159
+
160
+ function escapeHtml(text: string): string {
161
+ return text
162
+ .replace(/&/g, "&amp;")
163
+ .replace(/</g, "&lt;")
164
+ .replace(/>/g, "&gt;");
165
+ }
166
+
167
+ const TELEGRAM_UNBOUND_REROUTE_CALLBACK_PREFIX = "reroute:";
168
+ const TELEGRAM_UNBOUND_REROUTE_RESTORE_MENU_CALLBACK_PREFIX = "rerouterestore:";
169
+ const TELEGRAM_UNBOUND_REROUTE_NEW_SLOT_CALLBACK_PREFIX = "reroutenew:";
170
+
171
+ function formatTelegramUnboundRerouteCallbackData(
172
+ rerouteId: string,
173
+ threadId: number,
174
+ ): string {
175
+ return `${TELEGRAM_UNBOUND_REROUTE_CALLBACK_PREFIX}${rerouteId}:${threadId}`;
176
+ }
177
+
178
+ function formatTelegramUnboundRerouteRestoreMenuCallbackData(
179
+ rerouteId: string,
180
+ ): string {
181
+ return `${TELEGRAM_UNBOUND_REROUTE_RESTORE_MENU_CALLBACK_PREFIX}${rerouteId}`;
182
+ }
183
+
184
+ function formatTelegramUnboundRerouteNewSlotCallbackData(
185
+ rerouteId: string,
186
+ threadId: number,
187
+ ): string {
188
+ return `${TELEGRAM_UNBOUND_REROUTE_NEW_SLOT_CALLBACK_PREFIX}${rerouteId}:${threadId}`;
189
+ }
190
+
191
+ function parseTelegramUnboundRerouteRestoreMenuCallbackData(
192
+ data: string | undefined,
193
+ ): { rerouteId: string } | undefined {
194
+ const match = data?.match(/^rerouterestore:([a-z0-9]+)$/);
195
+ const rerouteId = match?.[1];
196
+ return rerouteId ? { rerouteId } : undefined;
197
+ }
198
+
199
+ function parseTelegramUnboundRerouteCallbackData(
200
+ data: string | undefined,
201
+ ): { rerouteId: string; threadId: number; useNewSlot: boolean } | undefined {
202
+ const match = data?.match(/^(reroute|reroutenew):([a-z0-9]+):(\d+)$/);
203
+ const prefix = match?.[1];
204
+ const rerouteId = match?.[2];
205
+ const threadId = Number(match?.[3]);
206
+ if (!prefix || !rerouteId || !Number.isSafeInteger(threadId))
207
+ return undefined;
208
+ return { rerouteId, threadId, useNewSlot: prefix === "reroutenew" };
209
+ }
210
+
211
+ function getTelegramThreadRecordLabel(
212
+ record: Threads.TelegramTopicTargetRecord,
213
+ ): string {
214
+ return getRestoredThreadName(record, record.slot ?? "");
215
+ }
216
+
217
+ function getTelegramRouteThreadButtonLabel(
218
+ record: Threads.TelegramTopicTargetRecord,
219
+ ): string {
220
+ return `↪️ ${getTelegramThreadRecordLabel(record)}`;
221
+ }
222
+
223
+ function getTelegramReplaceThreadButtonLabel(
224
+ record: Threads.TelegramTopicTargetRecord,
225
+ ): string {
226
+ return `➡️ ${getTelegramThreadRecordLabel(record)}`;
227
+ }
228
+
229
+ function getNextTelegramSlotPreference(
230
+ slot: string | undefined,
231
+ ): string | undefined {
232
+ if (!slot || !/^[A-Z]$/.test(slot)) return undefined;
233
+ const index = slot.charCodeAt(0) - "A".charCodeAt(0);
234
+ return String.fromCharCode("A".charCodeAt(0) + ((index + 1) % 26));
235
+ }
236
+
237
+ function getRestoredThreadName(
238
+ record: Threads.TelegramTopicTargetRecord,
239
+ slot: string,
240
+ ): string {
241
+ return record.threadName &&
242
+ Threads.isTelegramTopicThreadNameValidForSlot(record.threadName, slot)
243
+ ? record.threadName
244
+ : (Threads.chooseTelegramThreadName({ slot }) ?? "Pi");
245
+ }
246
+
247
+ function isTelegramLiveThreadTarget(
248
+ record: Threads.TelegramTopicTargetRecord,
249
+ liveTargets: readonly Queue.TelegramQueueTarget[] | undefined,
250
+ ): boolean {
251
+ if (!liveTargets) return record.status === "active";
252
+ return liveTargets.some(
253
+ (target) =>
254
+ target.chatId === record.target.chatId &&
255
+ target.threadId === record.target.threadId,
256
+ );
257
+ }
258
+
259
+ function getTelegramRoutableThreadRecords(
260
+ records: readonly Threads.TelegramTopicTargetRecord[],
261
+ liveTargets: readonly Queue.TelegramQueueTarget[] | undefined,
262
+ ): Threads.TelegramTopicTargetRecord[] {
263
+ return records.filter(
264
+ (record) =>
265
+ record.status === "active" &&
266
+ isTelegramLiveThreadTarget(record, liveTargets),
267
+ );
268
+ }
269
+
270
+ function formatTelegramAllTabMenuChooserText(command: string): string {
271
+ return [
272
+ "<b>🧵 Choose target thread</b>",
273
+ "",
274
+ `You used <code>/${escapeHtml(command)}</code> from the <b>All</b> tab.`,
275
+ "Select the Pi thread that should handle it:",
276
+ ].join("\n");
277
+ }
278
+
279
+ function buildTelegramUnboundRerouteChooserMarkup(
280
+ rerouteId: string,
281
+ records: readonly Threads.TelegramTopicTargetRecord[],
282
+ _options: {
283
+ currentLeaderProfileKey?: string;
284
+ currentInstanceId?: string;
285
+ } = {},
286
+ ): Menu.TelegramReplyMarkup {
287
+ const activeRecords = records.filter((record) => record.status === "active");
288
+ const canRestoreAnyLiveThread = activeRecords.length > 0;
289
+ const rows = activeRecords.map((record) => [
290
+ {
291
+ text: getTelegramRouteThreadButtonLabel(record),
292
+ callback_data: formatTelegramUnboundRerouteCallbackData(
293
+ rerouteId,
294
+ record.target.threadId,
295
+ ),
296
+ },
297
+ ]);
298
+ return {
299
+ inline_keyboard: canRestoreAnyLiveThread
300
+ ? [
301
+ ...rows,
302
+ [
303
+ {
304
+ text: "🔁 Replace/restore thread…",
305
+ callback_data:
306
+ formatTelegramUnboundRerouteRestoreMenuCallbackData(rerouteId),
307
+ },
308
+ ],
309
+ ]
310
+ : rows,
311
+ };
312
+ }
313
+
314
+ function buildTelegramUnboundRerouteRestoreChooserMarkup(
315
+ rerouteId: string,
316
+ records: readonly Threads.TelegramTopicTargetRecord[],
317
+ ): Menu.TelegramReplyMarkup {
318
+ return {
319
+ inline_keyboard: records
320
+ .filter((record) => record.status === "active")
321
+ .map((record) => [
322
+ {
323
+ text: getTelegramReplaceThreadButtonLabel(record),
324
+ callback_data: formatTelegramUnboundRerouteNewSlotCallbackData(
325
+ rerouteId,
326
+ record.target.threadId,
327
+ ),
328
+ },
329
+ ]),
330
+ };
331
+ }
332
+
333
+ function formatTelegramUnboundRerouteRestoreChooserText(): string {
334
+ return [
335
+ "<b>🧵 Replace/restore Telegram thread</b>",
336
+ "",
337
+ "Choose the Pi instance to move to this new Telegram thread:",
338
+ ].join("\n");
339
+ }
340
+
341
+ function formatTelegramUnboundTopicGuidance(): string {
342
+ return [
343
+ "⚠️ <b>New thread is not a Pi instance</b>",
344
+ "",
345
+ "To create a bound Telegram tab:",
346
+ "<code>1.</code> Start another Pi instance in your terminal.",
347
+ "<code>2.</code> Run <code>/telegram-connect</code> in that instance.",
348
+ "<code>3.</code> The bridge will create and bind a fresh Telegram tab for it.",
349
+ ].join("\n");
350
+ }
351
+
352
+ function formatTelegramTargetKey(target: Queue.TelegramQueueTarget): string {
353
+ return `${target.chatId}:${target.threadId ?? "all"}`;
354
+ }
355
+
356
+ function formatTelegramUnboundRerouteChooserText(
357
+ _records: readonly Threads.TelegramTopicTargetRecord[],
358
+ options: { includeGuidance?: boolean } = {},
359
+ ): string {
360
+ const rerouteText = [
361
+ "🧵 <b>Choose target thread</b>",
362
+ "",
363
+ "Your message is still in this Telegram thread.",
364
+ "Select the Pi thread that should handle it:",
365
+ ].join("\n");
366
+ return options.includeGuidance === false
367
+ ? rerouteText
368
+ : [formatTelegramUnboundTopicGuidance(), "", rerouteText].join("\n");
369
+ }
370
+
371
+ import * as Threads from "./threads.ts";
372
+ import type { TelegramUser } from "./updates.ts";
373
+ import * as Updates from "./updates.ts";
374
+ import { getTelegramVoiceReplyMode } from "./voice.ts";
375
+
376
+ async function deleteReservedTelegramTopicThroughReconciler(
377
+ deps: {
378
+ callApi?: <TResponse>(
379
+ method: string,
380
+ body: Record<string, unknown>,
381
+ ) => Promise<TResponse>;
382
+ threadStore?: Pick<
383
+ Threads.TelegramTopicTargetStore,
384
+ | "list"
385
+ | "listReservations"
386
+ | "listSyncObservations"
387
+ | "markStaleByTarget"
388
+ | "persist"
389
+ >;
390
+ getCurrentLeaderEpoch?: () => number | string | undefined;
391
+ getThreadReconciliationMachineState?: () =>
392
+ ThreadReconciler.ThreadReconciliationMachineState | undefined;
393
+ recordThreadReconciliationPlan?: (
394
+ plan: ThreadReconciler.ThreadReconciliationPlan,
395
+ ) => void;
396
+ recordRuntimeEvent?: (
397
+ category: string,
398
+ error: unknown,
399
+ details?: Record<string, unknown>,
400
+ ) => void;
401
+ },
402
+ target: { chatId: number; threadId: number },
403
+ messageId: number,
404
+ ): Promise<boolean> {
405
+ if (!deps.threadStore) return false;
406
+ const nowMs = Date.now();
407
+ const currentLeaderEpoch = deps.getCurrentLeaderEpoch?.();
408
+ const plan = ThreadReconciler.planThreadReconciliation({
409
+ nowMs,
410
+ currentLeaderEpoch,
411
+ previousState: deps.getThreadReconciliationMachineState?.(),
412
+ records: deps.threadStore.list(),
413
+ reservations: deps.threadStore.listReservations(),
414
+ observations: deps.threadStore.listSyncObservations(),
415
+ reservedMessages: [
416
+ {
417
+ target,
418
+ observedAtMs: nowMs,
419
+ messageId,
420
+ ...(currentLeaderEpoch !== undefined
421
+ ? { leaderEpoch: currentLeaderEpoch }
422
+ : {}),
423
+ },
424
+ ],
425
+ });
426
+ deps.recordThreadReconciliationPlan?.(plan);
427
+ await ThreadReconciler.applyThreadReconciliationPlan(plan, {
428
+ callApi: deps.callApi,
429
+ markStaleByTarget: (staleTarget, syncStatus, lastSyncError) =>
430
+ deps.threadStore?.markStaleByTarget(
431
+ staleTarget,
432
+ syncStatus,
433
+ lastSyncError,
434
+ ) ?? false,
435
+ persist: () => deps.threadStore?.persist() ?? Promise.resolve(),
436
+ getCurrentLeaderEpoch: deps.getCurrentLeaderEpoch,
437
+ recordRuntimeEvent: deps.recordRuntimeEvent,
438
+ });
439
+ return plan.actions.some(
440
+ (action) => action.kind === "close-delete-reserved-topic",
441
+ );
442
+ }
443
+
444
+ export type TelegramRoutedMessage = Updates.TelegramUpdateMessage &
445
+ Media.TelegramMediaMessage &
446
+ Media.TelegramMediaGroupMessage &
447
+ Commands.TelegramCommandRuntimeMessage &
448
+ Turns.TelegramTurnMessage;
449
+
450
+ export type TelegramRoutedCallbackQuery = Updates.TelegramCallbackQuery &
451
+ Menu.MenuCallbackQuery;
452
+
453
+ export interface TelegramInboundBusProjectionRuntime {
454
+ getTargetOwnership: Updates.TelegramTargetOwnershipLookup;
455
+ getLiveThreadTargets(): Queue.TelegramQueueTarget[];
456
+ getLocalThreadLabelForTarget(
457
+ target: Queue.TelegramQueueTarget,
458
+ ): string | undefined;
459
+ }
460
+
461
+ export function createTelegramInboundBusProjectionRuntime(deps: {
462
+ instanceId: string;
463
+ listFollowers(): readonly Bus.TelegramBusFollowerView[];
464
+ listThreadRecords(): readonly TelegramTopicTargetRecord[];
465
+ getLeaderTarget(): Queue.TelegramQueueTarget | undefined;
466
+ isFollowerRegistered(): boolean;
467
+ getFollowerTarget(): Queue.TelegramQueueTarget | undefined;
468
+ getCurrentIdentity(
469
+ target?: Queue.TelegramQueueTarget,
470
+ ): TelegramInstanceThreadIdentityCandidate;
471
+ }): TelegramInboundBusProjectionRuntime {
472
+ return {
473
+ getTargetOwnership(target) {
474
+ return Bus.getTelegramFollowerTargetOwnership({
475
+ target,
476
+ followers: deps.listFollowers(),
477
+ activeThreadRecords: deps.listThreadRecords(),
478
+ currentInstanceId: deps.instanceId,
479
+ });
480
+ },
481
+ getLiveThreadTargets() {
482
+ return Bus.listTelegramBusLiveThreadTargets({
483
+ leaderTarget: deps.getLeaderTarget(),
484
+ followers: deps.listFollowers(),
485
+ });
486
+ },
487
+ getLocalThreadLabelForTarget(target) {
488
+ const followerTarget = deps.getFollowerTarget();
489
+ const leaderTarget = deps.getLeaderTarget();
490
+ const isLocalFollowerTarget =
491
+ deps.isFollowerRegistered() &&
492
+ followerTarget?.chatId === target.chatId &&
493
+ followerTarget.threadId === target.threadId;
494
+ const isLocalLeaderTarget =
495
+ leaderTarget?.chatId === target.chatId &&
496
+ leaderTarget.threadId === target.threadId;
497
+ if (!isLocalFollowerTarget && !isLocalLeaderTarget) return undefined;
498
+ return deps.getCurrentIdentity(target).threadName;
499
+ },
500
+ };
501
+ }
502
+
503
+ export interface TelegramInboundRouteRuntimeDeps<
504
+ TMessage extends TelegramRoutedMessage,
505
+ TCallbackQuery extends TelegramRoutedCallbackQuery,
506
+ TContext,
507
+ TModel extends Model.MenuModel,
508
+ > {
509
+ configStore: Pick<
510
+ TelegramConfigStore,
511
+ "get" | "getAllowedUserId" | "setAllowedUserId" | "persist"
512
+ > & { set?: TelegramConfigStore["set"] };
513
+ callApi?: <TResponse>(
514
+ method: string,
515
+ body: Record<string, unknown>,
516
+ ) => Promise<TResponse>;
517
+ getCurrentInstanceId?: () => string | undefined;
518
+ getAdmissionScope?: () => string | undefined;
519
+ getAdmissionJournalBinding?: () => string | undefined;
520
+ getMessageOwnership?: Updates.TelegramMessageOwnershipLookup;
521
+ getTargetOwnership?: Updates.TelegramTargetOwnershipLookup;
522
+ recordMessageOwnership?: Updates.TelegramMessageOwnershipRecorder;
523
+ getLiveThreadTargets?: () => Queue.TelegramQueueTarget[];
524
+ getLocalThreadLabelForTarget?: (
525
+ target: Queue.TelegramQueueTarget,
526
+ ) => string | undefined;
527
+ getCurrentLeaderEpoch?: () => number | string | undefined;
528
+ setCurrentLeaderIdentity?: (identity: {
529
+ target: Queue.TelegramQueueTarget;
530
+ slot?: string;
531
+ threadName?: string;
532
+ }) => void;
533
+ getThreadReconciliationMachineState?: () =>
534
+ ThreadReconciler.ThreadReconciliationMachineState | undefined;
535
+ recordThreadReconciliationPlan?: (
536
+ plan: ThreadReconciler.ThreadReconciliationPlan,
537
+ ) => void;
538
+ handleTelegramTopicLifecycleUpdate?: (
539
+ lifecycle: Updates.TelegramTopicLifecycleUpdate<TMessage>,
540
+ ctx: TContext,
541
+ ) => Promise<void> | void;
542
+ handleTelegramThreadTargetObserved?: (
543
+ target: Threads.TelegramTopicTargetRecord["target"],
544
+ ctx: TContext,
545
+ ) => Promise<void> | void;
546
+ foreignOwnedUpdateForwarder?: Updates.TelegramForeignOwnedUpdateForwarder<
547
+ TContext,
548
+ Updates.TelegramMessageReactionUpdated,
549
+ TCallbackQuery,
550
+ TMessage
551
+ >;
552
+ replaceFollowerThreadTarget?: (input: {
553
+ record: Threads.TelegramTopicTargetRecord;
554
+ target: Threads.TelegramTopicTargetRecord["target"];
555
+ oldTarget: Threads.TelegramTopicTargetRecord["target"];
556
+ }) => Promise<boolean>;
557
+ bridgeRuntime: TelegramBridgeRuntime;
558
+ activeTurnRuntime: Queue.TelegramActiveTurnStore;
559
+ mediaGroupRuntime: Media.TelegramMediaGroupController<TMessage, TContext>;
560
+ textGroupRuntime: TextGroups.TelegramTextGroupController<TMessage, TContext>;
561
+ telegramQueueStore: Queue.TelegramQueueStateStore<TContext>;
562
+ queueMutationRuntime: Queue.TelegramQueueMutationController<TContext>;
563
+ modelMenuRuntime: Menu.TelegramModelMenuRuntime<TModel>;
564
+ currentModelRuntime: Model.CurrentModelRuntime<TContext, TModel>;
565
+ modelSwitchController: Model.TelegramModelSwitchController<
566
+ TContext,
567
+ Model.ScopedTelegramModel<TModel>
568
+ >;
569
+ menuActions: Menu.TelegramMenuActionRuntime<TContext, TModel>;
570
+ updateSettingsMenuMessage?: (
571
+ state: Menu.TelegramModelMenuState<TModel>,
572
+ ctx: TContext,
573
+ ) => Promise<void>;
574
+ openQueueMenu: (
575
+ chatId: number,
576
+ replyToMessageId: number,
577
+ ctx: TContext,
578
+ ) => Promise<void>;
579
+ openSettingsMenu?: (
580
+ chatId: number,
581
+ replyToMessageId: number,
582
+ ctx: TContext,
583
+ ) => Promise<void>;
584
+ settingsMenuCallbackHandler?: (
585
+ query: TCallbackQuery,
586
+ ctx: TContext,
587
+ ) => Promise<boolean>;
588
+ queueMenuCallbackHandler: (
589
+ query: TCallbackQuery,
590
+ ctx: TContext,
591
+ ) => Promise<boolean>;
592
+ buttonActionStore?: OutboundHandlers.TelegramButtonActionStore;
593
+ invokeBoundButtonAction?: (
594
+ action: OutboundHandlers.TelegramOutboundButtonAction,
595
+ query: TCallbackQuery,
596
+ ctx: TContext,
597
+ ) => Promise<false | "new" | "edit">;
598
+ inboundHandlerRuntime: TelegramInboundHandlerRuntime<TContext>;
599
+ threadStore?: Threads.TelegramTopicTargetStore;
600
+ updateStatus: (ctx: TContext, error?: string) => void;
601
+ isContextActive?: (ctx: TContext) => boolean;
602
+ dispatchNextQueuedTelegramTurn: (ctx: TContext) => void;
603
+ requestDeferredDispatchNextQueuedTelegramTurn?: (
604
+ dispatch: (ctx: TContext) => void,
605
+ ) => void;
606
+ hasDeferredDispatchContext?: () => boolean;
607
+ startTypingLoop?: (
608
+ ctx: TContext,
609
+ chatId?: number,
610
+ options?: { target?: { chatId: number; threadId?: number } },
611
+ ) => void;
612
+ stopTypingLoop?: () => void;
613
+ answerCallbackQuery: (
614
+ callbackQueryId: string,
615
+ text?: string,
616
+ ) => Promise<void>;
617
+ editInteractiveMessage?: (
618
+ chatId: number,
619
+ messageId: number,
620
+ text: string,
621
+ mode: "markdown" | "html" | "plain",
622
+ replyMarkup: Menu.TelegramReplyMarkup,
623
+ ) => Promise<void>;
624
+ editMessageReplyMarkup?: (
625
+ chatId: number,
626
+ messageId: number,
627
+ replyMarkup: OutboundHandlers.TelegramOutboundButtonMarkup,
628
+ ) => Promise<void>;
629
+ sendInteractiveMessage?: (
630
+ chatId: number,
631
+ text: string,
632
+ mode: "markdown" | "html" | "plain",
633
+ replyMarkup: Menu.TelegramReplyMarkup,
634
+ options?: { target?: Queue.TelegramQueueTarget; replyToMessageId?: number },
635
+ ) => Promise<number | undefined>;
636
+ deleteMessage?: (chatId: number, messageId: number) => Promise<void>;
637
+ answerGuestQuery: (guestQueryId: string, text?: string) => Promise<void>;
638
+ sendTextReply: (
639
+ chatId: number,
640
+ replyToMessageId: number,
641
+ text: string,
642
+ options?: { parseMode?: "HTML"; target?: Queue.TelegramQueueTarget },
643
+ ) => Promise<number | undefined>;
644
+ setMyCommands: Commands.TelegramBotCommandRegistrationDeps["setMyCommands"];
645
+ getCommands: () => Parameters<
646
+ typeof PromptTemplates.getTelegramPromptTemplateCommands
647
+ >[0];
648
+ downloadFile: Media.DownloadTelegramMessageFilesDeps["downloadFile"];
649
+ resolveTimeLine?: (chatId: number) => string | null;
650
+ getThinkingLevel: () => Model.ThinkingLevel;
651
+ setThinkingLevel: (level: Model.ThinkingLevel) => void;
652
+ persistScopedModelPatterns?: (
653
+ patterns: string[],
654
+ ctx: TContext,
655
+ ) => Promise<void>;
656
+ setModel: (model: TModel) => Promise<boolean>;
657
+ sendUserMessage?: (
658
+ message: string,
659
+ options?: Queue.TelegramPromptDeliveryOptions,
660
+ ) => void;
661
+ isIdle: (ctx: TContext) => boolean;
662
+ hasPendingMessages: (ctx: TContext) => boolean;
663
+ compact: (
664
+ ctx: TContext,
665
+ callbacks: { onComplete: () => void; onError: (error: unknown) => void },
666
+ ) => void;
667
+ recordRuntimeEvent?: (
668
+ category: string,
669
+ error: unknown,
670
+ details?: Record<string, unknown>,
671
+ ) => void;
672
+ sectionRegistry?: TelegramSectionRegistry;
673
+ }
674
+
675
+ const TELEGRAM_OWNED_CALLBACK_PREFIXES = [
676
+ "allmenu:",
677
+ TELEGRAM_UNBOUND_REROUTE_CALLBACK_PREFIX,
678
+ "compact:",
679
+ "menu:",
680
+ "model:",
681
+ "queue:",
682
+ "section:",
683
+ "settings:",
684
+ "status:",
685
+ "tgbtn:",
686
+ "thinking:",
687
+ ] as const;
688
+
689
+ function isTelegramOwnedCallbackData(data: string): boolean {
690
+ return TELEGRAM_OWNED_CALLBACK_PREFIXES.some((prefix) =>
691
+ data.startsWith(prefix),
692
+ );
693
+ }
694
+
695
+ export function createTelegramInboundRouteRuntime<
696
+ TUpdate extends Updates.TelegramUpdateFlow & {
697
+ message?: TMessage;
698
+ edited_message?: TMessage;
699
+ callback_query?: TCallbackQuery;
700
+ },
701
+ TMessage extends TelegramRoutedMessage,
702
+ TCallbackQuery extends TelegramRoutedCallbackQuery,
703
+ TContext,
704
+ TModel extends Model.MenuModel,
705
+ >(
706
+ deps: TelegramInboundRouteRuntimeDeps<
707
+ TMessage,
708
+ TCallbackQuery,
709
+ TContext,
710
+ TModel
711
+ >,
712
+ ): Updates.TelegramUpdateRuntimeController<TContext, TUpdate> {
713
+ type PendingRerouteCleanup =
714
+ | {
715
+ kind: "unbound";
716
+ target: { chatId: number; threadId: number };
717
+ messageId?: number;
718
+ }
719
+ | {
720
+ kind: "previous-leader";
721
+ target: { chatId: number; threadId: number };
722
+ }
723
+ | {
724
+ kind: "replaced-follower";
725
+ target: { chatId: number; threadId: number };
726
+ instanceId?: string;
727
+ };
728
+ type PendingUnboundReroute = {
729
+ messages: TMessage[];
730
+ createdAtMs: number;
731
+ dispatchKind: "prompt" | "command";
732
+ cleanup?: PendingRerouteCleanup;
733
+ foreignRetry?: {
734
+ instanceId: string;
735
+ threadId: number;
736
+ cleanup: PendingRerouteCleanup;
737
+ };
738
+ finalizeMessage?: string;
739
+ };
740
+ const pendingUnboundReroutes = new Map<string, PendingUnboundReroute>();
741
+ const guidedUnboundTopicKeys = new Set<string>();
742
+ let nextUnboundRerouteId = 0;
743
+ const requestDispatchNextQueuedTelegramTurn = (ctx: TContext): void => {
744
+ deps.dispatchNextQueuedTelegramTurn(ctx);
745
+ if (
746
+ deps.requestDeferredDispatchNextQueuedTelegramTurn &&
747
+ deps.hasDeferredDispatchContext?.() !== false
748
+ ) {
749
+ deps.requestDeferredDispatchNextQueuedTelegramTurn(
750
+ deps.dispatchNextQueuedTelegramTurn,
751
+ );
752
+ }
753
+ };
754
+ const createAdmissionReceipts = (
755
+ queueKind: Queue.TelegramQueueItemKind,
756
+ sources: readonly unknown[],
757
+ ): Queue.TelegramQueueAdmissionReceipt[] => {
758
+ const sourceUpdateIds =
759
+ Updates.collectTelegramAdmissionSourceUpdateIds(sources);
760
+ if (sourceUpdateIds.length === 0) return [];
761
+ const receipt = Queue.createTelegramQueueAdmissionReceipt({
762
+ queueKind,
763
+ scope: deps.getAdmissionScope?.() ?? "",
764
+ sourceUpdateIds,
765
+ });
766
+ const journalBindingKey = deps.getAdmissionJournalBinding?.();
767
+ return receipt
768
+ ? [{
769
+ ...receipt,
770
+ ...(journalBindingKey ? { journalBindingKey } : {}),
771
+ }]
772
+ : [];
773
+ };
774
+ const reportQueueAdmission = (
775
+ sources: readonly unknown[],
776
+ receipts: readonly Queue.TelegramQueueAdmissionReceipt[],
777
+ ): void => {
778
+ Updates.reportTelegramQueueAdmission(sources, receipts);
779
+ };
780
+ const prunePendingUnboundReroutes = () => {
781
+ const nowMs = Date.now();
782
+ for (const [id, entry] of pendingUnboundReroutes) {
783
+ if (nowMs - entry.createdAtMs > 30 * 60_000) {
784
+ pendingUnboundReroutes.delete(id);
785
+ }
786
+ }
787
+ while (pendingUnboundReroutes.size > 100) {
788
+ const oldest = pendingUnboundReroutes.keys().next().value;
789
+ if (!oldest) break;
790
+ pendingUnboundReroutes.delete(oldest);
791
+ }
792
+ };
793
+ const storePendingUnboundReroute = (
794
+ messages: TMessage[],
795
+ dispatchKind: "prompt" | "command" = "prompt",
796
+ ): string => {
797
+ prunePendingUnboundReroutes();
798
+ nextUnboundRerouteId += 1;
799
+ const id = nextUnboundRerouteId.toString(36);
800
+ pendingUnboundReroutes.set(id, {
801
+ messages,
802
+ createdAtMs: Date.now(),
803
+ dispatchKind,
804
+ });
805
+ return id;
806
+ };
807
+ const pendingUnboundRerouteMediaGroups = new Map<
808
+ string,
809
+ {
810
+ messages: TMessage[];
811
+ timer: ReturnType<typeof setTimeout>;
812
+ }
813
+ >();
814
+ const menuCallbackHandler = Menu.createTelegramMenuCallbackHandlerForContext<
815
+ TCallbackQuery,
816
+ TContext,
817
+ TModel
818
+ >({
819
+ getStoredModelMenuState: deps.modelMenuRuntime.getState,
820
+ getActiveModel: deps.currentModelRuntime.get,
821
+ getThinkingLevel: deps.getThinkingLevel,
822
+ setThinkingLevel: deps.setThinkingLevel,
823
+ updateStatus: deps.updateStatus,
824
+ updateModelMenuMessage: deps.menuActions.updateModelMenuMessage,
825
+ updateThinkingMenuMessage: deps.menuActions.updateThinkingMenuMessage,
826
+ updateStatusMessage: deps.menuActions.updateStatusMessage,
827
+ updateSettingsMenuMessage: deps.updateSettingsMenuMessage,
828
+ answerCallbackQuery: deps.answerCallbackQuery,
829
+ isIdle: deps.isIdle,
830
+ hasActiveTelegramTurn: deps.activeTurnRuntime.has,
831
+ hasAbortHandler: deps.bridgeRuntime.abort.hasHandler,
832
+ getActiveToolExecutions:
833
+ deps.bridgeRuntime.lifecycle.getActiveToolExecutions,
834
+ persistScopedModelPatterns: deps.persistScopedModelPatterns,
835
+ setModel: deps.setModel,
836
+ setCurrentModel: deps.currentModelRuntime.setCurrentModel,
837
+ stagePendingModelSwitch: deps.modelSwitchController.stagePendingSwitch,
838
+ restartInterruptedTelegramTurn:
839
+ deps.modelSwitchController.restartInterruptedTurn,
840
+ sectionRegistry: deps.sectionRegistry,
841
+ editInteractiveMessage: deps.editInteractiveMessage,
842
+ sendInteractiveMessage: deps.sendInteractiveMessage,
843
+ deleteMessage: deps.deleteMessage,
844
+ enqueueSectionPrompt: async (
845
+ prompt: string,
846
+ ctx: TContext,
847
+ target?: Queue.TelegramQueueTarget,
848
+ source?: unknown,
849
+ ) => {
850
+ const chatId = target?.chatId ?? deps.configStore.getAllowedUserId();
851
+ if (typeof chatId !== "number") return;
852
+ const order = deps.bridgeRuntime.queue.allocateItemOrder();
853
+ const admissionReceipts = createAdmissionReceipts(
854
+ "prompt",
855
+ source === undefined ? [] : [source],
856
+ );
857
+ const turn: Queue.PendingTelegramTurn = {
858
+ kind: "prompt",
859
+ chatId,
860
+ ...(target ? { target } : {}),
861
+ replyToMessageId: 0,
862
+ sourceMessageIds: [],
863
+ queueOrder: order,
864
+ queueLane: "default",
865
+ laneOrder: order,
866
+ queuedAttachments: [],
867
+ content: [
868
+ {
869
+ type: "text",
870
+ text: `[telegram] ${prompt}`,
871
+ },
872
+ ],
873
+ historyText: Turns.truncateTelegramQueueSummary(prompt),
874
+ statusSummary: Turns.truncateTelegramQueueSummary(prompt),
875
+ ...(admissionReceipts.length > 0 ? { admissionReceipts } : {}),
876
+ };
877
+ deps.queueMutationRuntime.append(turn, ctx);
878
+ reportQueueAdmission(
879
+ source === undefined ? [] : [source],
880
+ admissionReceipts,
881
+ );
882
+ deps.updateStatus(ctx);
883
+ requestDispatchNextQueuedTelegramTurn(ctx);
884
+ },
885
+ });
886
+ const cloneTelegramMessagesForThread = (
887
+ messages: TMessage[],
888
+ threadId: number,
889
+ ): TMessage[] => {
890
+ return messages.map(
891
+ (message) =>
892
+ Updates.carryTelegramUpdateExecutionFence(
893
+ message,
894
+ {
895
+ ...message,
896
+ message_id: 0,
897
+ message_thread_id: threadId,
898
+ reply_to_message: undefined,
899
+ } as TMessage,
900
+ ),
901
+ );
902
+ };
903
+ const applyThreadCleanupPlan = async (
904
+ plan: ThreadReconciler.ThreadReconciliationPlan,
905
+ assertExecutionCurrent?: () => void,
906
+ ): Promise<boolean> => {
907
+ assertExecutionCurrent?.();
908
+ deps.recordThreadReconciliationPlan?.(plan);
909
+ const result = await ThreadReconciler.applyThreadReconciliationPlan(plan, {
910
+ callApi: deps.callApi,
911
+ markStaleByTarget: (staleTarget, syncStatus, lastSyncError) =>
912
+ deps.threadStore?.markStaleByTarget(
913
+ staleTarget,
914
+ syncStatus,
915
+ lastSyncError,
916
+ ) ?? false,
917
+ persist: () => deps.threadStore?.persist() ?? Promise.resolve(),
918
+ removePendingProvisionById: (id) =>
919
+ deps.threadStore?.removePendingProvision(id) ?? false,
920
+ getCurrentLeaderEpoch: deps.getCurrentLeaderEpoch,
921
+ recordRuntimeEvent: deps.recordRuntimeEvent,
922
+ });
923
+ assertExecutionCurrent?.();
924
+ return (result.incompleteActions?.length ?? 0) === 0;
925
+ };
926
+ const dismissRerouteChooserMessage = async (
927
+ query: TCallbackQuery,
928
+ assertExecutionCurrent?: () => void,
929
+ ): Promise<boolean> => {
930
+ const chatId = query.message?.chat?.id;
931
+ const messageId = query.message?.message_id;
932
+ if (
933
+ typeof chatId !== "number" ||
934
+ typeof messageId !== "number" ||
935
+ !deps.deleteMessage
936
+ ) {
937
+ return false;
938
+ }
939
+ try {
940
+ assertExecutionCurrent?.();
941
+ await deps.deleteMessage(chatId, messageId);
942
+ assertExecutionCurrent?.();
943
+ return true;
944
+ } catch (error) {
945
+ deps.recordRuntimeEvent?.("telegram", error, {
946
+ phase: "reroute-chooser-delete",
947
+ chatId,
948
+ messageId,
949
+ threadId: query.message?.message_thread_id,
950
+ });
951
+ return false;
952
+ }
953
+ };
954
+ const closeReroutedUnboundTopic = async (
955
+ target: { chatId: number; threadId: number } | undefined,
956
+ messageId: number | undefined,
957
+ assertExecutionCurrent?: () => void,
958
+ ): Promise<boolean> => {
959
+ if (!target || !deps.threadStore) return true;
960
+ const nowMs = Date.now();
961
+ const currentLeaderEpoch = deps.getCurrentLeaderEpoch?.();
962
+ const plan = ThreadReconciler.planThreadReconciliation({
963
+ nowMs,
964
+ currentLeaderEpoch,
965
+ previousState: deps.getThreadReconciliationMachineState?.(),
966
+ records: deps.threadStore.list(),
967
+ reservations: deps.threadStore.listReservations(),
968
+ pendingProvisions: deps.threadStore.listPendingProvisions(),
969
+ unboundMessages: [
970
+ {
971
+ target,
972
+ observedAtMs: nowMs,
973
+ ...(typeof messageId === "number" ? { messageId } : {}),
974
+ ...(currentLeaderEpoch !== undefined
975
+ ? { leaderEpoch: currentLeaderEpoch }
976
+ : {}),
977
+ },
978
+ ],
979
+ });
980
+ return applyThreadCleanupPlan(plan, assertExecutionCurrent);
981
+ };
982
+ const closePreviousLeaderThread = async (
983
+ target: { chatId: number; threadId: number } | undefined,
984
+ assertExecutionCurrent?: () => void,
985
+ ): Promise<boolean> => {
986
+ if (!target || !deps.threadStore) return true;
987
+ const currentLeaderEpoch = deps.getCurrentLeaderEpoch?.();
988
+ return applyThreadCleanupPlan({
989
+ actions: [
990
+ {
991
+ kind: "close-delete-previous-leader-topic",
992
+ target,
993
+ reason: "previous-leader",
994
+ instanceId: deps.getCurrentInstanceId?.(),
995
+ ...(currentLeaderEpoch !== undefined
996
+ ? { leaderEpoch: currentLeaderEpoch }
997
+ : {}),
998
+ },
999
+ ],
1000
+ }, assertExecutionCurrent);
1001
+ };
1002
+ const closeReplacedFollowerThread = async (
1003
+ target: { chatId: number; threadId: number } | undefined,
1004
+ instanceId: string | undefined,
1005
+ assertExecutionCurrent?: () => void,
1006
+ ): Promise<boolean> => {
1007
+ if (!target || !deps.threadStore) return true;
1008
+ const currentLeaderEpoch = deps.getCurrentLeaderEpoch?.();
1009
+ return applyThreadCleanupPlan({
1010
+ actions: [
1011
+ {
1012
+ kind: "close-delete-replaced-follower-topic",
1013
+ target,
1014
+ reason: "replaced-follower",
1015
+ instanceId,
1016
+ ...(currentLeaderEpoch !== undefined
1017
+ ? { leaderEpoch: currentLeaderEpoch }
1018
+ : {}),
1019
+ },
1020
+ ],
1021
+ }, assertExecutionCurrent);
1022
+ };
1023
+ const retryPendingRerouteCleanup = async (
1024
+ cleanup: PendingRerouteCleanup,
1025
+ assertExecutionCurrent?: () => void,
1026
+ ): Promise<boolean> => {
1027
+ if (cleanup.kind === "unbound") {
1028
+ return closeReroutedUnboundTopic(
1029
+ cleanup.target,
1030
+ cleanup.messageId,
1031
+ assertExecutionCurrent,
1032
+ );
1033
+ }
1034
+ if (cleanup.kind === "previous-leader") {
1035
+ return closePreviousLeaderThread(cleanup.target, assertExecutionCurrent);
1036
+ }
1037
+ return closeReplacedFollowerThread(
1038
+ cleanup.target,
1039
+ cleanup.instanceId,
1040
+ assertExecutionCurrent,
1041
+ );
1042
+ };
1043
+ let dispatchReroutedCommandMessages:
1044
+ | ((messages: TMessage[], ctx: TContext) => Promise<void>)
1045
+ | undefined;
1046
+ const dispatchPendingRerouteMessages = async (
1047
+ pending: { dispatchKind: "prompt" | "command" },
1048
+ messages: TMessage[],
1049
+ ctx: TContext,
1050
+ ): Promise<void> => {
1051
+ if (pending.dispatchKind === "command" && dispatchReroutedCommandMessages) {
1052
+ await dispatchReroutedCommandMessages(messages, ctx);
1053
+ return;
1054
+ }
1055
+ await promptEnqueue(messages, ctx);
1056
+ };
1057
+ const finalizePendingReroute = async (
1058
+ rerouteId: string,
1059
+ pending: PendingUnboundReroute,
1060
+ query: TCallbackQuery,
1061
+ successMessage: string,
1062
+ assertExecutionCurrent?: () => void,
1063
+ ): Promise<void> => {
1064
+ const dismissed = await dismissRerouteChooserMessage(
1065
+ query,
1066
+ assertExecutionCurrent,
1067
+ );
1068
+ if (dismissed) {
1069
+ pendingUnboundReroutes.delete(rerouteId);
1070
+ await deps.answerCallbackQuery(query.id, successMessage);
1071
+ return;
1072
+ }
1073
+ pending.finalizeMessage = successMessage;
1074
+ await deps.answerCallbackQuery(
1075
+ query.id,
1076
+ `${successMessage} Chooser cleanup is still pending. Try again.`,
1077
+ );
1078
+ };
1079
+ const forwardPendingRerouteMessages = async (
1080
+ pending: PendingUnboundReroute,
1081
+ instanceId: string,
1082
+ threadId: number,
1083
+ ctx: TContext,
1084
+ assertExecutionCurrent?: () => void,
1085
+ ): Promise<boolean> => {
1086
+ assertExecutionCurrent?.();
1087
+ const forwardMessage = deps.foreignOwnedUpdateForwarder?.forwardMessage;
1088
+ if (!forwardMessage) return false;
1089
+ const messages = cloneTelegramMessagesForThread(pending.messages, threadId);
1090
+ const outcomes = await Promise.allSettled(
1091
+ messages.map((message) =>
1092
+ forwardMessage({
1093
+ message,
1094
+ ownership: { instanceId },
1095
+ ctx,
1096
+ }),
1097
+ ),
1098
+ );
1099
+ assertExecutionCurrent?.();
1100
+ pending.messages = pending.messages.filter((_, index) => {
1101
+ const outcome = outcomes[index];
1102
+ if (
1103
+ outcome?.status === "fulfilled" &&
1104
+ outcome.value.status === "accepted"
1105
+ ) {
1106
+ return false;
1107
+ }
1108
+ if (outcome?.status === "rejected") {
1109
+ deps.recordRuntimeEvent?.("bus", outcome.reason, {
1110
+ phase: "reroute-foreign-forward",
1111
+ instanceId,
1112
+ threadId,
1113
+ messageIndex: index,
1114
+ });
1115
+ }
1116
+ return true;
1117
+ });
1118
+ return pending.messages.length === 0;
1119
+ };
1120
+ const handleUnboundRerouteRestoreMenuCallback = async (
1121
+ query: TCallbackQuery,
1122
+ _ctx: TContext,
1123
+ ): Promise<boolean> => {
1124
+ const parsed = parseTelegramUnboundRerouteRestoreMenuCallbackData(
1125
+ query.data,
1126
+ );
1127
+ if (!parsed) return false;
1128
+ const chatId = query.message?.chat?.id;
1129
+ const messageId = query.message?.message_id;
1130
+ const pending = pendingUnboundReroutes.get(parsed.rerouteId);
1131
+ if (
1132
+ typeof chatId !== "number" ||
1133
+ typeof messageId !== "number" ||
1134
+ !deps.threadStore ||
1135
+ !pending
1136
+ ) {
1137
+ await deps.answerCallbackQuery(query.id, "Message route expired.");
1138
+ return true;
1139
+ }
1140
+ await deps.threadStore.load();
1141
+ const activeRecords = getTelegramRoutableThreadRecords(
1142
+ deps.threadStore.list(),
1143
+ deps.getLiveThreadTargets?.(),
1144
+ );
1145
+ const replyMarkup = buildTelegramUnboundRerouteRestoreChooserMarkup(
1146
+ parsed.rerouteId,
1147
+ activeRecords,
1148
+ );
1149
+ if (deps.editInteractiveMessage) {
1150
+ await deps.editInteractiveMessage(
1151
+ chatId,
1152
+ messageId,
1153
+ formatTelegramUnboundRerouteRestoreChooserText(),
1154
+ "html",
1155
+ replyMarkup,
1156
+ );
1157
+ } else if (deps.sendInteractiveMessage) {
1158
+ await deps.sendInteractiveMessage(
1159
+ chatId,
1160
+ formatTelegramUnboundRerouteRestoreChooserText(),
1161
+ "html",
1162
+ replyMarkup,
1163
+ typeof query.message?.message_thread_id === "number"
1164
+ ? {
1165
+ target: { chatId, threadId: query.message.message_thread_id },
1166
+ replyToMessageId: messageId,
1167
+ }
1168
+ : undefined,
1169
+ );
1170
+ }
1171
+ await deps.answerCallbackQuery(query.id, "Choose instance to restore.");
1172
+ return true;
1173
+ };
1174
+ const handleUnboundRerouteCallback = async (
1175
+ query: TCallbackQuery,
1176
+ ctx: TContext,
1177
+ ): Promise<boolean> => {
1178
+ const parsed = parseTelegramUnboundRerouteCallbackData(query.data);
1179
+ if (!parsed) return false;
1180
+ const assertExecutionCurrent =
1181
+ Updates.createTelegramUpdateExecutionFenceGuard(query);
1182
+ assertExecutionCurrent();
1183
+ const chatId = query.message?.chat?.id;
1184
+ const pending = pendingUnboundReroutes.get(parsed.rerouteId);
1185
+ if (typeof chatId !== "number" || !deps.threadStore || !pending) {
1186
+ await deps.answerCallbackQuery(query.id, "Message route expired.");
1187
+ return true;
1188
+ }
1189
+ await deps.threadStore.load();
1190
+ assertExecutionCurrent();
1191
+ if (pending.finalizeMessage) {
1192
+ await finalizePendingReroute(
1193
+ parsed.rerouteId,
1194
+ pending,
1195
+ query,
1196
+ pending.finalizeMessage,
1197
+ assertExecutionCurrent,
1198
+ );
1199
+ return true;
1200
+ }
1201
+ if (pending.foreignRetry) {
1202
+ const retry = pending.foreignRetry;
1203
+ const allForwarded = await forwardPendingRerouteMessages(
1204
+ pending,
1205
+ retry.instanceId,
1206
+ retry.threadId,
1207
+ ctx,
1208
+ assertExecutionCurrent,
1209
+ );
1210
+ if (!allForwarded) {
1211
+ await deps.answerCallbackQuery(
1212
+ query.id,
1213
+ "Target thread is unavailable; retrying will send only remaining messages.",
1214
+ );
1215
+ return true;
1216
+ }
1217
+ pending.foreignRetry = undefined;
1218
+ if (retry.cleanup) pending.cleanup = retry.cleanup;
1219
+ }
1220
+ if (pending.cleanup) {
1221
+ const cleanupComplete = await retryPendingRerouteCleanup(
1222
+ pending.cleanup,
1223
+ assertExecutionCurrent,
1224
+ );
1225
+ if (!cleanupComplete) {
1226
+ await deps.answerCallbackQuery(
1227
+ query.id,
1228
+ "Message routed, but thread cleanup is still pending. Try again.",
1229
+ );
1230
+ return true;
1231
+ }
1232
+ pending.cleanup = undefined;
1233
+ await finalizePendingReroute(
1234
+ parsed.rerouteId,
1235
+ pending,
1236
+ query,
1237
+ "Thread cleanup completed.",
1238
+ assertExecutionCurrent,
1239
+ );
1240
+ return true;
1241
+ }
1242
+ const record = getTelegramRoutableThreadRecords(
1243
+ deps.threadStore.list(),
1244
+ deps.getLiveThreadTargets?.(),
1245
+ ).find(
1246
+ (candidate) =>
1247
+ candidate.target.chatId === chatId &&
1248
+ candidate.target.threadId === parsed.threadId,
1249
+ );
1250
+ if (!record) {
1251
+ await deps.answerCallbackQuery(query.id, "Thread is not active yet.");
1252
+ return true;
1253
+ }
1254
+ const reroutedMessages = cloneTelegramMessagesForThread(
1255
+ pending.messages,
1256
+ parsed.threadId,
1257
+ );
1258
+ const sourceTarget =
1259
+ typeof query.message?.message_thread_id === "number"
1260
+ ? { chatId, threadId: query.message.message_thread_id }
1261
+ : undefined;
1262
+ const sourceMessageId = query.message?.message_id;
1263
+ const currentInstanceId = deps.getCurrentInstanceId?.();
1264
+ const leaderProfileKey = getLeaderTopicProfileKey(ctx, currentInstanceId);
1265
+ const isCurrentLeaderRecord = isCurrentLeaderTopicRecord(
1266
+ record,
1267
+ leaderProfileKey,
1268
+ currentInstanceId,
1269
+ );
1270
+ if (parsed.useNewSlot && !isCurrentLeaderRecord) {
1271
+ if (
1272
+ !sourceTarget ||
1273
+ !deps.replaceFollowerThreadTarget ||
1274
+ !deps.foreignOwnedUpdateForwarder?.forwardMessage
1275
+ ) {
1276
+ await deps.answerCallbackQuery(
1277
+ query.id,
1278
+ "Follower thread restore is not available yet.",
1279
+ );
1280
+ return true;
1281
+ }
1282
+ assertExecutionCurrent();
1283
+ const replaced = await deps.replaceFollowerThreadTarget({
1284
+ record,
1285
+ target: sourceTarget,
1286
+ oldTarget: record.target,
1287
+ });
1288
+ assertExecutionCurrent();
1289
+ if (!replaced) {
1290
+ await deps.answerCallbackQuery(
1291
+ query.id,
1292
+ "Follower thread is unavailable.",
1293
+ );
1294
+ return true;
1295
+ }
1296
+ const nowMs = Date.now();
1297
+ const slot = record.slot ?? "?";
1298
+ const threadName = getRestoredThreadName(record, slot);
1299
+ assertExecutionCurrent();
1300
+ deps.threadStore.markStaleByTarget(
1301
+ record.target,
1302
+ "deleted",
1303
+ "Follower thread was replaced by restore source.",
1304
+ );
1305
+ deps.threadStore.upsert({
1306
+ ...record,
1307
+ target: sourceTarget,
1308
+ status: "active",
1309
+ syncStatus: "open",
1310
+ updatedAtMs: nowMs,
1311
+ threadName,
1312
+ lastSyncObservedAtMs: nowMs,
1313
+ lastReconcileAction: "follower-thread-restore",
1314
+ rerouteConfirmedAtMs: nowMs,
1315
+ });
1316
+ await deps.threadStore.persist();
1317
+ assertExecutionCurrent();
1318
+ if (deps.callApi) {
1319
+ try {
1320
+ assertExecutionCurrent();
1321
+ await deps.callApi("editForumTopic", {
1322
+ chat_id: sourceTarget.chatId,
1323
+ message_thread_id: sourceTarget.threadId,
1324
+ name: Threads.getTelegramTopicTitleForThreadName(threadName, slot),
1325
+ });
1326
+ assertExecutionCurrent();
1327
+ } catch (renameError) {
1328
+ deps.recordRuntimeEvent?.("telegram", renameError, {
1329
+ phase: "follower-topic-reroute-restore-rename",
1330
+ chatId: sourceTarget.chatId,
1331
+ threadId: sourceTarget.threadId,
1332
+ slot: record.slot,
1333
+ });
1334
+ }
1335
+ }
1336
+ const cleanup: PendingRerouteCleanup = {
1337
+ kind: "replaced-follower",
1338
+ target: record.target,
1339
+ instanceId: record.instanceId,
1340
+ };
1341
+ const allForwarded = await forwardPendingRerouteMessages(
1342
+ pending,
1343
+ record.instanceId!,
1344
+ sourceTarget.threadId,
1345
+ ctx,
1346
+ assertExecutionCurrent,
1347
+ );
1348
+ if (!allForwarded) {
1349
+ pending.foreignRetry = {
1350
+ instanceId: record.instanceId!,
1351
+ threadId: sourceTarget.threadId,
1352
+ cleanup,
1353
+ };
1354
+ await deps.answerCallbackQuery(
1355
+ query.id,
1356
+ "Thread restored; retrying will send only remaining messages before old-thread cleanup.",
1357
+ );
1358
+ return true;
1359
+ }
1360
+ const cleanupComplete = await retryPendingRerouteCleanup(
1361
+ cleanup,
1362
+ assertExecutionCurrent,
1363
+ );
1364
+ if (!cleanupComplete) {
1365
+ pending.cleanup = cleanup;
1366
+ await deps.answerCallbackQuery(
1367
+ query.id,
1368
+ "Thread restored, but old-thread cleanup is still pending. Try again.",
1369
+ );
1370
+ return true;
1371
+ }
1372
+ await finalizePendingReroute(
1373
+ parsed.rerouteId,
1374
+ pending,
1375
+ query,
1376
+ "Message routed.",
1377
+ );
1378
+ return true;
1379
+ }
1380
+ if (
1381
+ record.instanceId &&
1382
+ record.instanceId !== currentInstanceId &&
1383
+ !isCurrentLeaderRecord
1384
+ ) {
1385
+ if (!deps.foreignOwnedUpdateForwarder?.forwardMessage) {
1386
+ await deps.answerCallbackQuery(
1387
+ query.id,
1388
+ "Open that thread and resend the message there.",
1389
+ );
1390
+ return true;
1391
+ }
1392
+ const allForwarded = await forwardPendingRerouteMessages(
1393
+ pending,
1394
+ record.instanceId,
1395
+ parsed.threadId,
1396
+ ctx,
1397
+ assertExecutionCurrent,
1398
+ );
1399
+ if (!allForwarded) {
1400
+ await deps.answerCallbackQuery(
1401
+ query.id,
1402
+ "Target thread is unavailable; retrying will send only remaining messages.",
1403
+ );
1404
+ return true;
1405
+ }
1406
+ const cleanupComplete = await closeReroutedUnboundTopic(
1407
+ sourceTarget,
1408
+ sourceMessageId,
1409
+ assertExecutionCurrent,
1410
+ );
1411
+ if (!cleanupComplete && sourceTarget) {
1412
+ pending.cleanup = {
1413
+ kind: "unbound",
1414
+ target: sourceTarget,
1415
+ ...(typeof sourceMessageId === "number"
1416
+ ? { messageId: sourceMessageId }
1417
+ : {}),
1418
+ };
1419
+ await deps.answerCallbackQuery(
1420
+ query.id,
1421
+ "Message routed, but thread cleanup is still pending. Try again.",
1422
+ );
1423
+ return true;
1424
+ }
1425
+ await finalizePendingReroute(
1426
+ parsed.rerouteId,
1427
+ pending,
1428
+ query,
1429
+ "Message routed.",
1430
+ );
1431
+ return true;
1432
+ }
1433
+ if (
1434
+ sourceTarget &&
1435
+ isCurrentLeaderRecord &&
1436
+ parsed.useNewSlot &&
1437
+ (record.target.chatId !== sourceTarget.chatId ||
1438
+ record.target.threadId !== sourceTarget.threadId)
1439
+ ) {
1440
+ deps.threadStore.markStaleByTarget(
1441
+ record.target,
1442
+ "deleted",
1443
+ "Current leader thread was replaced by a new-slot reroute source.",
1444
+ );
1445
+ const slot =
1446
+ deps.threadStore.allocateSlot(
1447
+ leaderProfileKey ?? record.profileKey,
1448
+ getNextTelegramSlotPreference(record.slot),
1449
+ ) ??
1450
+ record.slot ??
1451
+ "?";
1452
+ const nowMs = Date.now();
1453
+ const threadName = getRestoredThreadName(record, slot);
1454
+ deps.threadStore.upsert({
1455
+ ...record,
1456
+ target: sourceTarget,
1457
+ status: "active",
1458
+ updatedAtMs: nowMs,
1459
+ threadName,
1460
+ instanceId: currentInstanceId,
1461
+ slot,
1462
+ lastReconcileAction: "reroute-new-slot",
1463
+ rerouteConfirmedAtMs: nowMs,
1464
+ });
1465
+ await deps.threadStore.persist();
1466
+ deps.setCurrentLeaderIdentity?.({
1467
+ target: sourceTarget,
1468
+ slot,
1469
+ threadName,
1470
+ });
1471
+ if (deps.callApi) {
1472
+ try {
1473
+ assertExecutionCurrent();
1474
+ await deps.callApi("editForumTopic", {
1475
+ chat_id: sourceTarget.chatId,
1476
+ message_thread_id: sourceTarget.threadId,
1477
+ name: Threads.getTelegramTopicTitleForThreadName(threadName, slot),
1478
+ });
1479
+ assertExecutionCurrent();
1480
+ } catch (renameError) {
1481
+ deps.recordRuntimeEvent?.("telegram", renameError, {
1482
+ phase: "leader-topic-reroute-reclaim-rename",
1483
+ chatId: sourceTarget.chatId,
1484
+ threadId: sourceTarget.threadId,
1485
+ slot,
1486
+ });
1487
+ }
1488
+ }
1489
+ deps.recordRuntimeEvent?.(
1490
+ "bus",
1491
+ "Bus leader reclaimed reroute source thread",
1492
+ {
1493
+ phase: "leader-topic-reroute-reclaim",
1494
+ chatId: sourceTarget.chatId,
1495
+ threadId: sourceTarget.threadId,
1496
+ staleThreadId: record.target.threadId,
1497
+ slot,
1498
+ },
1499
+ );
1500
+ await dispatchPendingRerouteMessages(
1501
+ pending,
1502
+ cloneTelegramMessagesForThread(pending.messages, sourceTarget.threadId),
1503
+ ctx,
1504
+ );
1505
+ pending.messages = [];
1506
+ const cleanupComplete = await closePreviousLeaderThread(
1507
+ record.target,
1508
+ assertExecutionCurrent,
1509
+ );
1510
+ if (!cleanupComplete) {
1511
+ pending.cleanup = {
1512
+ kind: "previous-leader",
1513
+ target: record.target,
1514
+ };
1515
+ await deps.answerCallbackQuery(
1516
+ query.id,
1517
+ "Thread restored, but old-thread cleanup is still pending. Try again.",
1518
+ );
1519
+ return true;
1520
+ }
1521
+ await finalizePendingReroute(
1522
+ parsed.rerouteId,
1523
+ pending,
1524
+ query,
1525
+ "Message routed.",
1526
+ );
1527
+ return true;
1528
+ }
1529
+ await dispatchPendingRerouteMessages(pending, reroutedMessages, ctx);
1530
+ pending.messages = [];
1531
+ const cleanupComplete = await closeReroutedUnboundTopic(
1532
+ sourceTarget,
1533
+ sourceMessageId,
1534
+ assertExecutionCurrent,
1535
+ );
1536
+ if (!cleanupComplete && sourceTarget) {
1537
+ pending.cleanup = {
1538
+ kind: "unbound",
1539
+ target: sourceTarget,
1540
+ ...(typeof sourceMessageId === "number"
1541
+ ? { messageId: sourceMessageId }
1542
+ : {}),
1543
+ };
1544
+ await deps.answerCallbackQuery(
1545
+ query.id,
1546
+ "Message routed, but thread cleanup is still pending. Try again.",
1547
+ );
1548
+ return true;
1549
+ }
1550
+ await finalizePendingReroute(
1551
+ parsed.rerouteId,
1552
+ pending,
1553
+ query,
1554
+ "Message routed.",
1555
+ );
1556
+ return true;
1557
+ };
1558
+ const callbackHandler = async (
1559
+ query: TCallbackQuery,
1560
+ ctx: TContext,
1561
+ ): Promise<void> => {
1562
+ const assertExecutionCurrent =
1563
+ Updates.createTelegramUpdateExecutionFenceGuard(query);
1564
+ assertExecutionCurrent();
1565
+ if (await handleUnboundRerouteRestoreMenuCallback(query, ctx)) return;
1566
+ if (await handleUnboundRerouteCallback(query, ctx)) return;
1567
+ if (deps.buttonActionStore) {
1568
+ const handled = await OutboundHandlers.handleTelegramButtonCallbackQuery(
1569
+ query,
1570
+ ctx,
1571
+ {
1572
+ resolveAction: deps.buttonActionStore.resolve,
1573
+ answerCallbackQuery: deps.answerCallbackQuery,
1574
+ ...(deps.invokeBoundButtonAction
1575
+ ? {
1576
+ invokeBoundAction: (buttonQuery, action, context) =>
1577
+ deps.invokeBoundButtonAction!(
1578
+ action,
1579
+ buttonQuery as TCallbackQuery,
1580
+ context,
1581
+ ),
1582
+ }
1583
+ : {}),
1584
+ editMessageReplyMarkup: deps.editMessageReplyMarkup
1585
+ ? async (chatId, messageId, replyMarkup) => {
1586
+ try {
1587
+ await deps.editMessageReplyMarkup?.(
1588
+ chatId,
1589
+ messageId,
1590
+ replyMarkup,
1591
+ );
1592
+ } catch (error) {
1593
+ deps.recordRuntimeEvent?.("telegram", error, {
1594
+ phase: "button-selection-mark",
1595
+ chatId,
1596
+ messageId,
1597
+ });
1598
+ }
1599
+ }
1600
+ : undefined,
1601
+ enqueueButtonPrompt: (buttonQuery, action, context) => {
1602
+ const chatId = buttonQuery.message?.chat?.id;
1603
+ const messageId = buttonQuery.message?.message_id;
1604
+ if (typeof chatId !== "number" || typeof messageId !== "number")
1605
+ return false;
1606
+ const queueOrder = deps.bridgeRuntime.queue.allocateItemOrder();
1607
+ const admissionReceipts = createAdmissionReceipts("prompt", [
1608
+ buttonQuery,
1609
+ ]);
1610
+ const turn: Queue.PendingTelegramTurn = {
1611
+ ...OutboundHandlers.createTelegramButtonPromptTurn({
1612
+ chatId,
1613
+ target:
1614
+ typeof buttonQuery.message?.message_thread_id === "number"
1615
+ ? {
1616
+ chatId,
1617
+ threadId: buttonQuery.message.message_thread_id,
1618
+ }
1619
+ : { chatId },
1620
+ replyToMessageId: messageId,
1621
+ queueOrder,
1622
+ action,
1623
+ }),
1624
+ ...(admissionReceipts.length > 0 ? { admissionReceipts } : {}),
1625
+ };
1626
+ const result = Queue.appendTelegramPromptTurnOnce(
1627
+ deps.telegramQueueStore.getQueuedItems(),
1628
+ turn,
1629
+ );
1630
+ if (!result.appended) {
1631
+ reportQueueAdmission([buttonQuery], admissionReceipts);
1632
+ return false;
1633
+ }
1634
+ Updates.assertTelegramUpdateExecutionCurrent(buttonQuery);
1635
+ deps.telegramQueueStore.setQueuedItems(result.items);
1636
+ reportQueueAdmission([buttonQuery], admissionReceipts);
1637
+ deps.updateStatus(context);
1638
+ requestDispatchNextQueuedTelegramTurn(context);
1639
+ return true;
1640
+ },
1641
+ },
1642
+ );
1643
+ assertExecutionCurrent();
1644
+ if (handled) return;
1645
+ }
1646
+ const handledByCompact =
1647
+ await Commands.handleTelegramCompactConfirmationCallback(query, {
1648
+ ctx,
1649
+ answerCallbackQuery: deps.answerCallbackQuery,
1650
+ editInteractiveMessage: deps.editInteractiveMessage ?? (async () => {}),
1651
+ runCompact: async (compactCtx, chatId, replyToMessageId, target) => {
1652
+ await Commands.handleTelegramCompactCommand({
1653
+ isIdle: () => deps.isIdle(compactCtx),
1654
+ hasPendingMessages: () => deps.hasPendingMessages(compactCtx),
1655
+ hasActiveTelegramTurn: deps.activeTurnRuntime.has,
1656
+ hasDispatchPending: deps.bridgeRuntime.lifecycle.hasDispatchPending,
1657
+ hasQueuedTelegramItems: deps.telegramQueueStore.hasQueuedItems,
1658
+ isCompactionInProgress:
1659
+ deps.bridgeRuntime.lifecycle.isCompactionInProgress,
1660
+ setCompactionInProgress:
1661
+ deps.bridgeRuntime.lifecycle.setCompactionInProgress,
1662
+ updateStatus: () => deps.updateStatus(compactCtx),
1663
+ dispatchNextQueuedTelegramTurn: () =>
1664
+ deps.dispatchNextQueuedTelegramTurn(compactCtx),
1665
+ requestDeferredDispatchNextQueuedTelegramTurn:
1666
+ deps.requestDeferredDispatchNextQueuedTelegramTurn
1667
+ ? (dispatch) =>
1668
+ deps.requestDeferredDispatchNextQueuedTelegramTurn?.(() =>
1669
+ dispatch(),
1670
+ )
1671
+ : undefined,
1672
+ compact: (callbacks) => deps.compact(compactCtx, callbacks),
1673
+ startTypingLoop: deps.startTypingLoop
1674
+ ? () =>
1675
+ deps.startTypingLoop?.(compactCtx, chatId, {
1676
+ target,
1677
+ })
1678
+ : undefined,
1679
+ stopTypingLoop: deps.stopTypingLoop,
1680
+ sendTextReply: (text) =>
1681
+ deps
1682
+ .sendTextReply(chatId, replyToMessageId, text, { target })
1683
+ .then(() => {}),
1684
+ suppressStartNotice: true,
1685
+ recordRuntimeEvent: deps.recordRuntimeEvent,
1686
+ });
1687
+ },
1688
+ });
1689
+ assertExecutionCurrent();
1690
+ if (handledByCompact) return;
1691
+ const handledByQueue = await deps.queueMenuCallbackHandler(query, ctx);
1692
+ assertExecutionCurrent();
1693
+ if (handledByQueue) return;
1694
+ const handledBySettings = await deps.settingsMenuCallbackHandler?.(
1695
+ query,
1696
+ ctx,
1697
+ );
1698
+ assertExecutionCurrent();
1699
+ if (handledBySettings) return;
1700
+ const callbackData = query.data;
1701
+ if (callbackData && !isTelegramOwnedCallbackData(callbackData)) {
1702
+ const chatId = query.message?.chat?.id;
1703
+ const messageId = query.message?.message_id;
1704
+ if (typeof chatId === "number" && typeof messageId === "number") {
1705
+ const queueOrder = deps.bridgeRuntime.queue.allocateItemOrder();
1706
+ const target =
1707
+ typeof query.message?.message_thread_id === "number"
1708
+ ? { chatId, threadId: query.message.message_thread_id }
1709
+ : { chatId };
1710
+ const admissionReceipts = createAdmissionReceipts("prompt", [query]);
1711
+ const turn: Queue.PendingTelegramTurn = {
1712
+ kind: "prompt",
1713
+ chatId,
1714
+ target,
1715
+ replyToMessageId: messageId,
1716
+ sourceMessageIds: [messageId],
1717
+ queueOrder,
1718
+ queueLane: "priority",
1719
+ laneOrder: queueOrder,
1720
+ queuedAttachments: [],
1721
+ content: [{ type: "text", text: `[callback] ${callbackData}` }],
1722
+ historyText: callbackData,
1723
+ statusSummary: callbackData,
1724
+ ...(admissionReceipts.length > 0 ? { admissionReceipts } : {}),
1725
+ };
1726
+ const result = Queue.appendTelegramPromptTurnOnce(
1727
+ deps.telegramQueueStore.getQueuedItems(),
1728
+ turn,
1729
+ );
1730
+ if (result.appended) {
1731
+ Updates.assertTelegramUpdateExecutionCurrent(query);
1732
+ deps.telegramQueueStore.setQueuedItems(result.items);
1733
+ reportQueueAdmission([query], admissionReceipts);
1734
+ deps.updateStatus(ctx);
1735
+ requestDispatchNextQueuedTelegramTurn(ctx);
1736
+ } else {
1737
+ reportQueueAdmission([query], admissionReceipts);
1738
+ }
1739
+ }
1740
+ await deps.answerCallbackQuery(query.id);
1741
+ return;
1742
+ }
1743
+ await menuCallbackHandler(query, ctx);
1744
+ };
1745
+ const promptTurnBuilder = Turns.createTelegramPromptTurnRuntimeBuilder<
1746
+ TMessage,
1747
+ TContext
1748
+ >({
1749
+ allocateQueueOrder: deps.bridgeRuntime.queue.allocateItemOrder,
1750
+ downloadFile: deps.downloadFile,
1751
+ processAttachments: deps.inboundHandlerRuntime.process,
1752
+ resolveTimeLine: deps.resolveTimeLine,
1753
+ getAllowedUserId: deps.configStore.getAllowedUserId,
1754
+ getAdmissionScope: deps.getAdmissionScope,
1755
+ getAdmissionJournalBinding: deps.getAdmissionJournalBinding,
1756
+ assertExecutionCurrent(message) {
1757
+ Updates.assertTelegramUpdateExecutionCurrent(message);
1758
+ },
1759
+
1760
+ // Voice policy resolves missing, invalid, and legacy manual config to hidden.
1761
+ getVoiceReplyMode: () => getTelegramVoiceReplyMode(deps.configStore.get()),
1762
+ getTelegramThreadLabel(message) {
1763
+ if (!deps.threadStore) return undefined;
1764
+ const chatId = message.chat.id;
1765
+ const threadId = message.message_thread_id;
1766
+ if (!threadId) return undefined;
1767
+ const localLabel = deps.getLocalThreadLabelForTarget?.({
1768
+ chatId,
1769
+ threadId,
1770
+ });
1771
+ if (localLabel) return localLabel;
1772
+ const records = deps.threadStore.list();
1773
+ const currentInstanceId = deps.getCurrentInstanceId?.();
1774
+ for (const r of records) {
1775
+ if (r.target.chatId !== chatId || r.target.threadId !== threadId)
1776
+ continue;
1777
+ if (
1778
+ currentInstanceId &&
1779
+ r.instanceId &&
1780
+ r.instanceId !== currentInstanceId
1781
+ )
1782
+ continue;
1783
+ return r.threadName &&
1784
+ Threads.isTelegramTopicThreadNameValidForSlot(r.threadName, r.slot)
1785
+ ? r.threadName
1786
+ : getRestoredThreadName(r, r.slot ?? "");
1787
+ }
1788
+ return undefined;
1789
+ },
1790
+ });
1791
+ const enqueueContinueTurn = async (
1792
+ message: TMessage,
1793
+ ctx: TContext,
1794
+ ): Promise<void> => {
1795
+ deps.bridgeRuntime.lifecycle.setFoldQueuedPromptsIntoHistory(false);
1796
+ const continueMessage = {
1797
+ ...message,
1798
+ text: "continue",
1799
+ caption: undefined,
1800
+ } as TMessage;
1801
+ const turn = await promptTurnBuilder([continueMessage], [], ctx);
1802
+ const continueTurn = {
1803
+ ...turn,
1804
+ queueLane: "control" as const,
1805
+ laneOrder: deps.bridgeRuntime.queue.allocateControlOrder(),
1806
+ statusSummary: "continue",
1807
+ };
1808
+ Updates.assertTelegramUpdateExecutionCurrent(message);
1809
+ deps.queueMutationRuntime.append(continueTurn, ctx);
1810
+ reportQueueAdmission(
1811
+ [continueMessage],
1812
+ continueTurn.admissionReceipts ?? [],
1813
+ );
1814
+ requestDispatchNextQueuedTelegramTurn(ctx);
1815
+ };
1816
+ const reservedCommandNames = () =>
1817
+ new Set(Commands.getTelegramReservedCommandNames());
1818
+ const getPromptTemplateCommands = () =>
1819
+ PromptTemplates.getTelegramPromptTemplateCommands(
1820
+ deps.getCommands(),
1821
+ reservedCommandNames(),
1822
+ );
1823
+ const commandHandler = Commands.createTelegramCommandHandlerTargetRuntime<
1824
+ TMessage,
1825
+ TContext
1826
+ >({
1827
+ assertExecutionCurrent(message) {
1828
+ Updates.assertTelegramUpdateExecutionCurrent(message);
1829
+ },
1830
+ hasAbortHandler: deps.bridgeRuntime.abort.hasHandler,
1831
+ clearPendingModelSwitch: deps.modelSwitchController.clearPendingSwitch,
1832
+ hasQueuedTelegramItems: deps.telegramQueueStore.hasQueuedItems,
1833
+ clearQueuedTelegramItems: deps.queueMutationRuntime.clear,
1834
+ setFoldQueuedPromptsIntoHistory:
1835
+ deps.bridgeRuntime.lifecycle.setFoldQueuedPromptsIntoHistory,
1836
+ abortCurrentTurn: deps.bridgeRuntime.abort.abortTurn,
1837
+ isIdle: deps.isIdle,
1838
+ hasPendingMessages: deps.hasPendingMessages,
1839
+ hasActiveTelegramTurn: deps.activeTurnRuntime.has,
1840
+ hasDispatchPending: deps.bridgeRuntime.lifecycle.hasDispatchPending,
1841
+ isCompactionInProgress: deps.bridgeRuntime.lifecycle.isCompactionInProgress,
1842
+ setCompactionInProgress:
1843
+ deps.bridgeRuntime.lifecycle.setCompactionInProgress,
1844
+ updateStatus: deps.updateStatus,
1845
+ isContextActive: deps.isContextActive,
1846
+ dispatchNextQueuedTelegramTurn: deps.dispatchNextQueuedTelegramTurn,
1847
+ requestDeferredDispatchNextQueuedTelegramTurn:
1848
+ deps.requestDeferredDispatchNextQueuedTelegramTurn,
1849
+ startTypingLoop: deps.startTypingLoop,
1850
+ stopTypingLoop: deps.stopTypingLoop,
1851
+ enqueueContinueTurn,
1852
+ compact: deps.compact,
1853
+ allocateItemOrder: deps.bridgeRuntime.queue.allocateItemOrder,
1854
+ allocateControlOrder: deps.bridgeRuntime.queue.allocateControlOrder,
1855
+ appendControlItem: deps.queueMutationRuntime.append,
1856
+ getAdmissionScope: deps.getAdmissionScope,
1857
+ getAdmissionJournalBinding: deps.getAdmissionJournalBinding,
1858
+ onControlQueued: (message, receipt) =>
1859
+ reportQueueAdmission([message], [receipt]),
1860
+ showStatus: deps.menuActions.sendStatusMessage,
1861
+ openModelMenu: deps.menuActions.openModelMenu,
1862
+ openThinkingMenu: (message, ctx) => {
1863
+ const chatId = (message as { chat: { id: number } }).chat.id;
1864
+ return deps.menuActions.openThinkingMenu(chatId, message.message_id, ctx);
1865
+ },
1866
+ openQueueMenu: (message, ctx) => {
1867
+ const chatId = (message as { chat: { id: number } }).chat.id;
1868
+ return deps.openQueueMenu(chatId, message.message_id, ctx);
1869
+ },
1870
+ openSettingsMenu: deps.openSettingsMenu,
1871
+ getAllowedUserId: deps.configStore.getAllowedUserId,
1872
+ setAllowedUserId: deps.configStore.setAllowedUserId,
1873
+ setMyCommands: deps.setMyCommands,
1874
+ getPromptTemplateCommands,
1875
+ persistConfig: deps.configStore.persist,
1876
+ sendTextReply: deps.sendTextReply,
1877
+ sendInteractiveMessage: deps.sendInteractiveMessage,
1878
+ recordRuntimeEvent: deps.recordRuntimeEvent,
1879
+ });
1880
+ const promptEnqueueController =
1881
+ Queue.createTelegramPromptEnqueueController<TMessage, TContext>({
1882
+ ...deps.telegramQueueStore,
1883
+ getFoldQueuedPromptsIntoHistory:
1884
+ deps.bridgeRuntime.lifecycle.shouldFoldQueuedPromptsIntoHistory,
1885
+ setFoldQueuedPromptsIntoHistory:
1886
+ deps.bridgeRuntime.lifecycle.setFoldQueuedPromptsIntoHistory,
1887
+ createTurn: async (messages, historyTurns, turnCtx) => {
1888
+ const turn = await promptTurnBuilder(messages, historyTurns, turnCtx);
1889
+ return turn.replyToMessageId > 0
1890
+ ? turn
1891
+ : { ...turn, replyToMessageId: 0 };
1892
+ },
1893
+ updateStatus: deps.updateStatus,
1894
+ dispatchNextQueuedTelegramTurn: requestDispatchNextQueuedTelegramTurn,
1895
+ assertExecutionCurrent: (messages) =>
1896
+ Updates.assertTelegramUpdateExecutionCurrent(messages[0]),
1897
+ });
1898
+ const promptEnqueue = async (
1899
+ messages: TMessage[],
1900
+ ctx: TContext,
1901
+ ): Promise<Queue.PendingTelegramTurn> => {
1902
+ return promptEnqueueController.enqueue(messages, ctx, (turn) => {
1903
+ reportQueueAdmission(messages, turn.admissionReceipts ?? []);
1904
+ });
1905
+ };
1906
+ const sendUnboundRerouteChooserNow = async (
1907
+ messages: TMessage[],
1908
+ ctx: TContext,
1909
+ reportDeferred = true,
1910
+ ): Promise<void> => {
1911
+ const message = messages[0];
1912
+ if (!message || !deps.threadStore) return;
1913
+ const records = deps.threadStore.list();
1914
+ const activeRecords = getTelegramRoutableThreadRecords(
1915
+ records,
1916
+ deps.getLiveThreadTargets?.(),
1917
+ );
1918
+ const sourceTarget =
1919
+ typeof message.message_thread_id === "number"
1920
+ ? { chatId: message.chat.id, threadId: message.message_thread_id }
1921
+ : undefined;
1922
+ const sourceKey = sourceTarget
1923
+ ? formatTelegramTargetKey(sourceTarget)
1924
+ : undefined;
1925
+ const includeGuidance = sourceKey
1926
+ ? !guidedUnboundTopicKeys.has(sourceKey)
1927
+ : true;
1928
+ if (sourceKey) guidedUnboundTopicKeys.add(sourceKey);
1929
+ if (activeRecords.length === 0) {
1930
+ await deps.sendTextReply(
1931
+ message.chat.id,
1932
+ message.message_id,
1933
+ [
1934
+ includeGuidance ? formatTelegramUnboundTopicGuidance() : undefined,
1935
+ "This thread is not bound to a Pi instance. Open an active Pi thread or run /telegram-connect from a Pi session to bind one.",
1936
+ ]
1937
+ .filter((line): line is string => typeof line === "string")
1938
+ .join("\n\n"),
1939
+ { parseMode: "HTML", target: sourceTarget },
1940
+ );
1941
+ return;
1942
+ }
1943
+ const rerouteId = storePendingUnboundReroute(messages);
1944
+ if (reportDeferred) {
1945
+ for (const source of messages) {
1946
+ Updates.reportTelegramUpdateDeferred(source);
1947
+ }
1948
+ }
1949
+ const text = formatTelegramUnboundRerouteChooserText(activeRecords, {
1950
+ includeGuidance,
1951
+ });
1952
+ const currentInstanceId = deps.getCurrentInstanceId?.();
1953
+ const replyMarkup = buildTelegramUnboundRerouteChooserMarkup(
1954
+ rerouteId,
1955
+ activeRecords,
1956
+ {
1957
+ currentLeaderProfileKey: getLeaderTopicProfileKey(
1958
+ ctx,
1959
+ currentInstanceId,
1960
+ ),
1961
+ currentInstanceId,
1962
+ },
1963
+ );
1964
+ if (deps.sendInteractiveMessage) {
1965
+ await deps.sendInteractiveMessage(
1966
+ message.chat.id,
1967
+ text,
1968
+ "html",
1969
+ replyMarkup,
1970
+ sourceTarget
1971
+ ? { target: sourceTarget, replyToMessageId: message.message_id }
1972
+ : { replyToMessageId: message.message_id },
1973
+ );
1974
+ return;
1975
+ }
1976
+ await deps.sendTextReply(message.chat.id, message.message_id, text, {
1977
+ parseMode: "HTML",
1978
+ target: sourceTarget,
1979
+ });
1980
+ };
1981
+ const sendUnboundRerouteChooser = async (
1982
+ message: TMessage,
1983
+ ctx: TContext,
1984
+ ): Promise<void> => {
1985
+ const groupKey = Media.getTelegramMediaGroupKey(message);
1986
+ if (!groupKey) {
1987
+ await sendUnboundRerouteChooserNow([message], ctx);
1988
+ return;
1989
+ }
1990
+ const existing = pendingUnboundRerouteMediaGroups.get(groupKey);
1991
+ if (existing) clearTimeout(existing.timer);
1992
+ const messages = [...(existing?.messages ?? []), message];
1993
+ const timer = setTimeout(() => {
1994
+ pendingUnboundRerouteMediaGroups.delete(groupKey);
1995
+ void sendUnboundRerouteChooserNow(messages, ctx, false);
1996
+ }, 1200);
1997
+ timer.unref?.();
1998
+ pendingUnboundRerouteMediaGroups.set(groupKey, { messages, timer });
1999
+ Updates.reportTelegramUpdateDeferred(message);
2000
+ };
2001
+ const getKnownTelegramAllTabCommand = (
2002
+ text: string,
2003
+ ): Commands.ParsedTelegramCommand | undefined => {
2004
+ const command = Commands.parseTelegramCommand(text);
2005
+ if (!command) return undefined;
2006
+ if (reservedCommandNames().has(command.name)) return command;
2007
+ if (Commands.findTelegramExtensionCommand(command.name)) return command;
2008
+ if (
2009
+ getPromptTemplateCommands().some(
2010
+ (template) => template.command === command.name,
2011
+ )
2012
+ ) {
2013
+ return command;
2014
+ }
2015
+ return undefined;
2016
+ };
2017
+ const sendAllTabCommandChooser = async (
2018
+ command: Commands.ParsedTelegramCommand,
2019
+ commandText: string,
2020
+ message: TMessage,
2021
+ options: {
2022
+ replyToSource?: boolean;
2023
+ target?: Queue.TelegramQueueTarget;
2024
+ } = {},
2025
+ ): Promise<boolean> => {
2026
+ if (!deps.threadStore) return false;
2027
+ const records = deps.threadStore.list();
2028
+ const activeRecords = getTelegramRoutableThreadRecords(
2029
+ records,
2030
+ deps.getLiveThreadTargets?.(),
2031
+ );
2032
+ if (activeRecords.length === 0) return false;
2033
+ const commandMessage = {
2034
+ ...message,
2035
+ text: commandText,
2036
+ caption: undefined,
2037
+ } as TMessage;
2038
+ const rerouteId = storePendingUnboundReroute([commandMessage], "command");
2039
+ Updates.reportTelegramUpdateDeferred(commandMessage);
2040
+ const text = formatTelegramAllTabMenuChooserText(command.name);
2041
+ const replyMarkup = buildTelegramUnboundRerouteChooserMarkup(
2042
+ rerouteId,
2043
+ activeRecords,
2044
+ );
2045
+ if (deps.sendInteractiveMessage) {
2046
+ await deps.sendInteractiveMessage(
2047
+ message.chat.id,
2048
+ text,
2049
+ "html",
2050
+ replyMarkup,
2051
+ options.target || options.replyToSource
2052
+ ? {
2053
+ ...(options.target ? { target: options.target } : {}),
2054
+ ...(options.replyToSource
2055
+ ? { replyToMessageId: message.message_id }
2056
+ : {}),
2057
+ }
2058
+ : undefined,
2059
+ );
2060
+ return true;
2061
+ }
2062
+ if (deps.callApi) {
2063
+ await deps.callApi("sendMessage", {
2064
+ chat_id: message.chat.id,
2065
+ text,
2066
+ parse_mode: "HTML",
2067
+ reply_markup: replyMarkup,
2068
+ ...(typeof options.target?.threadId === "number"
2069
+ ? { message_thread_id: options.target.threadId }
2070
+ : {}),
2071
+ ...(options.replyToSource
2072
+ ? {
2073
+ reply_parameters: {
2074
+ message_id: message.message_id,
2075
+ allow_sending_without_reply: true,
2076
+ },
2077
+ }
2078
+ : {}),
2079
+ });
2080
+ return true;
2081
+ }
2082
+ await deps.sendTextReply(message.chat.id, message.message_id, text, {
2083
+ parseMode: "HTML",
2084
+ target: options.target,
2085
+ });
2086
+ return true;
2087
+ };
2088
+ const commandOrPrompt = Commands.createTelegramCommandOrPromptRuntime<
2089
+ TMessage,
2090
+ TContext
2091
+ >({
2092
+ extractRawText: Media.extractFirstTelegramMessageText,
2093
+ assertExecutionCurrent(message) {
2094
+ Updates.assertTelegramUpdateExecutionCurrent(message);
2095
+ },
2096
+ shouldIgnoreMessages: (messages) =>
2097
+ !Media.hasTelegramMessagesPromptContent(messages),
2098
+ handleCommand: commandHandler,
2099
+ executeExtensionCommand: async (command, message, ctx) => {
2100
+ const extensionCommand = Commands.findTelegramExtensionCommand(
2101
+ command.name,
2102
+ );
2103
+ if (!extensionCommand) return false;
2104
+ const sourceTarget = Updates.getTelegramMessageTarget(message);
2105
+ const assertExecutionCurrent =
2106
+ Updates.createTelegramUpdateExecutionFenceGuard(message);
2107
+ try {
2108
+ assertExecutionCurrent();
2109
+ await extensionCommand.handler({
2110
+ name: command.name,
2111
+ args: command.args,
2112
+ reply: async (text) => {
2113
+ assertExecutionCurrent();
2114
+ await deps.sendTextReply(
2115
+ message.chat.id,
2116
+ message.message_id,
2117
+ text,
2118
+ { target: sourceTarget },
2119
+ );
2120
+ assertExecutionCurrent();
2121
+ },
2122
+ enqueuePrompt: async (prompt) => {
2123
+ assertExecutionCurrent();
2124
+ await promptEnqueue(
2125
+ [
2126
+ {
2127
+ ...message,
2128
+ text: prompt,
2129
+ caption: undefined,
2130
+ } as TMessage,
2131
+ ],
2132
+ ctx,
2133
+ );
2134
+ },
2135
+ });
2136
+ assertExecutionCurrent();
2137
+ } catch (error) {
2138
+ deps.recordRuntimeEvent?.("telegram-command", error, {
2139
+ command: command.name,
2140
+ });
2141
+ assertExecutionCurrent();
2142
+ await deps.sendTextReply(
2143
+ message.chat.id,
2144
+ message.message_id,
2145
+ "Command failed.",
2146
+ { target: sourceTarget },
2147
+ );
2148
+ }
2149
+ return true;
2150
+ },
2151
+ expandPromptTemplateCommand: (commandName, args) =>
2152
+ PromptTemplates.expandTelegramPromptTemplateCommand(
2153
+ commandName,
2154
+ args,
2155
+ getPromptTemplateCommands(),
2156
+ ),
2157
+ replaceMessageText: (message, text) =>
2158
+ ({ ...message, text, caption: undefined }) as TMessage,
2159
+ enqueueTurn: async (messages, ctx) => {
2160
+ await promptEnqueue(messages, ctx);
2161
+ },
2162
+ });
2163
+ dispatchReroutedCommandMessages = (messages, ctx) =>
2164
+ commandOrPrompt.dispatchMessages(messages, ctx);
2165
+ const mediaDispatch = Media.createTelegramMediaGroupDispatchRuntime<
2166
+ TMessage,
2167
+ TContext
2168
+ >({
2169
+ mediaGroups: deps.mediaGroupRuntime,
2170
+ dispatchMessages: commandOrPrompt.dispatchMessages,
2171
+ onDeferredMessage: Updates.reportTelegramUpdateDeferred,
2172
+ });
2173
+ const textDispatch = TextGroups.createTelegramTextGroupDispatchRuntime<
2174
+ TMessage,
2175
+ TContext
2176
+ >({
2177
+ textGroups: deps.textGroupRuntime,
2178
+ dispatchMessages: commandOrPrompt.dispatchMessages,
2179
+ dispatchSingleMessage: mediaDispatch.handleMessage,
2180
+ onDeferredMessage: Updates.reportTelegramUpdateDeferred,
2181
+ });
2182
+ const editRuntime = Turns.createTelegramQueuedPromptEditRuntime<
2183
+ TMessage,
2184
+ TContext
2185
+ >({
2186
+ ...deps.telegramQueueStore,
2187
+ updateStatus: deps.updateStatus,
2188
+ });
2189
+ const handleTelegramTopicLifecycleUpdate = async (
2190
+ lifecycle: Updates.TelegramTopicLifecycleUpdate<TMessage>,
2191
+ ctx: TContext,
2192
+ ): Promise<void> => {
2193
+ const assertExecutionCurrent =
2194
+ Updates.createTelegramUpdateExecutionFenceGuard(lifecycle.message);
2195
+ assertExecutionCurrent();
2196
+ await deps.handleTelegramTopicLifecycleUpdate?.(lifecycle, ctx);
2197
+ assertExecutionCurrent();
2198
+ if (lifecycle.kind !== "created" || !deps.threadStore) {
2199
+ return;
2200
+ }
2201
+ await deps.threadStore.load();
2202
+ assertExecutionCurrent();
2203
+ };
2204
+ const handleAuthorizedTelegramGuestMessage = async (
2205
+ guestMessage: Updates.TelegramGuestMessage & { from: TelegramUser },
2206
+ ctx: TContext,
2207
+ ): Promise<void> => {
2208
+ const assertExecutionCurrent =
2209
+ Updates.createTelegramUpdateExecutionFenceGuard(guestMessage);
2210
+ assertExecutionCurrent();
2211
+ const text = guestMessage.text ?? "";
2212
+ const gm = guestMessage as unknown as Record<string, unknown>;
2213
+ // Build telegram prefix with guest context
2214
+ const chatRaw = gm.chat as Record<string, unknown>;
2215
+ const chatType = chatRaw?.type as string;
2216
+ const fromRaw = gm.from as Record<string, unknown> | undefined;
2217
+ const replyMsg = gm.reply_to_message as Record<string, unknown> | undefined;
2218
+ const replyFromRaw = replyMsg?.from as Record<string, unknown> | undefined;
2219
+ const guestBotCallerUser = gm.guest_bot_caller_user as
2220
+ Record<string, unknown> | undefined;
2221
+ const guestBotCallerChat = gm.guest_bot_caller_chat as
2222
+ Record<string, unknown> | undefined;
2223
+ const ownerUserId = deps.configStore.getAllowedUserId();
2224
+ const replyPeer = formatTelegramPromptPeer(replyFromRaw);
2225
+ const guestPeer = resolveTelegramGuestPromptPeer({
2226
+ chatType,
2227
+ chat: chatRaw,
2228
+ from: fromRaw,
2229
+ replyFrom: replyFromRaw,
2230
+ guestBotCallerUser,
2231
+ guestBotCallerChat,
2232
+ ownerUserId,
2233
+ });
2234
+ const prefixParts = ["telegram"];
2235
+ if (guestPeer) {
2236
+ prefixParts.push(`guest:${guestPeer}`);
2237
+ } else if (chatType === "private") {
2238
+ deps.recordRuntimeEvent?.(
2239
+ "guest",
2240
+ new Error("Private Guest Mode remote peer could not be resolved"),
2241
+ {
2242
+ phase: "peer-attribution",
2243
+ chatId: typeof chatRaw?.id === "number" ? chatRaw.id : undefined,
2244
+ fromId: typeof fromRaw?.id === "number" ? fromRaw.id : undefined,
2245
+ hasReplyFrom: !!replyFromRaw,
2246
+ hasCallerUser: !!guestBotCallerUser,
2247
+ hasCallerChat: !!guestBotCallerChat,
2248
+ },
2249
+ );
2250
+ }
2251
+ const telegramPrefix = `[${prefixParts.join("|")}]`;
2252
+ // Extract reply context
2253
+ const replyText = replyMsg
2254
+ ? ((replyMsg.text as string) || (replyMsg.caption as string) || "").trim()
2255
+ : "";
2256
+ // Download files, run inbound handlers
2257
+ const guestMsg = guestMessage as unknown as Media.TelegramMediaMessage;
2258
+ const replyFiles = guestMsg.reply_to_message
2259
+ ? await Media.downloadTelegramMessageFiles(
2260
+ [guestMsg.reply_to_message as Media.TelegramMediaMessage],
2261
+ { downloadFile: deps.downloadFile },
2262
+ )
2263
+ : [];
2264
+ assertExecutionCurrent();
2265
+ const files = await Media.downloadTelegramMessageFiles([guestMsg], {
2266
+ downloadFile: deps.downloadFile,
2267
+ });
2268
+ assertExecutionCurrent();
2269
+ const processed = await deps.inboundHandlerRuntime.process(
2270
+ files,
2271
+ text,
2272
+ ctx,
2273
+ );
2274
+ assertExecutionCurrent();
2275
+ const rawText = processed.rawText || text;
2276
+ let sourceContext = "";
2277
+ if (replyMsg) {
2278
+ const replyHeader = replyPeer ? `[reply|from:${replyPeer}]` : "[reply]";
2279
+ const replyBlock = replyText
2280
+ ? `${replyHeader} ${replyText}`
2281
+ : replyHeader;
2282
+ sourceContext = appendTelegramSourceAttachmentSection(
2283
+ replyBlock,
2284
+ replyPeer,
2285
+ replyFiles,
2286
+ );
2287
+ }
2288
+ const promptText = Turns.buildTelegramTurnPrompt({
2289
+ telegramPrefix,
2290
+ rawText,
2291
+ files,
2292
+ promptFiles: processed.promptFiles,
2293
+ handlerOutputs: processed.handlerOutputs,
2294
+ sourceContext,
2295
+ });
2296
+ const order = deps.bridgeRuntime.queue.allocateItemOrder();
2297
+ const content: Queue.TelegramPromptContent[] = [
2298
+ { type: "text", text: promptText },
2299
+ ];
2300
+ for (const file of processed.promptFiles) {
2301
+ if (file.isImage && file.mimeType) {
2302
+ try {
2303
+ const buffer = await readFile(file.path);
2304
+ assertExecutionCurrent();
2305
+ content.push({
2306
+ type: "image",
2307
+ data: Buffer.from(buffer).toString("base64"),
2308
+ mimeType: file.mimeType,
2309
+ });
2310
+ } catch {
2311
+ // skip unreadable files
2312
+ }
2313
+ }
2314
+ }
2315
+ const admissionReceipts = createAdmissionReceipts("prompt", [guestMessage]);
2316
+ const guestTurn: Queue.PendingTelegramTurn = {
2317
+ kind: "prompt",
2318
+ chatId: 0,
2319
+ replyToMessageId: 0,
2320
+ guestQueryId: guestMessage.guest_query_id,
2321
+ sourceMessageIds: [],
2322
+ queueOrder: order,
2323
+ queueLane: "default",
2324
+ laneOrder: order,
2325
+ queuedAttachments: [],
2326
+ content,
2327
+ historyText: Turns.formatTelegramTurnStatusSummary(
2328
+ processed.rawText || text,
2329
+ processed.promptFiles,
2330
+ processed.handlerOutputs,
2331
+ ),
2332
+ statusSummary: Turns.truncateTelegramQueueSummary(
2333
+ processed.rawText || text,
2334
+ ),
2335
+ ...(admissionReceipts.length > 0 ? { admissionReceipts } : {}),
2336
+ };
2337
+ const items = deps.telegramQueueStore.getQueuedItems();
2338
+ Updates.assertTelegramUpdateExecutionCurrent(guestMessage);
2339
+ deps.telegramQueueStore.setQueuedItems(
2340
+ Queue.appendTelegramQueueItem(items, guestTurn),
2341
+ );
2342
+ reportQueueAdmission([guestMessage], admissionReceipts);
2343
+ deps.updateStatus(ctx);
2344
+ requestDispatchNextQueuedTelegramTurn(ctx);
2345
+ };
2346
+ return Updates.createTelegramPairedUpdateRuntime<TContext, TUpdate>({
2347
+ getAllowedUserId: deps.configStore.getAllowedUserId,
2348
+ getCurrentInstanceId: deps.getCurrentInstanceId,
2349
+ getMessageOwnership: deps.getMessageOwnership,
2350
+ getTargetOwnership: deps.getTargetOwnership,
2351
+ recordMessageOwnership: deps.recordMessageOwnership,
2352
+ handleTelegramTopicLifecycleUpdate,
2353
+ foreignOwnedUpdateForwarder: deps.foreignOwnedUpdateForwarder,
2354
+ setAllowedUserId: deps.configStore.setAllowedUserId,
2355
+ persistConfig: deps.configStore.persist,
2356
+ updateStatus: deps.updateStatus,
2357
+ removePendingMediaGroupMessages: deps.mediaGroupRuntime.removeMessages,
2358
+ flushPendingMediaGroupMessage: deps.mediaGroupRuntime.flushMessage,
2359
+ flushPendingTextGroupMessage: deps.textGroupRuntime.flushMessage,
2360
+ removeQueuedTelegramTurnsByMessageIds:
2361
+ deps.queueMutationRuntime.removeByMessageIds,
2362
+ applyQueuedTelegramTurnReactionByMessageId:
2363
+ deps.queueMutationRuntime.applyReactionByMessageId,
2364
+ answerCallbackQuery: deps.answerCallbackQuery,
2365
+ answerGuestQuery: deps.answerGuestQuery,
2366
+ handleAuthorizedTelegramCallbackQuery: callbackHandler,
2367
+ sendTextReply: deps.sendTextReply,
2368
+ handleAuthorizedTelegramMessage: async (message, ctx) => {
2369
+ const assertExecutionCurrent =
2370
+ Updates.createTelegramUpdateExecutionFenceGuard(message);
2371
+ assertExecutionCurrent();
2372
+ if (typeof message.message_thread_id === "number") {
2373
+ await deps.handleTelegramThreadTargetObserved?.(
2374
+ {
2375
+ chatId: message.chat.id,
2376
+ threadId: message.message_thread_id,
2377
+ },
2378
+ ctx,
2379
+ );
2380
+ assertExecutionCurrent();
2381
+ }
2382
+ const text = Media.extractFirstTelegramMessageText([
2383
+ message as TMessage,
2384
+ ]).trim();
2385
+ if (deps.threadStore && typeof message.message_thread_id !== "number") {
2386
+ await deps.threadStore.load();
2387
+ assertExecutionCurrent();
2388
+ if (deps.threadStore.getBotState().threadMode === "disabled") {
2389
+ await textDispatch.handleMessage(message as TMessage, ctx);
2390
+ return;
2391
+ }
2392
+ const records = deps.threadStore.list();
2393
+ const bindings = getTelegramRoutableThreadRecords(
2394
+ records,
2395
+ deps.getLiveThreadTargets?.(),
2396
+ );
2397
+ const command = getKnownTelegramAllTabCommand(text);
2398
+ if (bindings.length > 0 && command && command.name !== "thread") {
2399
+ if (
2400
+ await sendAllTabCommandChooser(command, text, message as TMessage, {
2401
+ replyToSource: true,
2402
+ })
2403
+ ) {
2404
+ return;
2405
+ }
2406
+ }
2407
+ if (bindings.length > 0 && !text.startsWith("/")) {
2408
+ const probeTarget = bindings[0]?.target;
2409
+ if (probeTarget?.threadId && deps.callApi) {
2410
+ try {
2411
+ await deps.callApi("sendChatAction", {
2412
+ chat_id: probeTarget.chatId,
2413
+ message_thread_id: probeTarget.threadId,
2414
+ action: "typing",
2415
+ });
2416
+ } catch (error) {
2417
+ if (
2418
+ Threads.isTelegramTopicModeUnavailableError(error) ||
2419
+ Threads.isTelegramTopicTargetStaleError(error)
2420
+ ) {
2421
+ deps.threadStore.setBotState({
2422
+ threadMode: "disabled",
2423
+ updatedAtMs: Date.now(),
2424
+ lastReconcileAction:
2425
+ "thread-mode-unavailable-threadless-prompt",
2426
+ });
2427
+ await deps.threadStore.persist();
2428
+ assertExecutionCurrent();
2429
+ await textDispatch.handleMessage(message as TMessage, ctx);
2430
+ return;
2431
+ }
2432
+ deps.recordRuntimeEvent?.("telegram", error, {
2433
+ phase: "threadless-topic-capability-check",
2434
+ chatId: probeTarget.chatId,
2435
+ threadId: probeTarget.threadId,
2436
+ });
2437
+ }
2438
+ }
2439
+ await deps.sendTextReply(
2440
+ message.chat.id,
2441
+ message.message_id,
2442
+ "This bot is in threaded multi-instance mode. Send prompts in a bound Pi thread tab so they route to the right instance.",
2443
+ );
2444
+ return;
2445
+ }
2446
+ }
2447
+ await textDispatch.handleMessage(message as TMessage, ctx);
2448
+ },
2449
+ handleAuthorizedTelegramEditedMessage: editRuntime.updateFromEditedMessage,
2450
+ handleAuthorizedTelegramGuestMessage,
2451
+ handleUnboundTelegramTopicMessage: async (message, ctx) => {
2452
+ const assertExecutionCurrent =
2453
+ Updates.createTelegramUpdateExecutionFenceGuard(message);
2454
+ assertExecutionCurrent();
2455
+ if (!deps.threadStore) {
2456
+ await textDispatch.handleMessage(message as TMessage, ctx);
2457
+ return;
2458
+ }
2459
+ await deps.threadStore.load();
2460
+ assertExecutionCurrent();
2461
+ if (deps.threadStore.getBotState().threadMode === "disabled") {
2462
+ await textDispatch.handleMessage(message as TMessage, ctx);
2463
+ return;
2464
+ }
2465
+ const target = Updates.getTelegramMessageTarget(message);
2466
+ if (!target?.threadId) {
2467
+ await textDispatch.handleMessage(message as TMessage, ctx);
2468
+ return;
2469
+ }
2470
+ const text = Media.extractFirstTelegramMessageText([
2471
+ message as TMessage,
2472
+ ]).trim();
2473
+ const instanceId = deps.getCurrentInstanceId?.();
2474
+ const leaderProfileKey = getLeaderTopicProfileKey(ctx, instanceId);
2475
+ const records = deps.threadStore.list();
2476
+ const routableRecords = getTelegramRoutableThreadRecords(
2477
+ records,
2478
+ deps.getLiveThreadTargets?.(),
2479
+ );
2480
+ const hasAnyRoutableThread = routableRecords.length > 0;
2481
+ const existing = records.find((r) => {
2482
+ return (
2483
+ r.target.chatId === target.chatId &&
2484
+ r.target.threadId === target.threadId
2485
+ );
2486
+ });
2487
+ if (existing) {
2488
+ const isLeaderTopic =
2489
+ (instanceId && existing.instanceId === instanceId) ||
2490
+ (!!leaderProfileKey && existing.profileKey === leaderProfileKey);
2491
+ if (existing.status === "active" && isLeaderTopic) {
2492
+ if (typeof existing.rerouteConfirmedAtMs !== "number") {
2493
+ const nowMs = Date.now();
2494
+ deps.threadStore.upsert({
2495
+ ...existing,
2496
+ updatedAtMs: nowMs,
2497
+ rerouteConfirmedAtMs: nowMs,
2498
+ });
2499
+ await deps.threadStore.persist();
2500
+ assertExecutionCurrent();
2501
+ }
2502
+ await textDispatch.handleMessage(message as TMessage, ctx);
2503
+ return;
2504
+ }
2505
+ if (existing.status === "starting") {
2506
+ await deps.sendTextReply(
2507
+ target.chatId,
2508
+ message.message_id,
2509
+ "Instance " +
2510
+ getTelegramThreadRecordLabel(existing) +
2511
+ " is starting. Please wait…",
2512
+ { target },
2513
+ );
2514
+ return;
2515
+ }
2516
+ if (existing.status === "active") {
2517
+ await deps.sendTextReply(
2518
+ target.chatId,
2519
+ message.message_id,
2520
+ "Instance " +
2521
+ getTelegramThreadRecordLabel(existing) +
2522
+ " is not connected to the Telegram bus yet. Run /telegram-connect in that Pi instance; keeping this thread.",
2523
+ { target },
2524
+ );
2525
+ return;
2526
+ }
2527
+ if (
2528
+ (existing.status === "stale" || existing.status === "offline") &&
2529
+ leaderProfileKey &&
2530
+ !hasActiveLeaderTopic(
2531
+ deps.threadStore.list(),
2532
+ leaderProfileKey,
2533
+ instanceId,
2534
+ ) &&
2535
+ !hasAnyRoutableThread
2536
+ ) {
2537
+ const priorLeaderRecord =
2538
+ deps.threadStore.getByProfileKey(leaderProfileKey);
2539
+ const slot =
2540
+ deps.threadStore.allocateSlot(
2541
+ leaderProfileKey,
2542
+ priorLeaderRecord?.slot ?? existing.slot,
2543
+ ) ??
2544
+ priorLeaderRecord?.slot ??
2545
+ existing.slot ??
2546
+ "A";
2547
+ const threadName =
2548
+ priorLeaderRecord?.threadName ??
2549
+ existing.threadName ??
2550
+ Threads.chooseTelegramThreadName({ slot }) ??
2551
+ "Pi";
2552
+ deps.threadStore.upsert({
2553
+ profileKey: leaderProfileKey,
2554
+ owner: {
2555
+ kind: "leader",
2556
+ cwd:
2557
+ typeof (ctx as { cwd?: unknown }).cwd === "string"
2558
+ ? (ctx as { cwd?: string }).cwd
2559
+ : undefined,
2560
+ instanceId,
2561
+ },
2562
+ target: { chatId: target.chatId, threadId: target.threadId },
2563
+ status: "active",
2564
+ createdAtMs: priorLeaderRecord?.createdAtMs ?? existing.createdAtMs,
2565
+ updatedAtMs: Date.now(),
2566
+ threadName,
2567
+ instanceId,
2568
+ slot,
2569
+ });
2570
+ await deps.threadStore.persist();
2571
+ assertExecutionCurrent();
2572
+ deps.setCurrentLeaderIdentity?.({
2573
+ target: { chatId: target.chatId, threadId: target.threadId },
2574
+ slot,
2575
+ threadName,
2576
+ });
2577
+ deps.recordRuntimeEvent?.(
2578
+ "bus",
2579
+ "Bus leader reclaimed unbound thread",
2580
+ {
2581
+ phase: "leader-topic-reclaim",
2582
+ chatId: target.chatId,
2583
+ threadId: target.threadId,
2584
+ slot,
2585
+ profileKey: leaderProfileKey,
2586
+ },
2587
+ );
2588
+ await textDispatch.handleMessage(message as TMessage, ctx);
2589
+ return;
2590
+ }
2591
+ await deps.sendTextReply(
2592
+ target.chatId,
2593
+ message.message_id,
2594
+ "Topic " +
2595
+ (existing.slot ?? "?") +
2596
+ " is " +
2597
+ existing.status +
2598
+ ". Start a Pi instance to claim it.",
2599
+ { target },
2600
+ );
2601
+ return;
2602
+ }
2603
+ const deletedObservation = deps.threadStore
2604
+ .listSyncObservations()
2605
+ .find(
2606
+ (observation) =>
2607
+ observation.syncStatus === "deleted" &&
2608
+ observation.target.chatId === target.chatId &&
2609
+ observation.target.threadId === target.threadId,
2610
+ );
2611
+ if (deletedObservation) {
2612
+ deps.recordRuntimeEvent?.(
2613
+ "inbound-worker",
2614
+ "Discarded update from a confirmed deleted Telegram thread",
2615
+ {
2616
+ phase: "discard-deleted-thread",
2617
+ chatId: target.chatId,
2618
+ threadId: target.threadId,
2619
+ messageId: message.message_id,
2620
+ },
2621
+ );
2622
+ return;
2623
+ }
2624
+ const reservations = deps.threadStore.listReservations();
2625
+ const reservation = reservations.find(
2626
+ (reservation) =>
2627
+ reservation.target.chatId === target.chatId &&
2628
+ reservation.target.threadId === target.threadId,
2629
+ );
2630
+ if (reservation) {
2631
+ await deps.sendTextReply(
2632
+ target.chatId,
2633
+ message.message_id,
2634
+ "Previous leader thread (" +
2635
+ (reservation.slot ?? "?") +
2636
+ "). Closing and deleting this old topic. Use the current thread tab instead.",
2637
+ { target },
2638
+ );
2639
+ await deleteReservedTelegramTopicThroughReconciler(
2640
+ deps,
2641
+ { chatId: target.chatId, threadId: target.threadId },
2642
+ message.message_id,
2643
+ );
2644
+ return;
2645
+ }
2646
+ const command = getKnownTelegramAllTabCommand(text);
2647
+ if (command && hasAnyRoutableThread) {
2648
+ if (
2649
+ await sendAllTabCommandChooser(command, text, message as TMessage, {
2650
+ target: { chatId: target.chatId, threadId: target.threadId },
2651
+ replyToSource: true,
2652
+ })
2653
+ ) {
2654
+ return;
2655
+ }
2656
+ }
2657
+ if (leaderProfileKey && deps.callApi) {
2658
+ const currentLeaderRecord = records.find((record) => {
2659
+ if (record.status !== "active") return false;
2660
+ if (instanceId && record.instanceId === instanceId) return true;
2661
+ return record.profileKey === leaderProfileKey;
2662
+ });
2663
+ if (
2664
+ currentLeaderRecord &&
2665
+ (currentLeaderRecord.target.chatId !== target.chatId ||
2666
+ currentLeaderRecord.target.threadId !== target.threadId)
2667
+ ) {
2668
+ let currentLeaderIsStale = false;
2669
+ try {
2670
+ await deps.callApi("sendChatAction", {
2671
+ chat_id: currentLeaderRecord.target.chatId,
2672
+ message_thread_id: currentLeaderRecord.target.threadId,
2673
+ action: "typing",
2674
+ });
2675
+ } catch (error) {
2676
+ currentLeaderIsStale =
2677
+ Threads.isTelegramTopicTargetStaleError(error);
2678
+ if (!currentLeaderIsStale) throw error;
2679
+ }
2680
+ if (currentLeaderIsStale) {
2681
+ deps.threadStore.markStaleByTarget(
2682
+ currentLeaderRecord.target,
2683
+ "deleted",
2684
+ "Current leader thread is stale during unbound prompt routing.",
2685
+ );
2686
+ const slot = currentLeaderRecord.slot ?? "A";
2687
+ const threadName = getRestoredThreadName(currentLeaderRecord, slot);
2688
+ deps.threadStore.upsert({
2689
+ ...currentLeaderRecord,
2690
+ profileKey: leaderProfileKey,
2691
+ owner: {
2692
+ kind: "leader",
2693
+ cwd:
2694
+ typeof (ctx as { cwd?: unknown }).cwd === "string"
2695
+ ? (ctx as { cwd?: string }).cwd
2696
+ : undefined,
2697
+ instanceId,
2698
+ },
2699
+ target: { chatId: target.chatId, threadId: target.threadId },
2700
+ status: "active",
2701
+ updatedAtMs: Date.now(),
2702
+ threadName,
2703
+ instanceId,
2704
+ slot,
2705
+ });
2706
+ await deps.threadStore.persist();
2707
+ assertExecutionCurrent();
2708
+ deps.setCurrentLeaderIdentity?.({
2709
+ target: { chatId: target.chatId, threadId: target.threadId },
2710
+ slot,
2711
+ threadName,
2712
+ });
2713
+ deps.recordRuntimeEvent?.(
2714
+ "bus",
2715
+ "Bus leader reclaimed stale-current unbound thread",
2716
+ {
2717
+ phase: "leader-topic-unbound-stale-reclaim",
2718
+ chatId: target.chatId,
2719
+ threadId: target.threadId,
2720
+ staleThreadId: currentLeaderRecord.target.threadId,
2721
+ slot,
2722
+ profileKey: leaderProfileKey,
2723
+ },
2724
+ );
2725
+ await textDispatch.handleMessage(message as TMessage, ctx);
2726
+ return;
2727
+ }
2728
+ }
2729
+ }
2730
+ if (
2731
+ leaderProfileKey &&
2732
+ !hasActiveLeaderTopic(records, leaderProfileKey, instanceId) &&
2733
+ !hasAnyRoutableThread
2734
+ ) {
2735
+ const priorLeaderRecord =
2736
+ deps.threadStore.getByProfileKey(leaderProfileKey);
2737
+ const priorLeaderIdentity =
2738
+ deps.threadStore.getIdentityByProfileKey(leaderProfileKey);
2739
+ const slot =
2740
+ deps.threadStore.allocateSlot(
2741
+ leaderProfileKey,
2742
+ priorLeaderRecord?.slot ?? priorLeaderIdentity?.slot,
2743
+ ) ??
2744
+ priorLeaderRecord?.slot ??
2745
+ priorLeaderIdentity?.slot ??
2746
+ "A";
2747
+ const identityThreadName =
2748
+ priorLeaderIdentity?.threadName &&
2749
+ Threads.isTelegramTopicThreadNameValidForSlot(
2750
+ priorLeaderIdentity.threadName,
2751
+ slot,
2752
+ )
2753
+ ? priorLeaderIdentity.threadName
2754
+ : undefined;
2755
+ const threadName =
2756
+ priorLeaderRecord?.threadName ??
2757
+ identityThreadName ??
2758
+ Threads.chooseTelegramThreadName({ slot }) ??
2759
+ "Pi";
2760
+ deps.threadStore.upsert({
2761
+ profileKey: leaderProfileKey,
2762
+ owner: {
2763
+ kind: "leader",
2764
+ cwd:
2765
+ typeof (ctx as { cwd?: unknown }).cwd === "string"
2766
+ ? (ctx as { cwd?: string }).cwd
2767
+ : undefined,
2768
+ instanceId,
2769
+ },
2770
+ target: { chatId: target.chatId, threadId: target.threadId },
2771
+ status: "active",
2772
+ createdAtMs: priorLeaderRecord?.createdAtMs ?? Date.now(),
2773
+ updatedAtMs: Date.now(),
2774
+ threadName,
2775
+ instanceId,
2776
+ slot,
2777
+ });
2778
+ await deps.threadStore.persist();
2779
+ assertExecutionCurrent();
2780
+ deps.setCurrentLeaderIdentity?.({
2781
+ target: { chatId: target.chatId, threadId: target.threadId },
2782
+ slot,
2783
+ threadName,
2784
+ });
2785
+ deps.recordRuntimeEvent?.(
2786
+ "bus",
2787
+ "Bus leader reclaimed unbound thread",
2788
+ {
2789
+ phase: "leader-topic-reclaim",
2790
+ chatId: target.chatId,
2791
+ threadId: target.threadId,
2792
+ slot,
2793
+ profileKey: leaderProfileKey,
2794
+ },
2795
+ );
2796
+ await textDispatch.handleMessage(message as TMessage, ctx);
2797
+ return;
2798
+ }
2799
+ await sendUnboundRerouteChooser(message as TMessage, ctx);
2800
+ return;
2801
+ },
2802
+ });
2803
+ }
2804
+
2805
+ // --- Assistant Output Delivery Authority ---
2806
+
2807
+ export interface TelegramAssistantOutputAuthority<TTransportStamp> {
2808
+ transportStamp: TTransportStamp;
2809
+ route: "direct" | "follower" | "none";
2810
+ directEpoch?: number | string;
2811
+ followerGeneration?: string;
2812
+ target?: Queue.TelegramQueueTarget;
2813
+ }
2814
+
2815
+ export interface TelegramAssistantOutputAuthorityRuntime<TTransportStamp> {
2816
+ captureAuthority: () => TelegramAssistantOutputAuthority<TTransportStamp>;
2817
+ isAuthorityActive: (
2818
+ authority: TelegramAssistantOutputAuthority<TTransportStamp>,
2819
+ ) => boolean;
2820
+ canDeliver: () => boolean;
2821
+ }
2822
+
2823
+ export function createTelegramAssistantOutputAuthorityRuntime<
2824
+ TTransportStamp,
2825
+ >(deps: {
2826
+ getPreferredTarget: () => Queue.TelegramQueueTarget | undefined;
2827
+ getFallbackChatId: () => number | undefined;
2828
+ getTransportStamp: () => TTransportStamp;
2829
+ isTransportStampActive: (stamp: TTransportStamp) => boolean;
2830
+ ownsDirect: () => boolean;
2831
+ getDirectEpoch: () => number | string | undefined;
2832
+ isFollowerRegistered: () => boolean;
2833
+ getFollowerGeneration: () => string | undefined;
2834
+ }): TelegramAssistantOutputAuthorityRuntime<TTransportStamp> {
2835
+ const getCurrentTarget = (): Queue.TelegramQueueTarget | undefined => {
2836
+ const preferred = deps.getPreferredTarget();
2837
+ if (preferred) return { ...preferred };
2838
+ const chatId = deps.getFallbackChatId();
2839
+ return chatId === undefined ? undefined : { chatId };
2840
+ };
2841
+ return {
2842
+ captureAuthority() {
2843
+ const target = getCurrentTarget();
2844
+ const directEpoch = deps.ownsDirect() ? deps.getDirectEpoch() : undefined;
2845
+ const followerGeneration = deps.isFollowerRegistered()
2846
+ ? deps.getFollowerGeneration()
2847
+ : undefined;
2848
+ return {
2849
+ transportStamp: deps.getTransportStamp(),
2850
+ route:
2851
+ directEpoch !== undefined
2852
+ ? "direct"
2853
+ : followerGeneration !== undefined
2854
+ ? "follower"
2855
+ : "none",
2856
+ directEpoch,
2857
+ followerGeneration,
2858
+ target,
2859
+ };
2860
+ },
2861
+ isAuthorityActive(authority) {
2862
+ if (!deps.isTransportStampActive(authority.transportStamp)) return false;
2863
+ const target = getCurrentTarget();
2864
+ if (
2865
+ authority.target === undefined ||
2866
+ target?.chatId !== authority.target.chatId ||
2867
+ target?.threadId !== authority.target.threadId
2868
+ ) {
2869
+ return false;
2870
+ }
2871
+ if (authority.route === "direct") {
2872
+ return (
2873
+ deps.ownsDirect() && deps.getDirectEpoch() === authority.directEpoch
2874
+ );
2875
+ }
2876
+ if (authority.route === "follower") {
2877
+ return (
2878
+ !deps.ownsDirect() &&
2879
+ deps.isFollowerRegistered() &&
2880
+ deps.getFollowerGeneration() === authority.followerGeneration
2881
+ );
2882
+ }
2883
+ return false;
2884
+ },
2885
+ canDeliver() {
2886
+ return deps.ownsDirect() || deps.isFollowerRegistered();
2887
+ },
2888
+ };
2889
+ }