@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,3075 @@
1
+ /**
2
+ * Telegram queue core contracts and pure planning helpers
3
+ * Zones: telegram queue, pi agent lifecycle, scheduling
4
+ * Owns queue item contracts, lane admission, pure queue mutations, and dispatch planning
5
+ */
6
+
7
+ import { createHash } from "node:crypto";
8
+
9
+ import { isVoiceTurn } from "./voice.ts";
10
+
11
+ // --- Queue Items ---
12
+
13
+ export interface QueuedAttachment {
14
+ path: string;
15
+ fileName: string;
16
+ }
17
+
18
+ export interface TelegramPromptTextContent {
19
+ type: "text";
20
+ text: string;
21
+ }
22
+
23
+ export interface TelegramPromptImageContent {
24
+ type: "image";
25
+ data: string;
26
+ mimeType: string;
27
+ }
28
+
29
+ export type TelegramPromptContent =
30
+ TelegramPromptTextContent | TelegramPromptImageContent;
31
+
32
+ export type TelegramQueueItemKind = "prompt" | "control";
33
+ export type TelegramQueueLane = "control" | "priority" | "default";
34
+ export type TelegramQueueReactionDisposition =
35
+ | { kind: "default" }
36
+ | { kind: "priority"; emoji: string }
37
+ | { kind: "suppressed"; emoji: string }
38
+ | {
39
+ kind: "priority-suppressed";
40
+ priorityEmoji: string;
41
+ suppressionEmoji: string;
42
+ }
43
+ | {
44
+ kind: "reaction-transition";
45
+ priorityEmoji?: string | null;
46
+ suppressionEmoji?: string | null;
47
+ };
48
+
49
+ export interface TelegramQueueAdmissionReceipt {
50
+ queueKind: TelegramQueueItemKind;
51
+ receiptId: string;
52
+ sourceUpdateIds: readonly number[];
53
+ journalBindingKey?: string;
54
+ }
55
+ export type TelegramQueueAdmissionMode =
56
+ "control-queue" | "priority-queue" | "default-queue";
57
+
58
+ export interface TelegramQueueLaneContract {
59
+ lane: TelegramQueueLane;
60
+ admissionMode: TelegramQueueAdmissionMode;
61
+ dispatchRank: number;
62
+ allowedKinds: readonly TelegramQueueItemKind[];
63
+ }
64
+
65
+ export const TELEGRAM_QUEUE_LANE_CONTRACTS: readonly TelegramQueueLaneContract[] =
66
+ [
67
+ // Control lane intentionally accepts both direct controls and resume prompts.
68
+ // Model-switch continuations need prompt semantics but must run before queued user work.
69
+ // Do not admit ordinary user prompts here without an explicit control-flow reason.
70
+ {
71
+ lane: "control",
72
+ admissionMode: "control-queue",
73
+ dispatchRank: 0,
74
+ allowedKinds: ["control", "prompt"],
75
+ },
76
+ {
77
+ lane: "priority",
78
+ admissionMode: "priority-queue",
79
+ dispatchRank: 1,
80
+ allowedKinds: ["prompt"],
81
+ },
82
+ {
83
+ lane: "default",
84
+ admissionMode: "default-queue",
85
+ dispatchRank: 2,
86
+ allowedKinds: ["prompt"],
87
+ },
88
+ ] as const;
89
+
90
+ export interface TelegramQueueTarget {
91
+ chatId: number;
92
+ threadId?: number;
93
+ }
94
+
95
+ export interface TelegramTransportStamp {
96
+ profile: string;
97
+ generation: string;
98
+ }
99
+
100
+ export interface TelegramTransportStampRuntime {
101
+ getStamp(): TelegramTransportStamp;
102
+ isActive(stamp: TelegramTransportStamp | undefined): boolean;
103
+ }
104
+
105
+ export interface TelegramQueueItemBase {
106
+ kind: TelegramQueueItemKind;
107
+ chatId: number;
108
+ target?: TelegramQueueTarget;
109
+ transportStamp?: TelegramTransportStamp;
110
+ replyToMessageId: number;
111
+ guestQueryId?: string;
112
+ queueOrder: number;
113
+ queueLane: TelegramQueueLane;
114
+ laneOrder: number;
115
+ statusSummary: string;
116
+ admissionReceipts?: TelegramQueueAdmissionReceipt[];
117
+ }
118
+
119
+ export interface PendingTelegramTurn extends TelegramQueueItemBase {
120
+ kind: "prompt";
121
+ sourceMessageIds: number[];
122
+ queuedAttachments: QueuedAttachment[];
123
+ content: TelegramPromptContent[];
124
+ historyText: string;
125
+ priorityEmoji?: string;
126
+ reactionSuppressionEmoji?: string;
127
+
128
+ /** Turn should preferably be delivered as voice (mirror mode + user sent voice) */
129
+ voiceReplyPreferred?: boolean;
130
+ /** Turn must be delivered as voice (voice mode) */
131
+ voiceReplyRequired?: boolean;
132
+ }
133
+
134
+ export interface PendingTelegramControlItem<
135
+ TContext = unknown,
136
+ > extends TelegramQueueItemBase {
137
+ kind: "control";
138
+ controlType: "status" | "model";
139
+ execute: (ctx: TContext) => Promise<void>;
140
+ }
141
+
142
+ export type TelegramQueueItem<TContext = unknown> =
143
+ PendingTelegramTurn | PendingTelegramControlItem<TContext>;
144
+
145
+ export const TELEGRAM_QUEUE_HANDOFF_PAYLOAD_MAX_BYTES = 8 * 1024 * 1024;
146
+ export const TELEGRAM_QUEUE_HANDOFF_MAX_RECEIPTS = 256;
147
+
148
+ export interface TelegramQueueHandoffBase {
149
+ chatId: number;
150
+ target?: TelegramQueueTarget;
151
+ transportStamp?: TelegramTransportStamp;
152
+ replyToMessageId: number;
153
+ guestQueryId?: string;
154
+ queueOrder: number;
155
+ queueLane: TelegramQueueLane;
156
+ laneOrder: number;
157
+ statusSummary: string;
158
+ admissionReceipts: TelegramQueueAdmissionReceipt[];
159
+ }
160
+
161
+ export interface TelegramPromptQueueHandoffPayload
162
+ extends TelegramQueueHandoffBase {
163
+ kind: "prompt";
164
+ sourceMessageIds: number[];
165
+ queuedAttachments: QueuedAttachment[];
166
+ content: TelegramPromptContent[];
167
+ historyText: string;
168
+ priorityEmoji?: string;
169
+ reactionSuppressionEmoji?: string;
170
+ voiceReplyPreferred?: boolean;
171
+ voiceReplyRequired?: boolean;
172
+ }
173
+
174
+ export interface TelegramControlQueueHandoffPayload
175
+ extends TelegramQueueHandoffBase {
176
+ kind: "control";
177
+ controlType: PendingTelegramControlItem<unknown>["controlType"];
178
+ }
179
+
180
+ export type TelegramQueueHandoffPayload =
181
+ | TelegramPromptQueueHandoffPayload
182
+ | TelegramControlQueueHandoffPayload;
183
+
184
+ export interface TelegramQueueHandoff {
185
+ handoffToken: string;
186
+ payload: TelegramQueueHandoffPayload;
187
+ }
188
+
189
+ export interface TelegramQueueHandoffStageReceipt {
190
+ status: "staged";
191
+ receiptId: string;
192
+ sourceUpdateIds: readonly number[];
193
+ }
194
+
195
+ export interface TelegramQueueHandoffAcceptedOwner {
196
+ instanceId: string;
197
+ processId: number;
198
+ processBirthId: string;
199
+ sessionGeneration: number;
200
+ acquisitionId: string;
201
+ acquiredAtMs: number;
202
+ handoffId?: string;
203
+ }
204
+
205
+ export interface TelegramQueueHandoffStageResult
206
+ extends TelegramQueueHandoffStageReceipt {
207
+ queueOwner: TelegramQueueHandoffAcceptedOwner;
208
+ }
209
+
210
+ export interface TelegramQueueHandoffStagingRuntime {
211
+ stage: (payload: TelegramQueueHandoffPayload) => TelegramQueueHandoffStageReceipt;
212
+ accept: (receipt: TelegramQueueAdmissionReceipt) => boolean;
213
+ cancel: (receipt: TelegramQueueAdmissionReceipt) => boolean;
214
+ hasStaged: (receipt: TelegramQueueAdmissionReceipt) => boolean;
215
+ }
216
+
217
+ export interface TelegramQueueStore<TContext = unknown> {
218
+ getQueuedItems: () => TelegramQueueItem<TContext>[];
219
+ setQueuedItems: (items: TelegramQueueItem<TContext>[]) => void;
220
+ }
221
+
222
+ export interface TelegramQueueStateStore<
223
+ TContext = unknown,
224
+ > extends TelegramQueueStore<TContext> {
225
+ hasQueuedItems: () => boolean;
226
+ }
227
+
228
+ export interface TelegramActiveTurnStore<
229
+ TTurn extends PendingTelegramTurn = PendingTelegramTurn,
230
+ > {
231
+ get: () => TTurn | undefined;
232
+ has: () => boolean;
233
+ set: (turn: TTurn) => void;
234
+ clear: () => void;
235
+ getChatId: () => number | undefined;
236
+ getTarget: () => TelegramQueueTarget | undefined;
237
+ getReplyToMessageId: () => number | undefined;
238
+ getGuestQueryId: () => string | undefined;
239
+ getSourceMessageIds: () => number[] | undefined;
240
+ }
241
+
242
+ export interface TelegramDispatchGuardState {
243
+ compactionInProgress: boolean;
244
+ hasActiveTelegramTurn: boolean;
245
+ hasPendingTelegramDispatch: boolean;
246
+ isIdle: boolean;
247
+ hasPendingMessages: boolean;
248
+ }
249
+
250
+ export function createTelegramQueueAdmissionReceipt(options: {
251
+ queueKind: TelegramQueueItemKind;
252
+ scope: string;
253
+ sourceUpdateIds: readonly number[];
254
+ }): TelegramQueueAdmissionReceipt | undefined {
255
+ if (options.sourceUpdateIds.length === 0) return undefined;
256
+ const scope = options.scope.trim();
257
+ if (!scope) {
258
+ throw new Error("Telegram queue admission receipt scope is required.");
259
+ }
260
+ if (options.queueKind !== "prompt" && options.queueKind !== "control") {
261
+ throw new Error("Telegram queue admission receipt kind is invalid.");
262
+ }
263
+ const sourceUpdateIds = [...new Set(options.sourceUpdateIds)].sort(
264
+ (left, right) => left - right,
265
+ );
266
+ if (
267
+ sourceUpdateIds.some(
268
+ (updateId) => !Number.isSafeInteger(updateId) || updateId < 0,
269
+ )
270
+ ) {
271
+ throw new Error("Telegram queue admission update ids must be safe integers.");
272
+ }
273
+ const digest = createHash("sha256")
274
+ .update(
275
+ JSON.stringify({
276
+ scope,
277
+ queueKind: options.queueKind,
278
+ sourceUpdateIds,
279
+ }),
280
+ )
281
+ .digest("hex");
282
+ return {
283
+ queueKind: options.queueKind,
284
+ receiptId: `telegram-${options.queueKind}-v1-${digest}`,
285
+ sourceUpdateIds,
286
+ journalBindingKey: scope,
287
+ };
288
+ }
289
+
290
+ export function isTelegramQueueItemDurablyAdmitted<TContext = unknown>(
291
+ item: TelegramQueueItem<TContext>,
292
+ isReceiptCommitted: (receipt: TelegramQueueAdmissionReceipt) => boolean,
293
+ ): boolean {
294
+ assertTelegramQueueItemAdmissionValid(item);
295
+ return (item.admissionReceipts ?? []).every(isReceiptCommitted);
296
+ }
297
+
298
+ export function getTelegramQueueLaneContract(
299
+ lane: TelegramQueueLane,
300
+ ): TelegramQueueLaneContract {
301
+ const contract = TELEGRAM_QUEUE_LANE_CONTRACTS.find(
302
+ (entry) => entry.lane === lane,
303
+ );
304
+ if (!contract) throw new Error(`Unknown Telegram queue lane: ${lane}`);
305
+ return contract;
306
+ }
307
+
308
+ export function getTelegramQueueItemAdmissionMode(
309
+ item: Pick<TelegramQueueItem, "queueLane">,
310
+ ): TelegramQueueAdmissionMode {
311
+ return getTelegramQueueLaneContract(item.queueLane).admissionMode;
312
+ }
313
+
314
+ export function isTelegramQueueItemAdmissionValid(
315
+ item: Pick<TelegramQueueItem, "kind" | "queueLane">,
316
+ ): boolean {
317
+ return getTelegramQueueLaneContract(item.queueLane).allowedKinds.includes(
318
+ item.kind,
319
+ );
320
+ }
321
+
322
+ export function assertTelegramQueueItemAdmissionValid(
323
+ item: Pick<
324
+ TelegramQueueItem,
325
+ "kind" | "queueLane" | "admissionReceipts"
326
+ >,
327
+ ): void {
328
+ if (!isTelegramQueueItemAdmissionValid(item)) {
329
+ throw new Error(
330
+ `Invalid Telegram queue admission: ${item.kind} item cannot use ${item.queueLane} lane`,
331
+ );
332
+ }
333
+ const receiptIds = new Set<string>();
334
+ for (const receipt of item.admissionReceipts ?? []) {
335
+ const sourceUpdateIds = new Set(receipt.sourceUpdateIds);
336
+ if (
337
+ receipt.queueKind !== item.kind ||
338
+ !receipt.receiptId ||
339
+ receiptIds.has(receipt.receiptId) ||
340
+ receipt.sourceUpdateIds.length === 0 ||
341
+ (receipt.journalBindingKey !== undefined &&
342
+ receipt.journalBindingKey.trim().length === 0) ||
343
+ sourceUpdateIds.size !== receipt.sourceUpdateIds.length ||
344
+ receipt.sourceUpdateIds.some(
345
+ (updateId, index) =>
346
+ !Number.isSafeInteger(updateId) ||
347
+ updateId < 0 ||
348
+ (index > 0 && updateId <= receipt.sourceUpdateIds[index - 1]!),
349
+ )
350
+ ) {
351
+ throw new Error(
352
+ `Invalid Telegram queue receipt for ${item.kind} item`,
353
+ );
354
+ }
355
+ receiptIds.add(receipt.receiptId);
356
+ }
357
+ }
358
+
359
+ function getTelegramQueueLaneRank(lane: TelegramQueueLane): number {
360
+ return getTelegramQueueLaneContract(lane).dispatchRank;
361
+ }
362
+
363
+ export function isPendingTelegramTurn<TContext = unknown>(
364
+ item: TelegramQueueItem<TContext>,
365
+ ): item is PendingTelegramTurn {
366
+ return item.kind === "prompt";
367
+ }
368
+
369
+ export function createTelegramQueueStore<TContext = unknown>(
370
+ initialItems: TelegramQueueItem<TContext>[] = [],
371
+ ): TelegramQueueStateStore<TContext> {
372
+ let queuedItems = initialItems;
373
+ return {
374
+ getQueuedItems: () => queuedItems,
375
+ setQueuedItems: (items) => {
376
+ queuedItems = items;
377
+ },
378
+ hasQueuedItems: () => queuedItems.length > 0,
379
+ };
380
+ }
381
+
382
+ export function createTelegramTransportStampRuntime(deps: {
383
+ getProfileName(): string | undefined;
384
+ getBotToken(): string | undefined;
385
+ }): TelegramTransportStampRuntime {
386
+ let profile: string | undefined;
387
+ let botToken: string | undefined;
388
+ let generation = 0;
389
+ const getStamp = function (): TelegramTransportStamp {
390
+ const nextProfile = deps.getProfileName() ?? "default";
391
+ const nextBotToken = deps.getBotToken();
392
+ if (nextProfile !== profile || nextBotToken !== botToken) {
393
+ profile = nextProfile;
394
+ botToken = nextBotToken;
395
+ generation += 1;
396
+ }
397
+ return { profile: nextProfile, generation: String(generation) };
398
+ };
399
+ return {
400
+ getStamp,
401
+ isActive(stamp) {
402
+ if (!stamp) return false;
403
+ const current = getStamp();
404
+ return (
405
+ stamp.profile === current.profile &&
406
+ stamp.generation === current.generation
407
+ );
408
+ },
409
+ };
410
+ }
411
+
412
+ export function createTelegramTransportStampedQueueStore<TContext>(
413
+ store: TelegramQueueStateStore<TContext>,
414
+ getTransportStamp: () => TelegramTransportStamp,
415
+ ): TelegramQueueStateStore<TContext> {
416
+ return {
417
+ getQueuedItems: store.getQueuedItems,
418
+ hasQueuedItems: store.hasQueuedItems,
419
+ setQueuedItems(items) {
420
+ const stamp = getTransportStamp();
421
+ store.setQueuedItems(
422
+ items.map((item) =>
423
+ item.transportStamp ? item : { ...item, transportStamp: stamp },
424
+ ),
425
+ );
426
+ },
427
+ };
428
+ }
429
+
430
+ export function createTelegramQueueItemCountGetter<TContext = unknown>(
431
+ store: Pick<TelegramQueueStore<TContext>, "getQueuedItems">,
432
+ ): () => number {
433
+ return () => {
434
+ return store.getQueuedItems().length;
435
+ };
436
+ }
437
+
438
+ export function createTelegramActiveTurnStore<
439
+ TTurn extends PendingTelegramTurn = PendingTelegramTurn,
440
+ >(): TelegramActiveTurnStore<TTurn> {
441
+ let activeTurn: TTurn | undefined;
442
+ return {
443
+ get: () => activeTurn,
444
+ has: () => !!activeTurn,
445
+ set: (turn) => {
446
+ activeTurn = { ...turn };
447
+ },
448
+ clear: () => {
449
+ activeTurn = undefined;
450
+ },
451
+ getChatId: () => activeTurn?.chatId,
452
+ getTarget: () =>
453
+ activeTurn?.target ? { ...activeTurn.target } : undefined,
454
+ getReplyToMessageId: () => activeTurn?.replyToMessageId,
455
+ getGuestQueryId: () => activeTurn?.guestQueryId,
456
+ getSourceMessageIds: () => activeTurn?.sourceMessageIds,
457
+ };
458
+ }
459
+
460
+ // --- Queue Mutations ---
461
+
462
+ export function partitionTelegramQueueItemsForHistory<TContext = unknown>(
463
+ items: TelegramQueueItem<TContext>[],
464
+ ): {
465
+ historyTurns: PendingTelegramTurn[];
466
+ remainingItems: TelegramQueueItem<TContext>[];
467
+ } {
468
+ const historyTurns: PendingTelegramTurn[] = [];
469
+ const remainingItems: TelegramQueueItem<TContext>[] = [];
470
+ for (const item of items) {
471
+ if (isPendingTelegramTurn(item)) {
472
+ historyTurns.push(item);
473
+ continue;
474
+ }
475
+ remainingItems.push(item);
476
+ }
477
+ return { historyTurns, remainingItems };
478
+ }
479
+
480
+ export function planTelegramPromptEnqueue<TContext = unknown>(
481
+ items: TelegramQueueItem<TContext>[],
482
+ foldQueuedPromptsIntoHistory: boolean,
483
+ ): {
484
+ historyTurns: PendingTelegramTurn[];
485
+ remainingItems: TelegramQueueItem<TContext>[];
486
+ } {
487
+ if (!foldQueuedPromptsIntoHistory) {
488
+ return { historyTurns: [], remainingItems: items };
489
+ }
490
+ return partitionTelegramQueueItemsForHistory(items);
491
+ }
492
+
493
+ export function areTelegramQueueAdmissionReceiptsEqual(
494
+ left: TelegramQueueAdmissionReceipt,
495
+ right: TelegramQueueAdmissionReceipt,
496
+ ): boolean {
497
+ return (
498
+ left.queueKind === right.queueKind &&
499
+ left.receiptId === right.receiptId &&
500
+ left.journalBindingKey === right.journalBindingKey &&
501
+ left.sourceUpdateIds.length === right.sourceUpdateIds.length &&
502
+ left.sourceUpdateIds.every(
503
+ (updateId, index) => updateId === right.sourceUpdateIds[index],
504
+ )
505
+ );
506
+ }
507
+
508
+ function getTelegramQueueAdmissionReceiptKey(
509
+ receipt: TelegramQueueAdmissionReceipt,
510
+ ): string {
511
+ return JSON.stringify([
512
+ receipt.receiptId,
513
+ receipt.journalBindingKey ?? null,
514
+ ]);
515
+ }
516
+
517
+ function isDuplicateTelegramQueueAdmission<TContext>(
518
+ items: TelegramQueueItem<TContext>[],
519
+ item: TelegramQueueItem<TContext>,
520
+ ): boolean {
521
+ const incomingReceipts = item.admissionReceipts ?? [];
522
+ if (incomingReceipts.length === 0) return false;
523
+ const queuedReceipts = new Map<
524
+ string,
525
+ { receipt: TelegramQueueAdmissionReceipt; count: number }
526
+ >();
527
+ for (const queuedItem of items) {
528
+ for (const receipt of queuedItem.admissionReceipts ?? []) {
529
+ const key = getTelegramQueueAdmissionReceiptKey(receipt);
530
+ const existing = queuedReceipts.get(key);
531
+ if (existing) existing.count += 1;
532
+ else queuedReceipts.set(key, { receipt, count: 1 });
533
+ }
534
+ }
535
+ let duplicateCount = 0;
536
+ for (const incoming of incomingReceipts) {
537
+ const match = queuedReceipts.get(
538
+ getTelegramQueueAdmissionReceiptKey(incoming),
539
+ );
540
+ if (!match) continue;
541
+ if (
542
+ match.count !== 1 ||
543
+ !areTelegramQueueAdmissionReceiptsEqual(match.receipt, incoming)
544
+ ) {
545
+ throw new Error(
546
+ `Conflicting Telegram queue receipt: ${incoming.receiptId}`,
547
+ );
548
+ }
549
+ duplicateCount += 1;
550
+ }
551
+ if (duplicateCount === 0) return false;
552
+ if (duplicateCount === incomingReceipts.length) return true;
553
+ throw new Error("Telegram queue item overlaps an existing receipt.");
554
+ }
555
+
556
+ export function appendTelegramQueueItem<
557
+ TContext = unknown,
558
+ TItem extends TelegramQueueItem<TContext> = TelegramQueueItem<TContext>,
559
+ >(
560
+ items: TelegramQueueItem<TContext>[],
561
+ item: TItem,
562
+ ): TelegramQueueItem<TContext>[] {
563
+ assertTelegramQueueItemAdmissionValid(item);
564
+ if (isDuplicateTelegramQueueAdmission(items, item)) return items;
565
+ return [...items, item];
566
+ }
567
+
568
+ export function createTelegramQueueHandoff<TContext>(input: {
569
+ handoffToken: string;
570
+ item: TelegramQueueItem<TContext>;
571
+ }): TelegramQueueHandoff {
572
+ if (!input.handoffToken) {
573
+ throw new Error("Telegram queue handoff token is required.");
574
+ }
575
+ const handoff = {
576
+ handoffToken: input.handoffToken,
577
+ payload: createTelegramQueueHandoffPayload(input.item),
578
+ };
579
+ if (Buffer.byteLength(JSON.stringify(handoff)) > TELEGRAM_QUEUE_HANDOFF_PAYLOAD_MAX_BYTES) {
580
+ throw new Error("Telegram queue handoff payload exceeds its byte limit.");
581
+ }
582
+ return handoff;
583
+ }
584
+
585
+ export function createTelegramQueueHandoffPayload<TContext>(
586
+ item: TelegramQueueItem<TContext>,
587
+ ): TelegramQueueHandoffPayload {
588
+ assertTelegramQueueItemAdmissionValid(item);
589
+ if (!item.admissionReceipts?.length) {
590
+ throw new Error("Telegram queue handoff requires durable admission receipts.");
591
+ }
592
+ if (item.admissionReceipts.length > TELEGRAM_QUEUE_HANDOFF_MAX_RECEIPTS) {
593
+ throw new Error("Telegram queue handoff has too many admission receipts.");
594
+ }
595
+ if (item.kind === "control") {
596
+ return structuredClone({
597
+ kind: item.kind,
598
+ controlType: item.controlType,
599
+ chatId: item.chatId,
600
+ ...(item.target ? { target: item.target } : {}),
601
+ ...(item.transportStamp ? { transportStamp: item.transportStamp } : {}),
602
+ replyToMessageId: item.replyToMessageId,
603
+ ...(item.guestQueryId ? { guestQueryId: item.guestQueryId } : {}),
604
+ queueOrder: item.queueOrder,
605
+ queueLane: item.queueLane,
606
+ laneOrder: item.laneOrder,
607
+ statusSummary: item.statusSummary,
608
+ admissionReceipts: item.admissionReceipts,
609
+ });
610
+ }
611
+ return structuredClone({
612
+ kind: item.kind,
613
+ chatId: item.chatId,
614
+ ...(item.target ? { target: item.target } : {}),
615
+ ...(item.transportStamp ? { transportStamp: item.transportStamp } : {}),
616
+ replyToMessageId: item.replyToMessageId,
617
+ ...(item.guestQueryId ? { guestQueryId: item.guestQueryId } : {}),
618
+ queueOrder: item.queueOrder,
619
+ queueLane: item.queueLane,
620
+ laneOrder: item.laneOrder,
621
+ statusSummary: item.statusSummary,
622
+ admissionReceipts: item.admissionReceipts,
623
+ sourceMessageIds: item.sourceMessageIds,
624
+ queuedAttachments: item.queuedAttachments,
625
+ content: item.content,
626
+ historyText: item.historyText,
627
+ ...(item.priorityEmoji ? { priorityEmoji: item.priorityEmoji } : {}),
628
+ ...(item.reactionSuppressionEmoji
629
+ ? { reactionSuppressionEmoji: item.reactionSuppressionEmoji }
630
+ : {}),
631
+ ...(item.voiceReplyPreferred !== undefined
632
+ ? { voiceReplyPreferred: item.voiceReplyPreferred }
633
+ : {}),
634
+ ...(item.voiceReplyRequired !== undefined
635
+ ? { voiceReplyRequired: item.voiceReplyRequired }
636
+ : {}),
637
+ });
638
+ }
639
+
640
+ export function restoreTelegramQueueHandoffPayload<TContext>(
641
+ payload: TelegramQueueHandoffPayload,
642
+ createControlExecution: (
643
+ payload: TelegramControlQueueHandoffPayload,
644
+ ) => PendingTelegramControlItem<TContext>["execute"],
645
+ ): TelegramQueueItem<TContext> {
646
+ const item: TelegramQueueItem<TContext> =
647
+ payload.kind === "prompt"
648
+ ? structuredClone(payload)
649
+ : {
650
+ ...structuredClone(payload),
651
+ execute: createControlExecution(payload),
652
+ };
653
+ assertTelegramQueueItemAdmissionValid(item);
654
+ return item;
655
+ }
656
+
657
+ function getTelegramQueueHandoffReceipt(
658
+ payload: TelegramQueueHandoffPayload,
659
+ ): TelegramQueueAdmissionReceipt {
660
+ const receipt = payload.admissionReceipts[0];
661
+ if (!receipt || payload.admissionReceipts.length !== 1) {
662
+ throw new Error(
663
+ "Telegram queue handoff requires exactly one complete receipt.",
664
+ );
665
+ }
666
+ return receipt;
667
+ }
668
+
669
+ function findTelegramQueueReceiptItem<TContext>(
670
+ items: readonly TelegramQueueItem<TContext>[],
671
+ receipt: TelegramQueueAdmissionReceipt,
672
+ ): TelegramQueueItem<TContext> | undefined {
673
+ const receiptKey = getTelegramQueueAdmissionReceiptKey(receipt);
674
+ let match: TelegramQueueItem<TContext> | undefined;
675
+ let matchedReceipt: TelegramQueueAdmissionReceipt | undefined;
676
+ for (const item of items) {
677
+ for (const candidate of item.admissionReceipts ?? []) {
678
+ if (getTelegramQueueAdmissionReceiptKey(candidate) !== receiptKey) {
679
+ continue;
680
+ }
681
+ if (match) {
682
+ throw new Error(
683
+ `Conflicting Telegram queue receipt: ${receipt.receiptId}`,
684
+ );
685
+ }
686
+ match = item;
687
+ matchedReceipt = candidate;
688
+ }
689
+ }
690
+ if (!match) return undefined;
691
+ if (
692
+ !matchedReceipt ||
693
+ !areTelegramQueueAdmissionReceiptsEqual(matchedReceipt, receipt)
694
+ ) {
695
+ throw new Error(`Conflicting Telegram queue receipt: ${receipt.receiptId}`);
696
+ }
697
+ return match;
698
+ }
699
+
700
+ export function removeTelegramQueueItemByReceipt<TContext>(input: {
701
+ receipt: TelegramQueueAdmissionReceipt;
702
+ store: TelegramQueueStore<TContext>;
703
+ }): boolean {
704
+ const current = input.store.getQueuedItems();
705
+ const item = findTelegramQueueReceiptItem(current, input.receipt);
706
+ if (!item) return false;
707
+ input.store.setQueuedItems(current.filter((candidate) => candidate !== item));
708
+ return true;
709
+ }
710
+
711
+ export function stageTelegramQueueHandoffPayload<TContext>(input: {
712
+ payload: TelegramQueueHandoffPayload;
713
+ store: TelegramQueueStore<TContext>;
714
+ createControlExecution: (
715
+ payload: TelegramControlQueueHandoffPayload,
716
+ ) => PendingTelegramControlItem<TContext>["execute"];
717
+ }): TelegramQueueHandoffStageReceipt {
718
+ const receipt = getTelegramQueueHandoffReceipt(input.payload);
719
+ const item = restoreTelegramQueueHandoffPayload(
720
+ input.payload,
721
+ input.createControlExecution,
722
+ );
723
+ const current = input.store.getQueuedItems();
724
+ const next = appendTelegramQueueItem(current, item);
725
+ if (next !== current) input.store.setQueuedItems(next);
726
+ return {
727
+ status: "staged",
728
+ receiptId: receipt.receiptId,
729
+ sourceUpdateIds: [...receipt.sourceUpdateIds],
730
+ };
731
+ }
732
+
733
+ export function createTelegramQueueHandoffStagingRuntime<TContext>(input: {
734
+ liveStore: TelegramQueueStore<TContext>;
735
+ createControlExecution: (
736
+ payload: TelegramControlQueueHandoffPayload,
737
+ ) => PendingTelegramControlItem<TContext>["execute"];
738
+ }): TelegramQueueHandoffStagingRuntime {
739
+ const stagedStore = createTelegramQueueStore<TContext>();
740
+ return {
741
+ stage(payload) {
742
+ const receipt = getTelegramQueueHandoffReceipt(payload);
743
+ const liveItem = findTelegramQueueReceiptItem(
744
+ input.liveStore.getQueuedItems(),
745
+ receipt,
746
+ );
747
+ if (liveItem) {
748
+ throw new Error(
749
+ `Telegram queue handoff receipt ${receipt.receiptId} is already live.`,
750
+ );
751
+ }
752
+ return stageTelegramQueueHandoffPayload({
753
+ payload,
754
+ store: stagedStore,
755
+ createControlExecution: input.createControlExecution,
756
+ });
757
+ },
758
+ accept(receipt) {
759
+ const stagedItems = stagedStore.getQueuedItems();
760
+ const stagedItem = findTelegramQueueReceiptItem(stagedItems, receipt);
761
+ if (!stagedItem) {
762
+ return Boolean(
763
+ findTelegramQueueReceiptItem(
764
+ input.liveStore.getQueuedItems(),
765
+ receipt,
766
+ ),
767
+ );
768
+ }
769
+ const current = input.liveStore.getQueuedItems();
770
+ const next = appendTelegramQueueItem(current, stagedItem);
771
+ if (next !== current) input.liveStore.setQueuedItems(next);
772
+ stagedStore.setQueuedItems(
773
+ stagedItems.filter((candidate) => candidate !== stagedItem),
774
+ );
775
+ return true;
776
+ },
777
+ cancel(receipt) {
778
+ const stagedItems = stagedStore.getQueuedItems();
779
+ const stagedItem = findTelegramQueueReceiptItem(stagedItems, receipt);
780
+ if (!stagedItem) return false;
781
+ stagedStore.setQueuedItems(
782
+ stagedItems.filter((candidate) => candidate !== stagedItem),
783
+ );
784
+ return true;
785
+ },
786
+ hasStaged(receipt) {
787
+ return Boolean(
788
+ findTelegramQueueReceiptItem(stagedStore.getQueuedItems(), receipt),
789
+ );
790
+ },
791
+ };
792
+ }
793
+
794
+ function getTelegramPromptTextSignature(item: PendingTelegramTurn): string {
795
+ return item.content
796
+ .filter(
797
+ (entry): entry is TelegramPromptTextContent => entry.type === "text",
798
+ )
799
+ .map((entry) => entry.text)
800
+ .join("\n");
801
+ }
802
+
803
+ function isDuplicateTelegramPromptTurn(
804
+ left: PendingTelegramTurn,
805
+ right: PendingTelegramTurn,
806
+ ): boolean {
807
+ return (
808
+ left.chatId === right.chatId &&
809
+ left.target?.threadId === right.target?.threadId &&
810
+ left.replyToMessageId === right.replyToMessageId &&
811
+ getTelegramPromptTextSignature(left) ===
812
+ getTelegramPromptTextSignature(right)
813
+ );
814
+ }
815
+
816
+ export function appendTelegramPromptTurnOnce<TContext = unknown>(
817
+ items: TelegramQueueItem<TContext>[],
818
+ turn: PendingTelegramTurn,
819
+ ): { items: TelegramQueueItem<TContext>[]; appended: boolean } {
820
+ assertTelegramQueueItemAdmissionValid(turn);
821
+ if (isDuplicateTelegramQueueAdmission(items, turn)) {
822
+ return { items, appended: false };
823
+ }
824
+ const hasAdmissionReceipts = (turn.admissionReceipts?.length ?? 0) > 0;
825
+ const duplicate =
826
+ !hasAdmissionReceipts &&
827
+ items.some(
828
+ (item) =>
829
+ isPendingTelegramTurn(item) &&
830
+ (item.admissionReceipts?.length ?? 0) === 0 &&
831
+ isDuplicateTelegramPromptTurn(item, turn),
832
+ );
833
+ if (duplicate) return { items, appended: false };
834
+ return {
835
+ items: [...items, turn].sort(compareTelegramQueueItems),
836
+ appended: true,
837
+ };
838
+ }
839
+
840
+ export function compareTelegramQueueItems<TContext = unknown>(
841
+ left: TelegramQueueItem<TContext>,
842
+ right: TelegramQueueItem<TContext>,
843
+ ): number {
844
+ assertTelegramQueueItemAdmissionValid(left);
845
+ assertTelegramQueueItemAdmissionValid(right);
846
+ const laneRankDelta =
847
+ getTelegramQueueLaneRank(left.queueLane) -
848
+ getTelegramQueueLaneRank(right.queueLane);
849
+ if (laneRankDelta !== 0) return laneRankDelta;
850
+ if (left.laneOrder !== right.laneOrder) {
851
+ return left.laneOrder - right.laneOrder;
852
+ }
853
+ return left.queueOrder - right.queueOrder;
854
+ }
855
+
856
+ export interface TelegramQueueMessageScope {
857
+ chatId?: number;
858
+ threadId?: number;
859
+ }
860
+
861
+ function isTelegramQueueItemInMessageScope<TContext = unknown>(
862
+ item: TelegramQueueItem<TContext>,
863
+ scope: TelegramQueueMessageScope | undefined,
864
+ ): boolean {
865
+ if (!scope) return true;
866
+ if (typeof scope.chatId === "number" && item.chatId !== scope.chatId) {
867
+ return false;
868
+ }
869
+ if (typeof scope.threadId === "number") {
870
+ return item.target?.threadId === scope.threadId;
871
+ }
872
+ return true;
873
+ }
874
+
875
+ export function removeTelegramQueueItemsByMessageIds<TContext = unknown>(
876
+ items: TelegramQueueItem<TContext>[],
877
+ messageIds: number[],
878
+ scope?: TelegramQueueMessageScope,
879
+ ): {
880
+ items: TelegramQueueItem<TContext>[];
881
+ removedItems: TelegramQueueItem<TContext>[];
882
+ removedCount: number;
883
+ } {
884
+ if (messageIds.length === 0 || items.length === 0) {
885
+ return { items, removedItems: [], removedCount: 0 };
886
+ }
887
+ const deletedMessageIds = new Set(messageIds);
888
+ const nextItems: TelegramQueueItem<TContext>[] = [];
889
+ const removedItems: TelegramQueueItem<TContext>[] = [];
890
+ for (const item of items) {
891
+ const shouldRemove =
892
+ isPendingTelegramTurn(item) &&
893
+ isTelegramQueueItemInMessageScope(item, scope) &&
894
+ item.sourceMessageIds.some((messageId) =>
895
+ deletedMessageIds.has(messageId),
896
+ );
897
+ (shouldRemove ? removedItems : nextItems).push(item);
898
+ }
899
+ return {
900
+ items: nextItems,
901
+ removedItems,
902
+ removedCount: removedItems.length,
903
+ };
904
+ }
905
+
906
+ export function applyTelegramQueuePromptReactionDisposition<
907
+ TContext = unknown,
908
+ >(
909
+ items: TelegramQueueItem<TContext>[],
910
+ messageId: number,
911
+ disposition: TelegramQueueReactionDisposition,
912
+ destinationLaneOrder?: number,
913
+ scope?: TelegramQueueMessageScope,
914
+ ): { items: TelegramQueueItem<TContext>[]; changed: boolean } {
915
+ let nextItems = items;
916
+ for (const [index, item] of items.entries()) {
917
+ if (
918
+ !isPendingTelegramTurn(item) ||
919
+ !isTelegramQueueItemInMessageScope(item, scope) ||
920
+ !item.sourceMessageIds.includes(messageId)
921
+ ) {
922
+ continue;
923
+ }
924
+ const isPriority = disposition.kind === "reaction-transition"
925
+ ? disposition.priorityEmoji === undefined
926
+ ? item.queueLane === "priority"
927
+ : disposition.priorityEmoji !== null
928
+ : disposition.kind === "priority" ||
929
+ disposition.kind === "priority-suppressed";
930
+ const queueLane: TelegramQueueLane = isPriority ? "priority" : "default";
931
+ const laneOrder = item.queueLane === queueLane
932
+ ? item.laneOrder
933
+ : destinationLaneOrder;
934
+ if (laneOrder === undefined) {
935
+ throw new Error("Telegram destination lane order is unavailable.");
936
+ }
937
+ const priorityEmoji = disposition.kind === "reaction-transition"
938
+ ? disposition.priorityEmoji === undefined
939
+ ? item.priorityEmoji
940
+ : disposition.priorityEmoji ?? undefined
941
+ : disposition.kind === "priority"
942
+ ? disposition.emoji
943
+ : disposition.kind === "priority-suppressed"
944
+ ? disposition.priorityEmoji
945
+ : undefined;
946
+ const reactionSuppressionEmoji =
947
+ disposition.kind === "reaction-transition"
948
+ ? disposition.suppressionEmoji === undefined
949
+ ? item.reactionSuppressionEmoji
950
+ : disposition.suppressionEmoji ?? undefined
951
+ : disposition.kind === "suppressed"
952
+ ? disposition.emoji
953
+ : disposition.kind === "priority-suppressed"
954
+ ? disposition.suppressionEmoji
955
+ : undefined;
956
+ if (
957
+ item.queueLane === queueLane &&
958
+ item.laneOrder === laneOrder &&
959
+ item.priorityEmoji === priorityEmoji &&
960
+ item.reactionSuppressionEmoji === reactionSuppressionEmoji
961
+ ) {
962
+ continue;
963
+ }
964
+ if (nextItems === items) nextItems = [...items];
965
+ nextItems[index] = {
966
+ ...item,
967
+ queueLane,
968
+ laneOrder,
969
+ priorityEmoji,
970
+ reactionSuppressionEmoji,
971
+ };
972
+ }
973
+ return { items: nextItems, changed: nextItems !== items };
974
+ }
975
+
976
+ export function consumeDispatchedTelegramPrompt<TContext = unknown>(
977
+ items: TelegramQueueItem<TContext>[],
978
+ hasPendingDispatch: boolean,
979
+ ): {
980
+ activeTurn?: PendingTelegramTurn;
981
+ remainingItems: TelegramQueueItem<TContext>[];
982
+ } {
983
+ if (!hasPendingDispatch) {
984
+ return { activeTurn: undefined, remainingItems: items };
985
+ }
986
+ const nextItem = items[0];
987
+ if (!nextItem || !isPendingTelegramTurn(nextItem)) {
988
+ return { activeTurn: undefined, remainingItems: items };
989
+ }
990
+ return { activeTurn: nextItem, remainingItems: items.slice(1) };
991
+ }
992
+
993
+ export function formatQueuedTelegramItemsStatus<TContext = unknown>(
994
+ items: TelegramQueueItem<TContext>[],
995
+ ): string {
996
+ return items.length === 0 ? "" : ` +${items.length}`;
997
+ }
998
+
999
+ export function truncateTelegramQueueSummary(
1000
+ text: string,
1001
+ maxWords = 5,
1002
+ maxLength = 40,
1003
+ ): string {
1004
+ const normalized = text.replace(/\s+/g, " ").trim();
1005
+ if (!normalized) return "";
1006
+ const words = normalized.split(" ");
1007
+ let summary = words.slice(0, maxWords).join(" ");
1008
+ if (summary.length === 0) summary = normalized;
1009
+ if (summary.length > maxLength) {
1010
+ summary = summary.slice(0, maxLength).trimEnd();
1011
+ }
1012
+ return summary.length < normalized.length || words.length > maxWords
1013
+ ? `${summary}…`
1014
+ : summary;
1015
+ }
1016
+
1017
+ export function canDispatchTelegramTurnState(
1018
+ state: TelegramDispatchGuardState,
1019
+ ): boolean {
1020
+ return (
1021
+ !state.compactionInProgress &&
1022
+ !state.hasActiveTelegramTurn &&
1023
+ !state.hasPendingTelegramDispatch &&
1024
+ state.isIdle &&
1025
+ !state.hasPendingMessages
1026
+ );
1027
+ }
1028
+
1029
+ export interface TelegramDispatchReadinessDeps<TContext> {
1030
+ isCompactionInProgress: () => boolean;
1031
+ hasActiveTurn: () => boolean;
1032
+ hasDispatchPending: () => boolean;
1033
+ isIdle: (ctx: TContext) => boolean;
1034
+ hasPendingMessages: (ctx: TContext) => boolean;
1035
+ }
1036
+
1037
+ export function createTelegramDispatchReadinessChecker<TContext>(
1038
+ deps: TelegramDispatchReadinessDeps<TContext>,
1039
+ ): (ctx: TContext) => boolean {
1040
+ return (ctx) =>
1041
+ canDispatchTelegramTurnState({
1042
+ compactionInProgress: deps.isCompactionInProgress(),
1043
+ hasActiveTelegramTurn: deps.hasActiveTurn(),
1044
+ hasPendingTelegramDispatch: deps.hasDispatchPending(),
1045
+ isIdle: deps.isIdle(ctx),
1046
+ hasPendingMessages: deps.hasPendingMessages(ctx),
1047
+ });
1048
+ }
1049
+
1050
+ export function buildPendingTelegramControlItem<TContext = unknown>(options: {
1051
+ chatId: number;
1052
+ target?: TelegramQueueTarget;
1053
+ replyToMessageId: number;
1054
+ controlType: PendingTelegramControlItem<TContext>["controlType"];
1055
+ queueOrder: number;
1056
+ laneOrder: number;
1057
+ statusSummary: string;
1058
+ admissionReceipts?: TelegramQueueAdmissionReceipt[];
1059
+ execute: PendingTelegramControlItem<TContext>["execute"];
1060
+ }): PendingTelegramControlItem<TContext> {
1061
+ return {
1062
+ kind: "control",
1063
+ controlType: options.controlType,
1064
+ chatId: options.chatId,
1065
+ ...(options.target ? { target: options.target } : {}),
1066
+ replyToMessageId: options.replyToMessageId,
1067
+ queueOrder: options.queueOrder,
1068
+ queueLane: "control",
1069
+ laneOrder: options.laneOrder,
1070
+ statusSummary: options.statusSummary,
1071
+ ...(options.admissionReceipts?.length
1072
+ ? { admissionReceipts: structuredClone(options.admissionReceipts) }
1073
+ : {}),
1074
+ execute: options.execute,
1075
+ };
1076
+ }
1077
+
1078
+ export interface TelegramControlItemBuilderDeps {
1079
+ allocateItemOrder: () => number;
1080
+ allocateControlOrder: () => number;
1081
+ }
1082
+
1083
+ export function createTelegramControlItemBuilder<TContext = unknown>(
1084
+ deps: TelegramControlItemBuilderDeps,
1085
+ ): (options: {
1086
+ chatId: number;
1087
+ target?: TelegramQueueTarget;
1088
+ replyToMessageId: number;
1089
+ controlType: PendingTelegramControlItem<TContext>["controlType"];
1090
+ statusSummary: string;
1091
+ admissionReceipts?: TelegramQueueAdmissionReceipt[];
1092
+ execute: PendingTelegramControlItem<TContext>["execute"];
1093
+ }) => PendingTelegramControlItem<TContext> {
1094
+ return (options) =>
1095
+ buildPendingTelegramControlItem<TContext>({
1096
+ ...options,
1097
+ queueOrder: deps.allocateItemOrder(),
1098
+ laneOrder: deps.allocateControlOrder(),
1099
+ });
1100
+ }
1101
+
1102
+ // --- Dispatch Planning ---
1103
+
1104
+ export type TelegramQueueDispatchAction<TContext = unknown> =
1105
+ | { kind: "none"; remainingItems: TelegramQueueItem<TContext>[] }
1106
+ | {
1107
+ kind: "control";
1108
+ item: PendingTelegramControlItem<TContext>;
1109
+ remainingItems: TelegramQueueItem<TContext>[];
1110
+ }
1111
+ | {
1112
+ kind: "prompt";
1113
+ item: PendingTelegramTurn;
1114
+ remainingItems: TelegramQueueItem<TContext>[];
1115
+ };
1116
+
1117
+ export function planNextTelegramQueueAction<TContext = unknown>(
1118
+ items: TelegramQueueItem<TContext>[],
1119
+ canDispatch: boolean,
1120
+ ): TelegramQueueDispatchAction<TContext> {
1121
+ if (!canDispatch || items.length === 0) {
1122
+ return { kind: "none", remainingItems: items };
1123
+ }
1124
+ const [firstItem, ...remainingItems] = items;
1125
+ if (!firstItem) {
1126
+ return { kind: "none", remainingItems: items };
1127
+ }
1128
+ assertTelegramQueueItemAdmissionValid(firstItem);
1129
+ if (isPendingTelegramTurn(firstItem)) {
1130
+ return { kind: "prompt", item: firstItem, remainingItems: items };
1131
+ }
1132
+ return { kind: "control", item: firstItem, remainingItems };
1133
+ }
1134
+
1135
+ export function shouldDispatchAfterTelegramAgentEnd(options: {
1136
+ hasTurn: boolean;
1137
+ stopReason?: string;
1138
+ foldQueuedPromptsIntoHistory: boolean;
1139
+ }): boolean {
1140
+ if (!options.hasTurn) return true;
1141
+ if (options.stopReason === "aborted") {
1142
+ return !options.foldQueuedPromptsIntoHistory;
1143
+ }
1144
+ return true;
1145
+ }
1146
+
1147
+ // --- Agent Runtime ---
1148
+
1149
+ export interface TelegramAgentStartPlan<TContext = unknown> {
1150
+ activeTurn?: PendingTelegramTurn;
1151
+ remainingItems: TelegramQueueItem<TContext>[];
1152
+ shouldResetPendingModelSwitch: boolean;
1153
+ shouldResetToolExecutions: boolean;
1154
+ shouldClearDispatchPending: boolean;
1155
+ shouldClearAbortHistory: boolean;
1156
+ }
1157
+
1158
+ export interface TelegramAgentStartRuntimeDeps<
1159
+ TTurn extends PendingTelegramTurn,
1160
+ TContext = unknown,
1161
+ > extends TelegramRuntimeEventRecorderPort {
1162
+ queuedItems: TelegramQueueItem<TContext>[];
1163
+ hasPendingDispatch: boolean;
1164
+ hasActiveTurn: boolean;
1165
+ resetToolExecutions: () => void;
1166
+ resetPendingModelSwitch: () => void;
1167
+ setQueuedItems: (items: TelegramQueueItem<TContext>[]) => void;
1168
+ clearDispatchPending: () => void;
1169
+ setFoldQueuedPromptsIntoHistory: (fold: boolean) => void;
1170
+ setActiveTurn: (turn: TTurn) => void;
1171
+ onPromptHandedOff?: (turn: TTurn) => void;
1172
+ createPreviewState: () => void;
1173
+ startTypingLoop: () => void;
1174
+ updateStatus: () => void;
1175
+ }
1176
+
1177
+ export interface TelegramAgentStartHookRuntimeDeps<
1178
+ TTurn extends PendingTelegramTurn,
1179
+ TContext = unknown,
1180
+ > extends TelegramRuntimeEventRecorderPort {
1181
+ setAbortHandler: (ctx: TContext) => void;
1182
+ getQueuedItems: () => TelegramQueueItem<TContext>[];
1183
+ hasPendingDispatch: () => boolean;
1184
+ hasActiveTurn: () => boolean;
1185
+ resetToolExecutions: () => void;
1186
+ resetPendingModelSwitch: () => void;
1187
+ setQueuedItems: (items: TelegramQueueItem<TContext>[]) => void;
1188
+ clearDispatchPending: () => void;
1189
+ setFoldQueuedPromptsIntoHistory: (fold: boolean) => void;
1190
+ setActiveTurn: (turn: TTurn) => void;
1191
+ onPromptHandedOff?: (turn: TTurn, ctx: TContext) => void;
1192
+ createPreviewState: () => void;
1193
+ startTypingLoop: (ctx: TContext) => void;
1194
+ updateStatus: (ctx: TContext) => void;
1195
+ }
1196
+
1197
+ export type TelegramAgentStartHookEvent = unknown;
1198
+
1199
+ export interface TelegramToolExecutionRuntimeDeps {
1200
+ hasActiveTurn: () => boolean;
1201
+ getActiveToolExecutions: () => number;
1202
+ setActiveToolExecutions: (count: number) => void;
1203
+ }
1204
+
1205
+ export interface TelegramToolExecutionEndRuntimeDeps extends TelegramToolExecutionRuntimeDeps {
1206
+ triggerPendingModelSwitchAbort: () => void;
1207
+ }
1208
+
1209
+ export interface TelegramToolExecutionHookRuntimeDeps<
1210
+ TContext,
1211
+ > extends TelegramToolExecutionRuntimeDeps {
1212
+ triggerPendingModelSwitchAbort: (ctx: TContext) => unknown;
1213
+ }
1214
+
1215
+ export type TelegramToolExecutionHookEvent = unknown;
1216
+
1217
+ export function buildTelegramAgentStartPlan<TContext = unknown>(options: {
1218
+ queuedItems: TelegramQueueItem<TContext>[];
1219
+ hasPendingDispatch: boolean;
1220
+ hasActiveTurn: boolean;
1221
+ }): TelegramAgentStartPlan<TContext> {
1222
+ if (options.hasActiveTurn || !options.hasPendingDispatch) {
1223
+ return {
1224
+ activeTurn: undefined,
1225
+ remainingItems: options.queuedItems,
1226
+ shouldResetPendingModelSwitch: true,
1227
+ shouldResetToolExecutions: true,
1228
+ shouldClearDispatchPending: options.hasPendingDispatch,
1229
+ shouldClearAbortHistory:
1230
+ !options.hasActiveTurn && !options.hasPendingDispatch,
1231
+ };
1232
+ }
1233
+ const nextDispatch = consumeDispatchedTelegramPrompt(
1234
+ options.queuedItems,
1235
+ options.hasPendingDispatch,
1236
+ );
1237
+ return {
1238
+ activeTurn: nextDispatch.activeTurn,
1239
+ remainingItems: nextDispatch.remainingItems,
1240
+ shouldResetPendingModelSwitch: true,
1241
+ shouldResetToolExecutions: true,
1242
+ shouldClearDispatchPending: options.hasPendingDispatch,
1243
+ shouldClearAbortHistory: false,
1244
+ };
1245
+ }
1246
+
1247
+ export function handleTelegramAgentStartRuntime<
1248
+ TTurn extends PendingTelegramTurn,
1249
+ TContext = unknown,
1250
+ >(deps: TelegramAgentStartRuntimeDeps<TTurn, TContext>): void {
1251
+ const startPlan = buildTelegramAgentStartPlan({
1252
+ queuedItems: deps.queuedItems,
1253
+ hasPendingDispatch: deps.hasPendingDispatch,
1254
+ hasActiveTurn: deps.hasActiveTurn,
1255
+ });
1256
+ if (startPlan.shouldResetToolExecutions) deps.resetToolExecutions();
1257
+ if (startPlan.shouldResetPendingModelSwitch) deps.resetPendingModelSwitch();
1258
+ if (startPlan.shouldClearAbortHistory) {
1259
+ deps.setFoldQueuedPromptsIntoHistory(false);
1260
+ }
1261
+ deps.setQueuedItems(startPlan.remainingItems);
1262
+ if (startPlan.shouldClearDispatchPending) deps.clearDispatchPending();
1263
+ if (startPlan.activeTurn) {
1264
+ const activeTurn = startPlan.activeTurn as TTurn;
1265
+ deps.setActiveTurn(activeTurn);
1266
+ try {
1267
+ deps.onPromptHandedOff?.(activeTurn);
1268
+ } catch (error) {
1269
+ deps.recordRuntimeEvent?.("queue", error, {
1270
+ phase: "prompt-handoff-receipt-settlement",
1271
+ });
1272
+ }
1273
+ deps.createPreviewState();
1274
+ deps.startTypingLoop();
1275
+ }
1276
+ deps.updateStatus();
1277
+ }
1278
+
1279
+ export function createTelegramAgentStartHook<
1280
+ TTurn extends PendingTelegramTurn,
1281
+ TContext = unknown,
1282
+ >(deps: TelegramAgentStartHookRuntimeDeps<TTurn, TContext>) {
1283
+ return async (
1284
+ _event: TelegramAgentStartHookEvent,
1285
+ ctx: TContext,
1286
+ ): Promise<void> => {
1287
+ deps.setAbortHandler(ctx);
1288
+ handleTelegramAgentStartRuntime<TTurn, TContext>({
1289
+ queuedItems: deps.getQueuedItems(),
1290
+ hasPendingDispatch: deps.hasPendingDispatch(),
1291
+ hasActiveTurn: deps.hasActiveTurn(),
1292
+ resetToolExecutions: deps.resetToolExecutions,
1293
+ resetPendingModelSwitch: deps.resetPendingModelSwitch,
1294
+ setQueuedItems: deps.setQueuedItems,
1295
+ clearDispatchPending: deps.clearDispatchPending,
1296
+ setFoldQueuedPromptsIntoHistory: deps.setFoldQueuedPromptsIntoHistory,
1297
+ setActiveTurn: deps.setActiveTurn,
1298
+ onPromptHandedOff: (turn) => deps.onPromptHandedOff?.(turn, ctx),
1299
+ createPreviewState: deps.createPreviewState,
1300
+ recordRuntimeEvent: deps.recordRuntimeEvent,
1301
+ startTypingLoop: () => deps.startTypingLoop(ctx),
1302
+ updateStatus: () => deps.updateStatus(ctx),
1303
+ });
1304
+ };
1305
+ }
1306
+
1307
+ export function getNextTelegramToolExecutionCount(options: {
1308
+ hasActiveTurn: boolean;
1309
+ currentCount: number;
1310
+ event: "start" | "end";
1311
+ }): number {
1312
+ if (!options.hasActiveTurn) return options.currentCount;
1313
+ if (options.event === "start") {
1314
+ return options.currentCount + 1;
1315
+ }
1316
+ return Math.max(0, options.currentCount - 1);
1317
+ }
1318
+
1319
+ export function handleTelegramToolExecutionStartRuntime(
1320
+ deps: TelegramToolExecutionRuntimeDeps,
1321
+ ): void {
1322
+ deps.setActiveToolExecutions(
1323
+ getNextTelegramToolExecutionCount({
1324
+ hasActiveTurn: deps.hasActiveTurn(),
1325
+ currentCount: deps.getActiveToolExecutions(),
1326
+ event: "start",
1327
+ }),
1328
+ );
1329
+ }
1330
+
1331
+ export function handleTelegramToolExecutionEndRuntime(
1332
+ deps: TelegramToolExecutionEndRuntimeDeps,
1333
+ ): void {
1334
+ const hasActiveTurn = deps.hasActiveTurn();
1335
+ deps.setActiveToolExecutions(
1336
+ getNextTelegramToolExecutionCount({
1337
+ hasActiveTurn,
1338
+ currentCount: deps.getActiveToolExecutions(),
1339
+ event: "end",
1340
+ }),
1341
+ );
1342
+ if (hasActiveTurn) deps.triggerPendingModelSwitchAbort();
1343
+ }
1344
+
1345
+ export type TelegramAgentLifecycleHooksRuntimeDeps<
1346
+ TTurn extends PendingTelegramTurn,
1347
+ TContext,
1348
+ TMessage,
1349
+ TReplyMarkup = unknown,
1350
+ > = TelegramAgentStartHookRuntimeDeps<TTurn, TContext> &
1351
+ TelegramAgentEndHookRuntimeDeps<TTurn, TContext, TMessage, TReplyMarkup> &
1352
+ TelegramToolExecutionHookRuntimeDeps<TContext>;
1353
+
1354
+ export function createTelegramAgentLifecycleHooks<
1355
+ TTurn extends PendingTelegramTurn,
1356
+ TContext,
1357
+ TMessage,
1358
+ TReplyMarkup = unknown,
1359
+ >(
1360
+ deps: TelegramAgentLifecycleHooksRuntimeDeps<
1361
+ TTurn,
1362
+ TContext,
1363
+ TMessage,
1364
+ TReplyMarkup
1365
+ >,
1366
+ ) {
1367
+ const onAgentStart = createTelegramAgentStartHook<TTurn, TContext>(deps);
1368
+ const deliverAgentEnd = createTelegramAgentEndHook<
1369
+ TTurn,
1370
+ TContext,
1371
+ TMessage,
1372
+ TReplyMarkup
1373
+ >(deps);
1374
+ let retainedErrorEvent: TelegramAgentEndHookEvent<TMessage> | undefined;
1375
+ return {
1376
+ onAgentStart,
1377
+ async onAgentEnd(
1378
+ event: TelegramAgentEndHookEvent<TMessage>,
1379
+ ctx: TContext,
1380
+ ): Promise<void> {
1381
+ const turn = deps.getActiveTurn();
1382
+ const assistant = turn ? deps.extractAssistant(event.messages) : {};
1383
+ if (turn && assistant.stopReason === "error") {
1384
+ retainedErrorEvent = event;
1385
+ deps.recordRuntimeEvent?.(
1386
+ "provider-retry",
1387
+ new Error("Retained Telegram turn after retryable agent error"),
1388
+ { phase: "retained", hasFinalText: !!assistant.text?.trim() },
1389
+ );
1390
+ return;
1391
+ }
1392
+ if (retainedErrorEvent) {
1393
+ retainedErrorEvent = undefined;
1394
+ deps.recordRuntimeEvent?.(
1395
+ "provider-retry",
1396
+ new Error("Recovered retained Telegram turn after agent retry"),
1397
+ { phase: "recovered" },
1398
+ );
1399
+ }
1400
+ await deliverAgentEnd(event, ctx, assistant);
1401
+ },
1402
+ async onAgentSettled(_event: unknown, ctx: TContext): Promise<void> {
1403
+ const event = retainedErrorEvent;
1404
+ if (!event) return;
1405
+ retainedErrorEvent = undefined;
1406
+ deps.recordRuntimeEvent?.(
1407
+ "provider-retry",
1408
+ new Error("Finalized retained Telegram turn after agent settled"),
1409
+ { phase: "settled-failure" },
1410
+ );
1411
+ await deliverAgentEnd(event, ctx);
1412
+ },
1413
+ clearRetainedAgentEnd(): void {
1414
+ retainedErrorEvent = undefined;
1415
+ },
1416
+ ...createTelegramToolExecutionHooks<TContext>(deps),
1417
+ };
1418
+ }
1419
+
1420
+ export function createTelegramToolExecutionHooks<TContext>(
1421
+ deps: TelegramToolExecutionHookRuntimeDeps<TContext>,
1422
+ ) {
1423
+ return {
1424
+ onToolExecutionStart: (): void => {
1425
+ handleTelegramToolExecutionStartRuntime(deps);
1426
+ },
1427
+ onToolExecutionEnd: (
1428
+ _event: TelegramToolExecutionHookEvent,
1429
+ ctx: TContext,
1430
+ ): void => {
1431
+ handleTelegramToolExecutionEndRuntime({
1432
+ hasActiveTurn: deps.hasActiveTurn,
1433
+ getActiveToolExecutions: deps.getActiveToolExecutions,
1434
+ setActiveToolExecutions: deps.setActiveToolExecutions,
1435
+ triggerPendingModelSwitchAbort: () => {
1436
+ deps.triggerPendingModelSwitchAbort(ctx);
1437
+ },
1438
+ });
1439
+ },
1440
+ };
1441
+ }
1442
+
1443
+ // --- Agent End Lifecycle ---
1444
+
1445
+ export interface TelegramAgentEndPlan {
1446
+ kind: "no-turn" | "aborted" | "error" | "text" | "attachments-only" | "empty";
1447
+ shouldClearPreview: boolean;
1448
+ shouldDispatchNext: boolean;
1449
+ shouldSendErrorMessage: boolean;
1450
+ shouldSendAttachmentNotice: boolean;
1451
+ }
1452
+
1453
+ export interface TelegramAgentEndAssistantResult {
1454
+ text?: string;
1455
+ stopReason?: string;
1456
+ errorMessage?: string;
1457
+ }
1458
+
1459
+ export interface TelegramAgentEndOutboundVoiceReply {
1460
+ text: string;
1461
+ lang?: string;
1462
+ rate?: string;
1463
+ }
1464
+
1465
+ export interface TelegramAgentEndOutboundReplyPlan<TReplyMarkup = unknown> {
1466
+ markdown: string;
1467
+ replyMarkup?: TReplyMarkup;
1468
+ voiceText?: string;
1469
+ voiceReplies?: TelegramAgentEndOutboundVoiceReply[];
1470
+ lang?: string;
1471
+ rate?: string;
1472
+ }
1473
+
1474
+ export interface TelegramAgentEndRuntimeDeps<
1475
+ TTurn extends PendingTelegramTurn,
1476
+ TReplyMarkup = unknown,
1477
+ > {
1478
+ turn: TTurn | undefined;
1479
+ assistant: TelegramAgentEndAssistantResult;
1480
+ foldQueuedPromptsIntoHistory: boolean;
1481
+ resetRuntimeState: () => void;
1482
+ isSessionActive?: () => boolean;
1483
+ isTurnTransportActive?: (turn: TTurn) => boolean;
1484
+ waitForTypingIdle?: () => Promise<void>;
1485
+ waitForActivityIdle?: () => Promise<void>;
1486
+ updateStatus: () => void;
1487
+ dispatchNextQueuedTelegramTurn: () => void;
1488
+ scheduleActiveTurnDelivery?: (task: () => Promise<void>) => void;
1489
+ clearPreview: (
1490
+ chatId: number,
1491
+ options?: { target?: TelegramQueueTarget },
1492
+ ) => Promise<void>;
1493
+ setPreviewPendingText: (text: string) => void;
1494
+ finalizeMarkdownPreview: (
1495
+ chatId: number,
1496
+ markdown: string,
1497
+ replyToMessageId: number,
1498
+ options?: { replyMarkup?: TReplyMarkup; target?: TelegramQueueTarget },
1499
+ ) => Promise<boolean>;
1500
+ sendMarkdownReply: (
1501
+ chatId: number,
1502
+ replyToMessageId: number | undefined,
1503
+ markdown: string,
1504
+ options?: { replyMarkup?: TReplyMarkup; target?: TelegramQueueTarget },
1505
+ ) => Promise<unknown>;
1506
+ sendTextReply: (
1507
+ chatId: number,
1508
+ replyToMessageId: number,
1509
+ text: string,
1510
+ options?: { target?: TelegramQueueTarget },
1511
+ ) => Promise<unknown>;
1512
+ sendQueuedAttachments: (turn: TTurn) => Promise<void>;
1513
+ sendRichAttachmentReply?: (
1514
+ turn: TTurn,
1515
+ markdown: string,
1516
+ options?: { replyMarkup?: TReplyMarkup },
1517
+ ) => Promise<boolean>;
1518
+ answerGuestQuery?: (
1519
+ guestQueryId: string,
1520
+ text?: string,
1521
+ options?: { parseMode?: string },
1522
+ ) => Promise<void>;
1523
+ sendGuestReply?: (guestQueryId: string, markdown: string) => Promise<void>;
1524
+ sendGuestAttachment?: (
1525
+ turn: TTurn,
1526
+ attachment: QueuedAttachment,
1527
+ caption?: string,
1528
+ ) => Promise<void>;
1529
+ sendGuestVoiceReply?: (
1530
+ turn: TTurn,
1531
+ plan: TelegramAgentEndOutboundReplyPlan<TReplyMarkup>,
1532
+ caption?: string,
1533
+ ) => Promise<void>;
1534
+ planOutboundReply?: (
1535
+ markdown: string,
1536
+ ) => TelegramAgentEndOutboundReplyPlan<TReplyMarkup>;
1537
+ sendOutboundReplyArtifacts?: (
1538
+ turn: TTurn,
1539
+ plan: TelegramAgentEndOutboundReplyPlan,
1540
+ options?: { replyToPrompt?: boolean },
1541
+ ) => Promise<void>;
1542
+ recordRuntimeEvent?: (
1543
+ category: string,
1544
+ error: unknown,
1545
+ details?: Record<string, unknown>,
1546
+ ) => void;
1547
+ }
1548
+
1549
+ export interface TelegramAgentEndHookRuntimeDeps<
1550
+ TTurn extends PendingTelegramTurn,
1551
+ TContext,
1552
+ TMessage,
1553
+ TReplyMarkup = unknown,
1554
+ > {
1555
+ getActiveTurn: () => TTurn | undefined;
1556
+ loadConfig?: () => Promise<void>;
1557
+ extractAssistant: (
1558
+ messages: readonly TMessage[],
1559
+ ) => TelegramAgentEndAssistantResult;
1560
+ getFoldQueuedPromptsIntoHistory: () => boolean;
1561
+ resetRuntimeState: () => void;
1562
+ isSessionActive?: (ctx: TContext) => boolean;
1563
+ isTurnTransportActive?: (turn: TTurn) => boolean;
1564
+ waitForTypingIdle?: () => Promise<void>;
1565
+ waitForActivityIdle?: () => Promise<void>;
1566
+ updateStatus: (ctx: TContext) => void;
1567
+ dispatchNextQueuedTelegramTurn: (ctx: TContext) => void;
1568
+ requestDeferredDispatchNextQueuedTelegramTurn: (
1569
+ dispatch: (ctx: TContext) => void,
1570
+ ) => void;
1571
+ scheduleActiveTurnDelivery?: TelegramAgentEndRuntimeDeps<
1572
+ TTurn,
1573
+ TReplyMarkup
1574
+ >["scheduleActiveTurnDelivery"];
1575
+ clearPreview: TelegramAgentEndRuntimeDeps<
1576
+ TTurn,
1577
+ TReplyMarkup
1578
+ >["clearPreview"];
1579
+ setPreviewPendingText: (text: string) => void;
1580
+ finalizeMarkdownPreview: TelegramAgentEndRuntimeDeps<
1581
+ TTurn,
1582
+ TReplyMarkup
1583
+ >["finalizeMarkdownPreview"];
1584
+ sendMarkdownReply: TelegramAgentEndRuntimeDeps<
1585
+ TTurn,
1586
+ TReplyMarkup
1587
+ >["sendMarkdownReply"];
1588
+ sendTextReply: TelegramAgentEndRuntimeDeps<TTurn>["sendTextReply"];
1589
+ sendQueuedAttachments: (turn: TTurn) => Promise<void>;
1590
+ sendRichAttachmentReply?: TelegramAgentEndRuntimeDeps<
1591
+ TTurn,
1592
+ TReplyMarkup
1593
+ >["sendRichAttachmentReply"];
1594
+ answerGuestQuery?: TelegramAgentEndRuntimeDeps<TTurn>["answerGuestQuery"];
1595
+ sendGuestReply?: TelegramAgentEndRuntimeDeps<TTurn>["sendGuestReply"];
1596
+ sendGuestAttachment?: TelegramAgentEndRuntimeDeps<TTurn>["sendGuestAttachment"];
1597
+ sendGuestVoiceReply?: TelegramAgentEndRuntimeDeps<TTurn>["sendGuestVoiceReply"];
1598
+ planOutboundReply?: TelegramAgentEndRuntimeDeps<
1599
+ TTurn,
1600
+ TReplyMarkup
1601
+ >["planOutboundReply"];
1602
+ sendOutboundReplyArtifacts?: TelegramAgentEndRuntimeDeps<TTurn>["sendOutboundReplyArtifacts"];
1603
+ recordRuntimeEvent?: TelegramAgentEndRuntimeDeps<TTurn>["recordRuntimeEvent"];
1604
+ }
1605
+
1606
+ export interface TelegramAgentEndHookEvent<TMessage> {
1607
+ messages: readonly TMessage[];
1608
+ }
1609
+
1610
+ export function buildTelegramAgentEndPlan(options: {
1611
+ hasTurn: boolean;
1612
+ stopReason?: string;
1613
+ hasFinalText: boolean;
1614
+ hasQueuedAttachments: boolean;
1615
+ foldQueuedPromptsIntoHistory: boolean;
1616
+ }): TelegramAgentEndPlan {
1617
+ const shouldDispatchNext = shouldDispatchAfterTelegramAgentEnd({
1618
+ hasTurn: options.hasTurn,
1619
+ stopReason: options.stopReason,
1620
+ foldQueuedPromptsIntoHistory: options.foldQueuedPromptsIntoHistory,
1621
+ });
1622
+ if (!options.hasTurn) {
1623
+ return {
1624
+ kind: "no-turn",
1625
+ shouldClearPreview: false,
1626
+ shouldDispatchNext,
1627
+ shouldSendErrorMessage: false,
1628
+ shouldSendAttachmentNotice: false,
1629
+ };
1630
+ }
1631
+ if (options.stopReason === "aborted") {
1632
+ return {
1633
+ kind: "aborted",
1634
+ shouldClearPreview: true,
1635
+ shouldDispatchNext,
1636
+ shouldSendErrorMessage: false,
1637
+ shouldSendAttachmentNotice: false,
1638
+ };
1639
+ }
1640
+ if (options.stopReason === "error") {
1641
+ return {
1642
+ kind: "error",
1643
+ shouldClearPreview: true,
1644
+ shouldDispatchNext,
1645
+ shouldSendErrorMessage: true,
1646
+ shouldSendAttachmentNotice: false,
1647
+ };
1648
+ }
1649
+ if (options.hasFinalText) {
1650
+ return {
1651
+ kind: "text",
1652
+ shouldClearPreview: false,
1653
+ shouldDispatchNext,
1654
+ shouldSendErrorMessage: false,
1655
+ shouldSendAttachmentNotice: false,
1656
+ };
1657
+ }
1658
+ if (options.hasQueuedAttachments) {
1659
+ return {
1660
+ kind: "attachments-only",
1661
+ shouldClearPreview: true,
1662
+ shouldDispatchNext,
1663
+ shouldSendErrorMessage: false,
1664
+ shouldSendAttachmentNotice: true,
1665
+ };
1666
+ }
1667
+ return {
1668
+ kind: "empty",
1669
+ shouldClearPreview: true,
1670
+ shouldDispatchNext,
1671
+ shouldSendErrorMessage: false,
1672
+ shouldSendAttachmentNotice: false,
1673
+ };
1674
+ }
1675
+
1676
+ export function createTelegramAgentEndHook<
1677
+ TTurn extends PendingTelegramTurn,
1678
+ TContext,
1679
+ TMessage,
1680
+ TReplyMarkup = unknown,
1681
+ >(
1682
+ deps: TelegramAgentEndHookRuntimeDeps<
1683
+ TTurn,
1684
+ TContext,
1685
+ TMessage,
1686
+ TReplyMarkup
1687
+ >,
1688
+ ) {
1689
+ return async (
1690
+ event: TelegramAgentEndHookEvent<TMessage>,
1691
+ ctx: TContext,
1692
+ assistantOverride?: TelegramAgentEndAssistantResult,
1693
+ ): Promise<void> => {
1694
+ await deps.loadConfig?.();
1695
+ if (deps.isSessionActive && !deps.isSessionActive(ctx)) return;
1696
+ const turn = deps.getActiveTurn();
1697
+ await handleTelegramAgentEndRuntime({
1698
+ turn,
1699
+ assistant:
1700
+ assistantOverride ??
1701
+ (turn ? deps.extractAssistant(event.messages) : {}),
1702
+ foldQueuedPromptsIntoHistory: deps.getFoldQueuedPromptsIntoHistory(),
1703
+ resetRuntimeState: deps.resetRuntimeState,
1704
+ isSessionActive: () => deps.isSessionActive?.(ctx) ?? true,
1705
+ isTurnTransportActive: deps.isTurnTransportActive,
1706
+ waitForTypingIdle: deps.waitForTypingIdle,
1707
+ waitForActivityIdle: deps.waitForActivityIdle,
1708
+ updateStatus: () => deps.updateStatus(ctx),
1709
+ dispatchNextQueuedTelegramTurn: () => {
1710
+ deps.requestDeferredDispatchNextQueuedTelegramTurn(
1711
+ deps.dispatchNextQueuedTelegramTurn,
1712
+ );
1713
+ },
1714
+ scheduleActiveTurnDelivery: deps.scheduleActiveTurnDelivery
1715
+ ? (task) =>
1716
+ deps.scheduleActiveTurnDelivery?.(async () => {
1717
+ if (deps.isSessionActive?.(ctx) === false) return;
1718
+ await task();
1719
+ })
1720
+ : undefined,
1721
+ clearPreview: deps.clearPreview,
1722
+ setPreviewPendingText: deps.setPreviewPendingText,
1723
+ finalizeMarkdownPreview: deps.finalizeMarkdownPreview,
1724
+ sendMarkdownReply: deps.sendMarkdownReply,
1725
+ sendTextReply: deps.sendTextReply,
1726
+ sendQueuedAttachments: deps.sendQueuedAttachments,
1727
+ sendRichAttachmentReply: deps.sendRichAttachmentReply,
1728
+ answerGuestQuery: deps.answerGuestQuery,
1729
+ sendGuestReply: deps.sendGuestReply,
1730
+ sendGuestAttachment: deps.sendGuestAttachment,
1731
+ sendGuestVoiceReply: deps.sendGuestVoiceReply,
1732
+ planOutboundReply: deps.planOutboundReply,
1733
+ sendOutboundReplyArtifacts: deps.sendOutboundReplyArtifacts,
1734
+ recordRuntimeEvent: deps.recordRuntimeEvent,
1735
+ });
1736
+ };
1737
+ }
1738
+
1739
+ export async function handleTelegramAgentEndRuntime<
1740
+ TTurn extends PendingTelegramTurn,
1741
+ TReplyMarkup = unknown,
1742
+ >(deps: TelegramAgentEndRuntimeDeps<TTurn, TReplyMarkup>): Promise<void> {
1743
+ const { turn, assistant } = deps;
1744
+ const rawFinalText = assistant.text;
1745
+ let outboundReply = rawFinalText
1746
+ ? deps.planOutboundReply?.(rawFinalText)
1747
+ : undefined;
1748
+ // Preserve the planned reply so voice-fallback can use stripped markdown + replyMarkup
1749
+ const plannedReply = outboundReply;
1750
+
1751
+ // Transparent voice interception: when the turn is voice-tagged and the agent
1752
+ // did not explicitly use <!-- telegram_voice --> markup, we automatically
1753
+ // convert the whole response to voice.
1754
+ const voiceInterceptionGuard =
1755
+ turn &&
1756
+ isVoiceTurn(turn) &&
1757
+ rawFinalText?.trim() &&
1758
+ deps.planOutboundReply &&
1759
+ (!outboundReply ||
1760
+ (!outboundReply.voiceText && !outboundReply.voiceReplies?.length));
1761
+ if (voiceInterceptionGuard) {
1762
+ const voiceText =
1763
+ plannedReply !== undefined
1764
+ ? plannedReply.markdown?.trim() || ""
1765
+ : (rawFinalText ?? "");
1766
+ outboundReply = outboundReply
1767
+ ? { ...outboundReply, voiceText, markdown: "" }
1768
+ : { markdown: "", voiceText };
1769
+ }
1770
+
1771
+ const finalText = outboundReply ? outboundReply.markdown : rawFinalText;
1772
+ const hasOutboundArtifacts =
1773
+ !!outboundReply?.voiceText || !!outboundReply?.voiceReplies?.length;
1774
+ const replyMarkup = outboundReply?.replyMarkup;
1775
+ const isDeliveryActive = (): boolean =>
1776
+ deps.isSessionActive?.() !== false &&
1777
+ (!turn || deps.isTurnTransportActive?.(turn) !== false);
1778
+ const updateStatusIgnoringStaleContext = (): void => {
1779
+ try {
1780
+ deps.updateStatus();
1781
+ } catch (error) {
1782
+ if (!isTelegramStaleContextError(error)) throw error;
1783
+ }
1784
+ };
1785
+ if (!isDeliveryActive()) {
1786
+ deps.resetRuntimeState();
1787
+ updateStatusIgnoringStaleContext();
1788
+ deps.dispatchNextQueuedTelegramTurn();
1789
+ return;
1790
+ }
1791
+ deps.resetRuntimeState();
1792
+ await deps.waitForTypingIdle?.();
1793
+ if (!isDeliveryActive()) {
1794
+ updateStatusIgnoringStaleContext();
1795
+ deps.dispatchNextQueuedTelegramTurn();
1796
+ return;
1797
+ }
1798
+ updateStatusIgnoringStaleContext();
1799
+ const endPlan = buildTelegramAgentEndPlan({
1800
+ hasTurn: !!turn,
1801
+ stopReason: assistant.stopReason,
1802
+ hasFinalText: !!finalText || hasOutboundArtifacts,
1803
+ hasQueuedAttachments: (turn?.queuedAttachments.length ?? 0) > 0,
1804
+ foldQueuedPromptsIntoHistory: deps.foldQueuedPromptsIntoHistory,
1805
+ });
1806
+ if (!turn) {
1807
+ if (endPlan.shouldDispatchNext) deps.dispatchNextQueuedTelegramTurn();
1808
+ return;
1809
+ }
1810
+ if (turn.guestQueryId) {
1811
+ if (assistant.errorMessage) {
1812
+ await deps.answerGuestQuery?.(
1813
+ turn.guestQueryId,
1814
+ "Telegram bridge: Pi failed while processing the request.",
1815
+ );
1816
+ if (endPlan.shouldDispatchNext) deps.dispatchNextQueuedTelegramTurn();
1817
+ return;
1818
+ }
1819
+ const [guestAttachment] = turn.queuedAttachments;
1820
+ if (guestAttachment && deps.sendGuestAttachment) {
1821
+ try {
1822
+ await deps.sendGuestAttachment(
1823
+ turn,
1824
+ guestAttachment,
1825
+ finalText || undefined,
1826
+ );
1827
+ } catch (error) {
1828
+ deps.recordRuntimeEvent?.("delivery", error, {
1829
+ phase: "guest-attachment",
1830
+ guestQueryId: turn.guestQueryId,
1831
+ });
1832
+ }
1833
+ } else if (
1834
+ outboundReply &&
1835
+ (outboundReply.voiceText || outboundReply.voiceReplies?.length) &&
1836
+ deps.sendGuestVoiceReply
1837
+ ) {
1838
+ try {
1839
+ await deps.sendGuestVoiceReply(
1840
+ turn,
1841
+ outboundReply,
1842
+ finalText || undefined,
1843
+ );
1844
+ } catch (error) {
1845
+ deps.recordRuntimeEvent?.("delivery", error, {
1846
+ phase: "guest-voice",
1847
+ guestQueryId: turn.guestQueryId,
1848
+ });
1849
+ }
1850
+ } else if (finalText) {
1851
+ if (deps.sendGuestReply) {
1852
+ await deps.sendGuestReply(turn.guestQueryId, finalText);
1853
+ } else {
1854
+ await deps.answerGuestQuery?.(turn.guestQueryId, finalText);
1855
+ }
1856
+ }
1857
+ if (!isDeliveryActive()) return;
1858
+ if (endPlan.shouldDispatchNext) deps.dispatchNextQueuedTelegramTurn();
1859
+ return;
1860
+ }
1861
+ if (endPlan.shouldClearPreview) {
1862
+ await deps.clearPreview(turn.chatId, { target: turn.target });
1863
+ }
1864
+ if (!isDeliveryActive()) return;
1865
+ if (endPlan.shouldSendErrorMessage) {
1866
+ await deps.sendTextReply(
1867
+ turn.chatId,
1868
+ turn.replyToMessageId,
1869
+ assistant.errorMessage ||
1870
+ "Telegram bridge: Pi failed while processing the request.",
1871
+ { target: turn.target },
1872
+ );
1873
+ if (!isDeliveryActive()) return;
1874
+ if (endPlan.shouldDispatchNext) deps.dispatchNextQueuedTelegramTurn();
1875
+ return;
1876
+ }
1877
+ const deliverActiveTurn = async () => {
1878
+ await deps.waitForActivityIdle?.();
1879
+ if (!isDeliveryActive()) return;
1880
+ if (finalText) deps.setPreviewPendingText(finalText);
1881
+ if (!finalText && hasOutboundArtifacts)
1882
+ await deps.clearPreview(turn.chatId, { target: turn.target });
1883
+ if (!isDeliveryActive()) return;
1884
+ let richAttachmentDelivered = false;
1885
+ if (
1886
+ endPlan.kind === "text" &&
1887
+ finalText &&
1888
+ !hasOutboundArtifacts &&
1889
+ deps.sendRichAttachmentReply
1890
+ ) {
1891
+ try {
1892
+ richAttachmentDelivered = await deps.sendRichAttachmentReply(
1893
+ turn,
1894
+ finalText,
1895
+ { replyMarkup },
1896
+ );
1897
+ if (!isDeliveryActive()) return;
1898
+ if (richAttachmentDelivered) {
1899
+ await deps.clearPreview(turn.chatId, { target: turn.target });
1900
+ }
1901
+ } catch (error) {
1902
+ deps.recordRuntimeEvent?.("delivery", error, {
1903
+ phase: "rich-attachment-commit-unknown",
1904
+ chatId: turn.chatId,
1905
+ });
1906
+ if (endPlan.shouldDispatchNext) deps.dispatchNextQueuedTelegramTurn();
1907
+ return;
1908
+ }
1909
+ }
1910
+ if (!isDeliveryActive()) return;
1911
+ if (!richAttachmentDelivered && endPlan.kind === "text" && finalText) {
1912
+ try {
1913
+ const finalized = await deps.finalizeMarkdownPreview(
1914
+ turn.chatId,
1915
+ finalText,
1916
+ turn.replyToMessageId,
1917
+ { replyMarkup, target: turn.target },
1918
+ );
1919
+ if (!isDeliveryActive()) return;
1920
+ if (!finalized) {
1921
+ await deps.clearPreview(turn.chatId, { target: turn.target });
1922
+ if (!isDeliveryActive()) return;
1923
+ await deps.sendMarkdownReply(
1924
+ turn.chatId,
1925
+ turn.replyToMessageId,
1926
+ finalText,
1927
+ { replyMarkup, target: turn.target },
1928
+ );
1929
+ }
1930
+ } catch (error) {
1931
+ deps.recordRuntimeEvent?.("delivery", error, {
1932
+ phase: "final-text",
1933
+ chatId: turn.chatId,
1934
+ replyToMessageId: turn.replyToMessageId,
1935
+ });
1936
+ }
1937
+ }
1938
+ if (!isDeliveryActive()) return;
1939
+ if (outboundReply && deps.sendOutboundReplyArtifacts) {
1940
+ try {
1941
+ await deps.sendOutboundReplyArtifacts(turn, outboundReply, {
1942
+ replyToPrompt: !finalText,
1943
+ });
1944
+ if (!isDeliveryActive()) return;
1945
+ } catch (error) {
1946
+ deps.recordRuntimeEvent?.("delivery", error, {
1947
+ phase: "voice-artifacts",
1948
+ chatId: turn.chatId,
1949
+ });
1950
+ // Fallback to planned text when voice delivery fails and text wasn't already delivered
1951
+ if (!isDeliveryActive()) return;
1952
+ if (rawFinalText?.trim() && !finalText && hasOutboundArtifacts) {
1953
+ try {
1954
+ const fallbackMarkdown =
1955
+ plannedReply?.markdown ||
1956
+ outboundReply?.voiceText ||
1957
+ rawFinalText;
1958
+ await deps.sendMarkdownReply(
1959
+ turn.chatId,
1960
+ turn.replyToMessageId,
1961
+ fallbackMarkdown,
1962
+ plannedReply?.replyMarkup || turn.target
1963
+ ? {
1964
+ replyMarkup: plannedReply?.replyMarkup,
1965
+ target: turn.target,
1966
+ }
1967
+ : undefined,
1968
+ );
1969
+ } catch (fallbackError) {
1970
+ deps.recordRuntimeEvent?.("delivery", fallbackError, {
1971
+ phase: "voice-fallback-text",
1972
+ chatId: turn.chatId,
1973
+ });
1974
+ }
1975
+ }
1976
+ }
1977
+ }
1978
+ if (!isDeliveryActive()) return;
1979
+ if (!richAttachmentDelivered && endPlan.shouldSendAttachmentNotice) {
1980
+ await deps.sendTextReply(
1981
+ turn.chatId,
1982
+ turn.replyToMessageId,
1983
+ "Attached requested file(s).",
1984
+ { target: turn.target },
1985
+ );
1986
+ }
1987
+ if (!isDeliveryActive()) return;
1988
+ if (!richAttachmentDelivered) await deps.sendQueuedAttachments(turn);
1989
+ if (!isDeliveryActive()) return;
1990
+ if (endPlan.shouldDispatchNext) deps.dispatchNextQueuedTelegramTurn();
1991
+ };
1992
+ if (
1993
+ deps.scheduleActiveTurnDelivery &&
1994
+ (endPlan.kind === "text" || endPlan.kind === "attachments-only")
1995
+ ) {
1996
+ deps.scheduleActiveTurnDelivery(deliverActiveTurn);
1997
+ return;
1998
+ }
1999
+ await deliverActiveTurn();
2000
+ }
2001
+
2002
+ // --- Session Runtime ---
2003
+
2004
+ export interface TelegramSessionStartState<TModel = unknown> {
2005
+ currentTelegramModel: TModel | undefined;
2006
+ activeTelegramToolExecutions: number;
2007
+ pendingTelegramModelSwitch: undefined;
2008
+ nextQueuedTelegramItemOrder: number;
2009
+ nextQueuedTelegramControlOrder: number;
2010
+ telegramTurnDispatchPending: boolean;
2011
+ compactionInProgress: boolean;
2012
+ }
2013
+
2014
+ export interface TelegramSessionShutdownState<TQueueItem> {
2015
+ queuedTelegramItems: TQueueItem[];
2016
+ nextQueuedTelegramItemOrder: number;
2017
+ nextQueuedTelegramControlOrder: number;
2018
+ currentTelegramModel: undefined;
2019
+ activeTelegramToolExecutions: number;
2020
+ pendingTelegramModelSwitch: undefined;
2021
+ telegramTurnDispatchPending: boolean;
2022
+ compactionInProgress: boolean;
2023
+ foldQueuedPromptsIntoHistory: boolean;
2024
+ }
2025
+
2026
+ export interface TelegramSessionRuntimeCounterState {
2027
+ nextQueuedTelegramItemOrder?: number;
2028
+ nextQueuedTelegramControlOrder?: number;
2029
+ }
2030
+
2031
+ export interface TelegramSessionRuntimeFlagState {
2032
+ activeTelegramToolExecutions?: number;
2033
+ telegramTurnDispatchPending?: boolean;
2034
+ compactionInProgress?: boolean;
2035
+ foldQueuedPromptsIntoHistory?: boolean;
2036
+ }
2037
+
2038
+ export interface TelegramSessionStateApplierDeps<TQueueItem, TModel> {
2039
+ setQueuedItems: (items: TQueueItem[]) => void;
2040
+ setCurrentModel: (model: TModel | undefined) => void;
2041
+ setPendingModelSwitch: (selection: undefined) => void;
2042
+ syncCounters: (state: TelegramSessionRuntimeCounterState) => void;
2043
+ syncFlags: (state: TelegramSessionRuntimeFlagState) => void;
2044
+ }
2045
+
2046
+ export interface TelegramSessionStateApplier<TQueueItem, TModel> {
2047
+ applyStartState: (state: TelegramSessionStartState<TModel>) => void;
2048
+ applyShutdownState: (state: TelegramSessionShutdownState<TQueueItem>) => void;
2049
+ }
2050
+
2051
+ export interface TelegramSessionStartRuntimeDeps<TContext, TModel = unknown> {
2052
+ ctx: TContext;
2053
+ currentModel: TModel | undefined;
2054
+ loadConfig: () => Promise<void>;
2055
+ isSessionActive?: () => boolean;
2056
+ applyState: (state: TelegramSessionStartState<TModel>) => void;
2057
+ bindDeferredDispatchContext?: (ctx: TContext) => void;
2058
+ prepareTempDir: () => Promise<unknown>;
2059
+ updateStatus: () => void;
2060
+ }
2061
+
2062
+ export interface TelegramSessionShutdownRuntimeDeps<TQueueItem> {
2063
+ isSessionActive?: () => boolean;
2064
+ unbindDeferredDispatchContext?: () => void;
2065
+ applyState: (state: TelegramSessionShutdownState<TQueueItem>) => void;
2066
+ clearPendingMediaGroups: () => void;
2067
+ clearModelMenuState: () => void;
2068
+ getActiveTurnChatId: () => number | undefined;
2069
+ getActiveTurnTarget?: () => TelegramQueueTarget | undefined;
2070
+ clearPreview: (
2071
+ chatId: number,
2072
+ options?: { target?: TelegramQueueTarget },
2073
+ ) => Promise<void>;
2074
+ previewShutdownTimeoutMs?: number;
2075
+ clearActiveTurn: () => void;
2076
+ clearAbort: () => void;
2077
+ stopPolling: () => Promise<void>;
2078
+ }
2079
+
2080
+ export interface TelegramSessionLifecycleHookRuntimeDeps<
2081
+ TContext,
2082
+ TQueueItem,
2083
+ TModel = unknown,
2084
+ > extends TelegramRuntimeEventRecorderPort {
2085
+ getCurrentModel: (ctx: TContext) => TModel | undefined;
2086
+ loadConfig: () => Promise<void>;
2087
+ applySessionStartState: (state: TelegramSessionStartState<TModel>) => void;
2088
+ bindDeferredDispatchContext?: (ctx: TContext) => void;
2089
+ prepareTempDir: () => Promise<unknown>;
2090
+ updateStatus: (ctx: TContext) => void;
2091
+ isSessionActive?: (ctx: TContext) => boolean;
2092
+ unbindDeferredDispatchContext?: () => void;
2093
+ applySessionShutdownState: (
2094
+ state: TelegramSessionShutdownState<TQueueItem>,
2095
+ ) => void;
2096
+ clearPendingMediaGroups: () => void;
2097
+ clearModelMenuState: () => void;
2098
+ getActiveTurnChatId: () => number | undefined;
2099
+ getActiveTurnTarget?: () => TelegramQueueTarget | undefined;
2100
+ clearPreview: (
2101
+ chatId: number,
2102
+ options?: { target?: TelegramQueueTarget },
2103
+ ) => Promise<void>;
2104
+ previewShutdownTimeoutMs?: number;
2105
+ clearActiveTurn: () => void;
2106
+ clearAbort: () => void;
2107
+ stopPolling: () => Promise<void>;
2108
+ }
2109
+
2110
+ export type TelegramSessionLifecycleHookEvent = unknown;
2111
+
2112
+ export function createTelegramSessionStateApplier<TQueueItem, TModel>(
2113
+ deps: TelegramSessionStateApplierDeps<TQueueItem, TModel>,
2114
+ ): TelegramSessionStateApplier<TQueueItem, TModel> {
2115
+ return {
2116
+ applyStartState: (state) => {
2117
+ deps.setCurrentModel(state.currentTelegramModel);
2118
+ deps.setPendingModelSwitch(state.pendingTelegramModelSwitch);
2119
+ deps.syncCounters(state);
2120
+ deps.syncFlags(state);
2121
+ },
2122
+ applyShutdownState: (state) => {
2123
+ deps.setQueuedItems(state.queuedTelegramItems);
2124
+ deps.syncCounters(state);
2125
+ deps.syncFlags(state);
2126
+ deps.setCurrentModel(state.currentTelegramModel);
2127
+ deps.setPendingModelSwitch(state.pendingTelegramModelSwitch);
2128
+ },
2129
+ };
2130
+ }
2131
+
2132
+ export interface TelegramQueueMutationRuntimeDeps<
2133
+ TContext,
2134
+ > extends TelegramQueueStore<TContext>, TelegramRuntimeEventRecorderPort {
2135
+ ctx: TContext;
2136
+ allocateLaneOrder?: () => number;
2137
+ onItemsDiscarded?: (
2138
+ items: readonly TelegramQueueItem<TContext>[],
2139
+ ctx: TContext,
2140
+ ) => void;
2141
+ updateStatus: (ctx: TContext) => void;
2142
+ }
2143
+
2144
+ export interface TelegramQueueMutationControllerDeps<
2145
+ TContext,
2146
+ > extends TelegramQueueStore<TContext>, TelegramRuntimeEventRecorderPort {
2147
+ allocateLaneOrder?: () => number;
2148
+ onItemsDiscarded?: (
2149
+ items: readonly TelegramQueueItem<TContext>[],
2150
+ ctx: TContext,
2151
+ ) => void;
2152
+ updateStatus: (ctx: TContext) => void;
2153
+ }
2154
+
2155
+ export interface TelegramQueueMutationController<TContext> {
2156
+ append: (item: TelegramQueueItem<TContext>, ctx: TContext) => void;
2157
+ reorder: (ctx: TContext) => void;
2158
+ clear: (ctx: TContext) => number;
2159
+ removeByMessageIds: (
2160
+ messageIds: number[],
2161
+ ctx: TContext,
2162
+ scope?: TelegramQueueMessageScope,
2163
+ ) => number;
2164
+ applyReactionByMessageId: (
2165
+ messageId: number,
2166
+ disposition: TelegramQueueReactionDisposition,
2167
+ ctx: TContext,
2168
+ scope?: TelegramQueueMessageScope,
2169
+ ) => boolean;
2170
+ }
2171
+
2172
+ export interface TelegramControlQueueControllerDeps<TContext> {
2173
+ appendControlItem: (
2174
+ item: PendingTelegramControlItem<TContext>,
2175
+ ctx: TContext,
2176
+ ) => void;
2177
+ dispatchNextQueuedTelegramTurn: (ctx: TContext) => void;
2178
+ }
2179
+
2180
+ export interface TelegramControlQueueController<TContext> {
2181
+ enqueue: (
2182
+ item: PendingTelegramControlItem<TContext>,
2183
+ ctx: TContext,
2184
+ onQueued?: (item: PendingTelegramControlItem<TContext>) => void,
2185
+ ) => void;
2186
+ }
2187
+
2188
+ export interface TelegramPromptEnqueueRuntimeDeps<
2189
+ TMessage,
2190
+ TContext = unknown,
2191
+ > extends TelegramQueueStore<TContext> {
2192
+ getFoldQueuedPromptsIntoHistory: () => boolean;
2193
+ setFoldQueuedPromptsIntoHistory: (fold: boolean) => void;
2194
+ createTurn: (
2195
+ messages: TMessage[],
2196
+ historyTurns: PendingTelegramTurn[],
2197
+ ) => Promise<PendingTelegramTurn>;
2198
+ updateStatus: () => void;
2199
+ dispatchNextQueuedTelegramTurn: () => void;
2200
+ assertExecutionCurrent?: () => void;
2201
+ onQueued?: (turn: PendingTelegramTurn) => void;
2202
+ }
2203
+
2204
+ export interface TelegramPromptEnqueueControllerDeps<
2205
+ TMessage,
2206
+ TContext = unknown,
2207
+ > extends TelegramQueueStore<TContext> {
2208
+ getFoldQueuedPromptsIntoHistory: () => boolean;
2209
+ setFoldQueuedPromptsIntoHistory: (fold: boolean) => void;
2210
+ createTurn: (
2211
+ messages: TMessage[],
2212
+ historyTurns: PendingTelegramTurn[],
2213
+ ctx: TContext,
2214
+ ) => Promise<PendingTelegramTurn>;
2215
+ updateStatus: (ctx: TContext) => void;
2216
+ dispatchNextQueuedTelegramTurn: (ctx: TContext) => void;
2217
+ assertExecutionCurrent?: (messages: TMessage[]) => void;
2218
+ }
2219
+
2220
+ export interface TelegramPromptEnqueueController<TMessage, TContext = unknown> {
2221
+ enqueue: (
2222
+ messages: TMessage[],
2223
+ ctx: TContext,
2224
+ onQueued?: (turn: PendingTelegramTurn) => void,
2225
+ ) => Promise<PendingTelegramTurn>;
2226
+ }
2227
+
2228
+ function isTelegramStaleContextError(error: unknown): boolean {
2229
+ return (
2230
+ error instanceof Error &&
2231
+ (error.message.includes("stale after session") ||
2232
+ error.message.includes("stale ctx"))
2233
+ );
2234
+ }
2235
+
2236
+ export function buildTelegramSessionStartState<TModel = unknown>(
2237
+ currentModel: TModel | undefined,
2238
+ ): TelegramSessionStartState<TModel> {
2239
+ return {
2240
+ currentTelegramModel: currentModel,
2241
+ activeTelegramToolExecutions: 0,
2242
+ pendingTelegramModelSwitch: undefined,
2243
+ nextQueuedTelegramItemOrder: 0,
2244
+ nextQueuedTelegramControlOrder: 0,
2245
+ telegramTurnDispatchPending: false,
2246
+ compactionInProgress: false,
2247
+ };
2248
+ }
2249
+
2250
+ export function buildTelegramSessionShutdownState<
2251
+ TQueueItem,
2252
+ >(): TelegramSessionShutdownState<TQueueItem> {
2253
+ return {
2254
+ queuedTelegramItems: [],
2255
+ nextQueuedTelegramItemOrder: 0,
2256
+ nextQueuedTelegramControlOrder: 0,
2257
+ currentTelegramModel: undefined,
2258
+ activeTelegramToolExecutions: 0,
2259
+ pendingTelegramModelSwitch: undefined,
2260
+ telegramTurnDispatchPending: false,
2261
+ compactionInProgress: false,
2262
+ foldQueuedPromptsIntoHistory: false,
2263
+ };
2264
+ }
2265
+
2266
+ export async function startTelegramSessionRuntime<TContext, TModel = unknown>(
2267
+ deps: TelegramSessionStartRuntimeDeps<TContext, TModel>,
2268
+ ): Promise<void> {
2269
+ await deps.loadConfig();
2270
+ if (deps.isSessionActive?.() === false) return;
2271
+ deps.applyState(buildTelegramSessionStartState(deps.currentModel));
2272
+ await deps.prepareTempDir();
2273
+ if (deps.isSessionActive?.() === false) return;
2274
+ try {
2275
+ deps.bindDeferredDispatchContext?.(deps.ctx);
2276
+ } catch (error) {
2277
+ if (!isTelegramStaleContextError(error)) throw error;
2278
+ }
2279
+ deps.updateStatus();
2280
+ }
2281
+
2282
+ export async function shutdownTelegramSessionRuntime<TQueueItem>(
2283
+ deps: TelegramSessionShutdownRuntimeDeps<TQueueItem>,
2284
+ ): Promise<void> {
2285
+ if (deps.isSessionActive?.() === false) return;
2286
+ deps.unbindDeferredDispatchContext?.();
2287
+ await deps.stopPolling();
2288
+ if (deps.isSessionActive?.() === false) return;
2289
+ deps.applyState(buildTelegramSessionShutdownState<TQueueItem>());
2290
+ deps.clearPendingMediaGroups();
2291
+ deps.clearModelMenuState();
2292
+ const activeTurnChatId = deps.getActiveTurnChatId();
2293
+ if (activeTurnChatId !== undefined) {
2294
+ const target = deps.getActiveTurnTarget?.();
2295
+ const previewTimeoutMs = deps.previewShutdownTimeoutMs ?? 1000;
2296
+ let timeout: ReturnType<typeof setTimeout> | undefined;
2297
+ await Promise.race([
2298
+ deps.clearPreview(activeTurnChatId, target ? { target } : undefined),
2299
+ new Promise<void>((resolve) => {
2300
+ timeout = setTimeout(resolve, previewTimeoutMs);
2301
+ }),
2302
+ ]).finally(() => {
2303
+ if (timeout) clearTimeout(timeout);
2304
+ });
2305
+ if (deps.isSessionActive?.() === false) return;
2306
+ }
2307
+ deps.clearActiveTurn();
2308
+ deps.clearAbort();
2309
+ }
2310
+
2311
+ export type TelegramSessionLifecycleRuntimeDeps<
2312
+ TContext,
2313
+ TQueueItem,
2314
+ TModel = unknown,
2315
+ > = Omit<
2316
+ TelegramSessionLifecycleHookRuntimeDeps<TContext, TQueueItem, TModel>,
2317
+ "applySessionStartState" | "applySessionShutdownState"
2318
+ > &
2319
+ TelegramSessionStateApplierDeps<TQueueItem, TModel>;
2320
+
2321
+ export function createTelegramSessionLifecycleRuntime<
2322
+ TContext,
2323
+ TQueueItem,
2324
+ TModel = unknown,
2325
+ >(deps: TelegramSessionLifecycleRuntimeDeps<TContext, TQueueItem, TModel>) {
2326
+ const stateApplier = createTelegramSessionStateApplier({
2327
+ setQueuedItems: deps.setQueuedItems,
2328
+ setCurrentModel: deps.setCurrentModel,
2329
+ setPendingModelSwitch: deps.setPendingModelSwitch,
2330
+ syncCounters: deps.syncCounters,
2331
+ syncFlags: deps.syncFlags,
2332
+ });
2333
+ return createTelegramSessionLifecycleHooks({
2334
+ getCurrentModel: deps.getCurrentModel,
2335
+ loadConfig: deps.loadConfig,
2336
+ applySessionStartState: stateApplier.applyStartState,
2337
+ bindDeferredDispatchContext: deps.bindDeferredDispatchContext,
2338
+ prepareTempDir: deps.prepareTempDir,
2339
+ updateStatus: deps.updateStatus,
2340
+ isSessionActive: deps.isSessionActive,
2341
+ unbindDeferredDispatchContext: deps.unbindDeferredDispatchContext,
2342
+ applySessionShutdownState: stateApplier.applyShutdownState,
2343
+ clearPendingMediaGroups: deps.clearPendingMediaGroups,
2344
+ clearModelMenuState: deps.clearModelMenuState,
2345
+ getActiveTurnChatId: deps.getActiveTurnChatId,
2346
+ getActiveTurnTarget: deps.getActiveTurnTarget,
2347
+ clearPreview: deps.clearPreview,
2348
+ clearActiveTurn: deps.clearActiveTurn,
2349
+ clearAbort: deps.clearAbort,
2350
+ stopPolling: deps.stopPolling,
2351
+ recordRuntimeEvent: deps.recordRuntimeEvent,
2352
+ });
2353
+ }
2354
+
2355
+ export function createTelegramSessionLifecycleHooks<
2356
+ TContext,
2357
+ TQueueItem,
2358
+ TModel = unknown,
2359
+ >(deps: TelegramSessionLifecycleHookRuntimeDeps<TContext, TQueueItem, TModel>) {
2360
+ return {
2361
+ onSessionStart: async (
2362
+ _event: TelegramSessionLifecycleHookEvent,
2363
+ ctx: TContext,
2364
+ ): Promise<void> => {
2365
+ try {
2366
+ await startTelegramSessionRuntime({
2367
+ ctx,
2368
+ currentModel: deps.getCurrentModel(ctx),
2369
+ loadConfig: deps.loadConfig,
2370
+ isSessionActive: () => deps.isSessionActive?.(ctx) ?? true,
2371
+ applyState: deps.applySessionStartState,
2372
+ bindDeferredDispatchContext: deps.bindDeferredDispatchContext,
2373
+ prepareTempDir: deps.prepareTempDir,
2374
+ updateStatus: () => deps.updateStatus(ctx),
2375
+ });
2376
+ } catch (error) {
2377
+ deps.recordRuntimeEvent?.("session", error, { phase: "start" });
2378
+ throw error;
2379
+ }
2380
+ },
2381
+ onSessionShutdown: async (
2382
+ _event?: TelegramSessionLifecycleHookEvent,
2383
+ ctx?: TContext,
2384
+ ): Promise<void> => {
2385
+ try {
2386
+ await shutdownTelegramSessionRuntime<TQueueItem>({
2387
+ isSessionActive: () =>
2388
+ ctx === undefined ? true : (deps.isSessionActive?.(ctx) ?? true),
2389
+ unbindDeferredDispatchContext: deps.unbindDeferredDispatchContext,
2390
+ applyState: deps.applySessionShutdownState,
2391
+ clearPendingMediaGroups: deps.clearPendingMediaGroups,
2392
+ clearModelMenuState: deps.clearModelMenuState,
2393
+ getActiveTurnChatId: deps.getActiveTurnChatId,
2394
+ getActiveTurnTarget: deps.getActiveTurnTarget,
2395
+ clearPreview: deps.clearPreview,
2396
+ previewShutdownTimeoutMs: deps.previewShutdownTimeoutMs,
2397
+ clearActiveTurn: deps.clearActiveTurn,
2398
+ clearAbort: deps.clearAbort,
2399
+ stopPolling: deps.stopPolling,
2400
+ });
2401
+ } catch (error) {
2402
+ deps.recordRuntimeEvent?.("session", error, { phase: "shutdown" });
2403
+ throw error;
2404
+ }
2405
+ },
2406
+ };
2407
+ }
2408
+
2409
+ export function createTelegramQueueMutationController<TContext>(
2410
+ deps: TelegramQueueMutationControllerDeps<TContext>,
2411
+ ): TelegramQueueMutationController<TContext> {
2412
+ const buildRuntimeDeps = (
2413
+ ctx: TContext,
2414
+ ): TelegramQueueMutationRuntimeDeps<TContext> => ({
2415
+ ...deps,
2416
+ ctx,
2417
+ });
2418
+ return {
2419
+ append: (item, ctx) =>
2420
+ appendTelegramQueueItemRuntime(item, buildRuntimeDeps(ctx)),
2421
+ reorder: (ctx) => reorderTelegramQueueItemsRuntime(buildRuntimeDeps(ctx)),
2422
+ clear: (ctx) => clearTelegramQueueItemsRuntime(buildRuntimeDeps(ctx)),
2423
+ removeByMessageIds: (messageIds, ctx, scope) =>
2424
+ removeTelegramQueueItemsByMessageIdsRuntime(
2425
+ messageIds,
2426
+ buildRuntimeDeps(ctx),
2427
+ scope,
2428
+ ),
2429
+ applyReactionByMessageId: (messageId, disposition, ctx, scope) =>
2430
+ applyTelegramQueuePromptReactionDispositionRuntime(
2431
+ messageId,
2432
+ disposition,
2433
+ buildRuntimeDeps(ctx),
2434
+ scope,
2435
+ ),
2436
+ };
2437
+ }
2438
+
2439
+ function updateTelegramQueueStatusRuntime<TContext>(
2440
+ deps: TelegramQueueMutationRuntimeDeps<TContext>,
2441
+ ): void {
2442
+ try {
2443
+ deps.updateStatus(deps.ctx);
2444
+ } catch (error) {
2445
+ if (!isTelegramStaleContextError(error)) throw error;
2446
+ }
2447
+ }
2448
+
2449
+ function commitReorderedTelegramQueueItemsRuntime<TContext>(
2450
+ items: TelegramQueueItem<TContext>[],
2451
+ deps: TelegramQueueMutationRuntimeDeps<TContext>,
2452
+ ): void {
2453
+ deps.setQueuedItems([...items].sort(compareTelegramQueueItems));
2454
+ updateTelegramQueueStatusRuntime(deps);
2455
+ }
2456
+
2457
+ function appendTelegramQueueItemRuntime<TContext>(
2458
+ item: TelegramQueueItem<TContext>,
2459
+ deps: TelegramQueueMutationRuntimeDeps<TContext>,
2460
+ ): void {
2461
+ const currentItems = deps.getQueuedItems();
2462
+ const nextItems = appendTelegramQueueItem(currentItems, item);
2463
+ if (nextItems === currentItems) return;
2464
+ commitReorderedTelegramQueueItemsRuntime(nextItems, deps);
2465
+ }
2466
+
2467
+ export function reorderTelegramQueueItemsRuntime<TContext>(
2468
+ deps: TelegramQueueMutationRuntimeDeps<TContext>,
2469
+ ): void {
2470
+ commitReorderedTelegramQueueItemsRuntime(deps.getQueuedItems(), deps);
2471
+ }
2472
+
2473
+ export function clearTelegramQueueItemsRuntime<TContext>(
2474
+ deps: TelegramQueueMutationRuntimeDeps<TContext>,
2475
+ ): number {
2476
+ const removedItems = deps.getQueuedItems();
2477
+ const removedCount = removedItems.length;
2478
+ if (removedCount === 0) return 0;
2479
+ deps.setQueuedItems([]);
2480
+ try {
2481
+ deps.onItemsDiscarded?.(removedItems, deps.ctx);
2482
+ } catch (error) {
2483
+ deps.recordRuntimeEvent?.("queue", error, {
2484
+ phase: "discard-receipt-settlement",
2485
+ });
2486
+ }
2487
+ updateTelegramQueueStatusRuntime(deps);
2488
+ return removedCount;
2489
+ }
2490
+
2491
+ export function removeTelegramQueueItemsByMessageIdsRuntime<TContext>(
2492
+ messageIds: number[],
2493
+ deps: TelegramQueueMutationRuntimeDeps<TContext>,
2494
+ scope?: TelegramQueueMessageScope,
2495
+ ): number {
2496
+ const { items, removedItems, removedCount } =
2497
+ removeTelegramQueueItemsByMessageIds(
2498
+ deps.getQueuedItems(),
2499
+ messageIds,
2500
+ scope,
2501
+ );
2502
+ if (removedCount === 0) return 0;
2503
+ deps.setQueuedItems(items);
2504
+ try {
2505
+ deps.onItemsDiscarded?.(removedItems, deps.ctx);
2506
+ } catch (error) {
2507
+ deps.recordRuntimeEvent?.("queue", error, {
2508
+ phase: "discard-receipt-settlement",
2509
+ });
2510
+ }
2511
+ updateTelegramQueueStatusRuntime(deps);
2512
+ return removedCount;
2513
+ }
2514
+
2515
+ export function applyTelegramQueuePromptReactionDispositionRuntime<TContext>(
2516
+ messageId: number,
2517
+ disposition: TelegramQueueReactionDisposition,
2518
+ deps: TelegramQueueMutationRuntimeDeps<TContext>,
2519
+ scope?: TelegramQueueMessageScope,
2520
+ ): boolean {
2521
+ const queuedItems = deps.getQueuedItems();
2522
+ const changesLane = queuedItems.some((item) => {
2523
+ if (
2524
+ !isPendingTelegramTurn(item) ||
2525
+ !isTelegramQueueItemInMessageScope(item, scope) ||
2526
+ !item.sourceMessageIds.includes(messageId)
2527
+ ) {
2528
+ return false;
2529
+ }
2530
+ const queueLane: TelegramQueueLane =
2531
+ disposition.kind === "reaction-transition"
2532
+ ? disposition.priorityEmoji === undefined
2533
+ ? item.queueLane
2534
+ : disposition.priorityEmoji === null
2535
+ ? "default"
2536
+ : "priority"
2537
+ : disposition.kind === "priority" ||
2538
+ disposition.kind === "priority-suppressed"
2539
+ ? "priority"
2540
+ : "default";
2541
+ return item.queueLane !== queueLane;
2542
+ });
2543
+ const destinationLaneOrder = changesLane
2544
+ ? deps.allocateLaneOrder?.()
2545
+ : undefined;
2546
+ if (changesLane && destinationLaneOrder === undefined) return false;
2547
+ const { changed, items } = applyTelegramQueuePromptReactionDisposition(
2548
+ queuedItems,
2549
+ messageId,
2550
+ disposition,
2551
+ destinationLaneOrder,
2552
+ scope,
2553
+ );
2554
+ if (!changed) return false;
2555
+ commitReorderedTelegramQueueItemsRuntime(items, deps);
2556
+ return true;
2557
+ }
2558
+
2559
+ export async function enqueueTelegramPromptTurnRuntime<
2560
+ TMessage,
2561
+ TContext = unknown,
2562
+ >(
2563
+ messages: TMessage[],
2564
+ deps: TelegramPromptEnqueueRuntimeDeps<TMessage, TContext>,
2565
+ ): Promise<PendingTelegramTurn> {
2566
+ const enqueuePlan = planTelegramPromptEnqueue(
2567
+ deps.getQueuedItems(),
2568
+ deps.getFoldQueuedPromptsIntoHistory(),
2569
+ );
2570
+ deps.assertExecutionCurrent?.();
2571
+ deps.setFoldQueuedPromptsIntoHistory(false);
2572
+ const turn = await deps.createTurn(messages, enqueuePlan.historyTurns);
2573
+ deps.assertExecutionCurrent?.();
2574
+ deps.setQueuedItems(
2575
+ appendTelegramQueueItem(enqueuePlan.remainingItems, turn),
2576
+ );
2577
+ deps.onQueued?.(turn);
2578
+ deps.updateStatus();
2579
+ deps.dispatchNextQueuedTelegramTurn();
2580
+ return turn;
2581
+ }
2582
+
2583
+ export function createTelegramPromptEnqueueController<
2584
+ TMessage,
2585
+ TContext = unknown,
2586
+ >(
2587
+ deps: TelegramPromptEnqueueControllerDeps<TMessage, TContext>,
2588
+ ): TelegramPromptEnqueueController<TMessage, TContext> {
2589
+ return {
2590
+ enqueue: (messages, ctx, onQueued) =>
2591
+ enqueueTelegramPromptTurnRuntime(messages, {
2592
+ ...deps,
2593
+ createTurn: (nextMessages, historyTurns) =>
2594
+ deps.createTurn(nextMessages, historyTurns, ctx),
2595
+ updateStatus: () => deps.updateStatus(ctx),
2596
+ dispatchNextQueuedTelegramTurn: () =>
2597
+ deps.dispatchNextQueuedTelegramTurn(ctx),
2598
+ assertExecutionCurrent: () =>
2599
+ deps.assertExecutionCurrent?.(messages),
2600
+ onQueued,
2601
+ }),
2602
+ };
2603
+ }
2604
+
2605
+ export function createTelegramControlQueueController<TContext>(
2606
+ deps: TelegramControlQueueControllerDeps<TContext>,
2607
+ ): TelegramControlQueueController<TContext> {
2608
+ return {
2609
+ enqueue: (item, ctx, onQueued) => {
2610
+ deps.appendControlItem(item, ctx);
2611
+ onQueued?.(item);
2612
+ deps.dispatchNextQueuedTelegramTurn(ctx);
2613
+ },
2614
+ };
2615
+ }
2616
+
2617
+ // --- Control Runtime ---
2618
+
2619
+ function getTelegramQueueErrorMessage(error: unknown): string {
2620
+ return error instanceof Error ? error.message : String(error);
2621
+ }
2622
+
2623
+ export interface TelegramRuntimeEventRecorderPort {
2624
+ recordRuntimeEvent?: (
2625
+ category: string,
2626
+ error: unknown,
2627
+ details?: Record<string, unknown>,
2628
+ ) => void;
2629
+ }
2630
+
2631
+ export interface TelegramControlRuntimeDeps<
2632
+ TContext,
2633
+ > extends TelegramRuntimeEventRecorderPort {
2634
+ ctx: TContext;
2635
+ sendTextReply: (
2636
+ chatId: number,
2637
+ replyToMessageId: number,
2638
+ text: string,
2639
+ options?: { target?: TelegramQueueTarget },
2640
+ ) => Promise<number | undefined>;
2641
+ onSettled: (item: PendingTelegramControlItem<TContext>) => void;
2642
+ }
2643
+
2644
+ export async function executeTelegramControlItemRuntime<TContext>(
2645
+ item: PendingTelegramControlItem<TContext>,
2646
+ deps: TelegramControlRuntimeDeps<TContext>,
2647
+ ): Promise<void> {
2648
+ try {
2649
+ await item.execute(deps.ctx);
2650
+ } catch (error) {
2651
+ const message = getTelegramQueueErrorMessage(error);
2652
+ deps.recordRuntimeEvent?.("control", error, {
2653
+ controlType: item.controlType,
2654
+ chatId: item.chatId,
2655
+ replyToMessageId: item.replyToMessageId,
2656
+ });
2657
+ await deps.sendTextReply(
2658
+ item.chatId,
2659
+ item.replyToMessageId,
2660
+ `Telegram control action failed: ${message}`,
2661
+ { target: item.target },
2662
+ );
2663
+ } finally {
2664
+ deps.onSettled(item);
2665
+ }
2666
+ }
2667
+
2668
+ // --- Deferred Dispatch Runtime ---
2669
+
2670
+ export interface TelegramDeferredQueueDispatchRuntimeDeps extends TelegramRuntimeEventRecorderPort {
2671
+ delayMs?: number;
2672
+ setTimer?: (
2673
+ callback: () => void,
2674
+ ms: number,
2675
+ ) => ReturnType<typeof setTimeout>;
2676
+ clearTimer?: (timer: ReturnType<typeof setTimeout>) => void;
2677
+ }
2678
+
2679
+ export interface TelegramDeferredQueueDispatchRuntime<TContext = unknown> {
2680
+ bind: (ctx: TContext) => void;
2681
+ unbind: () => void;
2682
+ isBound: () => boolean;
2683
+ getGeneration: () => number;
2684
+ isGenerationActive: (generation: number) => boolean;
2685
+ request: (dispatchNextQueuedTelegramTurn: (ctx: TContext) => void) => void;
2686
+ }
2687
+
2688
+ /**
2689
+ * Production debounce for deferred queue dispatch; the factory defaults to this
2690
+ * so the entrypoint wires ports instead of policy constants.
2691
+ */
2692
+ export const TELEGRAM_DEFERRED_DISPATCH_DELAY_MS = 50;
2693
+
2694
+ export function createTelegramDeferredQueueDispatchRuntime<TContext = unknown>(
2695
+ deps: TelegramDeferredQueueDispatchRuntimeDeps = {},
2696
+ ): TelegramDeferredQueueDispatchRuntime<TContext> {
2697
+ let boundContext: TContext | undefined;
2698
+ let generation = 0;
2699
+ const timers = new Set<ReturnType<typeof setTimeout>>();
2700
+ const delayMs = deps.delayMs ?? TELEGRAM_DEFERRED_DISPATCH_DELAY_MS;
2701
+ const setTimer =
2702
+ deps.setTimer ??
2703
+ ((callback: () => void, ms: number): ReturnType<typeof setTimeout> =>
2704
+ setTimeout(callback, ms));
2705
+ const clearTimer =
2706
+ deps.clearTimer ??
2707
+ ((timer: ReturnType<typeof setTimeout>): void => clearTimeout(timer));
2708
+ const clearTimers = (): void => {
2709
+ for (const timer of timers) clearTimer(timer);
2710
+ timers.clear();
2711
+ };
2712
+ return {
2713
+ bind: (ctx) => {
2714
+ boundContext = ctx;
2715
+ generation += 1;
2716
+ },
2717
+ unbind: () => {
2718
+ boundContext = undefined;
2719
+ generation += 1;
2720
+ clearTimers();
2721
+ },
2722
+ isBound: () => boundContext !== undefined,
2723
+ getGeneration: () => generation,
2724
+ isGenerationActive: (expectedGeneration) =>
2725
+ boundContext !== undefined && generation === expectedGeneration,
2726
+ request: (dispatchNextQueuedTelegramTurn) => {
2727
+ if (boundContext === undefined) return;
2728
+ const scheduledGeneration = generation;
2729
+ let timer: ReturnType<typeof setTimeout>;
2730
+ timer = setTimer(() => {
2731
+ timers.delete(timer);
2732
+ if (generation !== scheduledGeneration || boundContext === undefined)
2733
+ return;
2734
+ try {
2735
+ dispatchNextQueuedTelegramTurn(boundContext);
2736
+ } catch (error) {
2737
+ try {
2738
+ deps.recordRuntimeEvent?.("dispatch", error, {
2739
+ phase: "deferred-queue-dispatch",
2740
+ generation: scheduledGeneration,
2741
+ });
2742
+ } catch {
2743
+ // Timer diagnostics cannot escape the deferred owner.
2744
+ }
2745
+ }
2746
+ }, delayMs);
2747
+ timer.unref?.();
2748
+ timers.add(timer);
2749
+ },
2750
+ };
2751
+ }
2752
+
2753
+ // --- Dispatch Watchdog Runtime ---
2754
+
2755
+ export interface TelegramQueueDispatchWatchdogRuntime<TContext = unknown> {
2756
+ start: (ctx: TContext) => void;
2757
+ stop: () => void;
2758
+ poke: () => void;
2759
+ }
2760
+
2761
+ export interface TelegramQueueDispatchWatchdogRuntimeDeps<
2762
+ TContext = unknown,
2763
+ > extends TelegramRuntimeEventRecorderPort {
2764
+ hasQueuedItems: () => boolean;
2765
+ dispatchNextQueuedTelegramTurn: (ctx: TContext) => void;
2766
+ intervalMs?: number;
2767
+ setInterval?: (
2768
+ callback: () => void,
2769
+ ms: number,
2770
+ ) => ReturnType<typeof setInterval>;
2771
+ clearInterval?: (timer: ReturnType<typeof setInterval>) => void;
2772
+ }
2773
+
2774
+ export function createTelegramQueueDispatchWatchdogRuntime<TContext = unknown>(
2775
+ deps: TelegramQueueDispatchWatchdogRuntimeDeps<TContext>,
2776
+ ): TelegramQueueDispatchWatchdogRuntime<TContext> {
2777
+ const intervalMs = deps.intervalMs ?? 1000;
2778
+ const setIntervalFn: NonNullable<
2779
+ TelegramQueueDispatchWatchdogRuntimeDeps<TContext>["setInterval"]
2780
+ > = deps.setInterval ?? ((callback, ms) => setInterval(callback, ms));
2781
+ const clearIntervalFn: NonNullable<
2782
+ TelegramQueueDispatchWatchdogRuntimeDeps<TContext>["clearInterval"]
2783
+ > = deps.clearInterval ?? ((timer) => clearInterval(timer));
2784
+ let ctx: TContext | undefined;
2785
+ let interval: ReturnType<typeof setInterval> | undefined;
2786
+ let dispatchInFlight = false;
2787
+ const tick = (): void => {
2788
+ if (ctx === undefined || dispatchInFlight || !deps.hasQueuedItems()) return;
2789
+ dispatchInFlight = true;
2790
+ try {
2791
+ deps.dispatchNextQueuedTelegramTurn(ctx);
2792
+ } catch (error) {
2793
+ try {
2794
+ deps.recordRuntimeEvent?.("dispatch", error, {
2795
+ phase: "queue-watchdog",
2796
+ });
2797
+ } catch {
2798
+ // Watchdog diagnostics cannot escape the interval owner.
2799
+ }
2800
+ } finally {
2801
+ dispatchInFlight = false;
2802
+ }
2803
+ };
2804
+ const stop = (): void => {
2805
+ ctx = undefined;
2806
+ if (!interval) return;
2807
+ clearIntervalFn(interval);
2808
+ interval = undefined;
2809
+ };
2810
+ return {
2811
+ start: (nextCtx) => {
2812
+ ctx = nextCtx;
2813
+ if (!interval) {
2814
+ const nextInterval = setIntervalFn(tick, intervalMs);
2815
+ interval = nextInterval;
2816
+ nextInterval.unref?.();
2817
+ }
2818
+ tick();
2819
+ },
2820
+ stop,
2821
+ poke: tick,
2822
+ };
2823
+ }
2824
+
2825
+ // --- Dispatch Runtime ---
2826
+
2827
+ export interface TelegramPromptDeliveryOptions {
2828
+ deliverAs: "followUp";
2829
+ }
2830
+
2831
+ export interface TelegramDispatchRuntimeDeps<TContext = unknown> {
2832
+ executeControlItem: (
2833
+ item: Extract<
2834
+ TelegramQueueDispatchAction<TContext>,
2835
+ { kind: "control" }
2836
+ >["item"],
2837
+ ) => void;
2838
+ onPromptDispatchStart: (chatId: number) => void;
2839
+ sendUserMessage: (
2840
+ content: Extract<
2841
+ TelegramQueueDispatchAction,
2842
+ { kind: "prompt" }
2843
+ >["item"]["content"],
2844
+ options?: TelegramPromptDeliveryOptions,
2845
+ ) => void;
2846
+ onPromptDispatchFailure: (message: string) => void;
2847
+ onIdle: () => void;
2848
+ }
2849
+
2850
+ export interface TelegramQueueDispatchControllerDeps<
2851
+ TContext = unknown,
2852
+ > extends TelegramRuntimeEventRecorderPort {
2853
+ getQueuedItems: () => TelegramQueueItem<TContext>[];
2854
+ setQueuedItems: (items: TelegramQueueItem<TContext>[]) => void;
2855
+ canDispatch: (ctx: TContext) => boolean;
2856
+ hasDispatchContext?: () => boolean;
2857
+ getDispatchGeneration?: () => number;
2858
+ isDispatchGenerationActive?: (generation: number) => boolean;
2859
+ updateStatus: (ctx: TContext, error?: string) => void;
2860
+ sendTextReply: TelegramControlRuntimeDeps<TContext>["sendTextReply"];
2861
+ onPromptDispatchStart: (ctx: TContext, chatId: number) => void;
2862
+ sendUserMessage: TelegramDispatchRuntimeDeps<TContext>["sendUserMessage"];
2863
+ onPromptDispatchFailure: (ctx: TContext, message: string) => void;
2864
+ isQueueItemTransportActive?: (item: TelegramQueueItem<TContext>) => boolean;
2865
+ hasPendingInboundQueueMutationForItem?: (
2866
+ item: TelegramQueueItem<TContext>,
2867
+ ) => boolean;
2868
+ isQueueItemAdmissionReady?: (item: TelegramQueueItem<TContext>) => boolean;
2869
+ onControlSettled?: (
2870
+ item: PendingTelegramControlItem<TContext>,
2871
+ ctx: TContext,
2872
+ ) => void;
2873
+ }
2874
+
2875
+ export interface TelegramQueueDispatchController<TContext = unknown> {
2876
+ dispatchNext: (ctx: TContext) => void;
2877
+ }
2878
+
2879
+ export function executeTelegramQueueDispatchPlan<TContext = unknown>(
2880
+ plan: TelegramQueueDispatchAction<TContext>,
2881
+ deps: TelegramDispatchRuntimeDeps<TContext>,
2882
+ ): void {
2883
+ if (plan.kind === "none") {
2884
+ deps.onIdle();
2885
+ return;
2886
+ }
2887
+ if (plan.kind === "control") {
2888
+ deps.executeControlItem(plan.item);
2889
+ return;
2890
+ }
2891
+ deps.onPromptDispatchStart(plan.item.chatId);
2892
+ try {
2893
+ deps.sendUserMessage(plan.item.content);
2894
+ } catch (error) {
2895
+ const message = getTelegramQueueErrorMessage(error);
2896
+ deps.onPromptDispatchFailure(message);
2897
+ }
2898
+ }
2899
+
2900
+ export type TelegramQueueDispatchRuntimeDeps<TContext = unknown> = Omit<
2901
+ TelegramQueueDispatchControllerDeps<TContext>,
2902
+ "canDispatch"
2903
+ > &
2904
+ TelegramDispatchReadinessDeps<TContext>;
2905
+
2906
+ export function createTelegramQueueDispatchRuntime<TContext = unknown>(
2907
+ deps: TelegramQueueDispatchRuntimeDeps<TContext>,
2908
+ ): TelegramQueueDispatchController<TContext> {
2909
+ return createTelegramQueueDispatchController({
2910
+ getQueuedItems: deps.getQueuedItems,
2911
+ setQueuedItems: deps.setQueuedItems,
2912
+ canDispatch: createTelegramDispatchReadinessChecker({
2913
+ isCompactionInProgress: deps.isCompactionInProgress,
2914
+ hasActiveTurn: deps.hasActiveTurn,
2915
+ hasDispatchPending: deps.hasDispatchPending,
2916
+ isIdle: deps.isIdle,
2917
+ hasPendingMessages: deps.hasPendingMessages,
2918
+ }),
2919
+ hasDispatchContext: deps.hasDispatchContext,
2920
+ getDispatchGeneration: deps.getDispatchGeneration,
2921
+ isDispatchGenerationActive: deps.isDispatchGenerationActive,
2922
+ updateStatus: deps.updateStatus,
2923
+ sendTextReply: deps.sendTextReply,
2924
+ onPromptDispatchStart: deps.onPromptDispatchStart,
2925
+ sendUserMessage: deps.sendUserMessage,
2926
+ onPromptDispatchFailure: deps.onPromptDispatchFailure,
2927
+ isQueueItemTransportActive: deps.isQueueItemTransportActive,
2928
+ hasPendingInboundQueueMutationForItem:
2929
+ deps.hasPendingInboundQueueMutationForItem,
2930
+ isQueueItemAdmissionReady: deps.isQueueItemAdmissionReady,
2931
+ onControlSettled: deps.onControlSettled,
2932
+ recordRuntimeEvent: deps.recordRuntimeEvent,
2933
+ });
2934
+ }
2935
+
2936
+ export function createTelegramQueueDispatchController<TContext = unknown>(
2937
+ deps: TelegramQueueDispatchControllerDeps<TContext>,
2938
+ ): TelegramQueueDispatchController<TContext> {
2939
+ let controlDispatchPending = false;
2940
+ const controller: TelegramQueueDispatchController<TContext> = {
2941
+ dispatchNext: (ctx) => {
2942
+ if (deps.hasDispatchContext && !deps.hasDispatchContext()) return;
2943
+ if (controlDispatchPending) {
2944
+ deps.updateStatus(ctx);
2945
+ return;
2946
+ }
2947
+ const queuedItems = deps.getQueuedItems();
2948
+ const activeItems: TelegramQueueItem<TContext>[] = [];
2949
+ const protectedInactiveItems: TelegramQueueItem<TContext>[] = [];
2950
+ const retainedItems: TelegramQueueItem<TContext>[] = [];
2951
+ let droppedInactiveItemCount = 0;
2952
+ for (const item of queuedItems) {
2953
+ if (
2954
+ !deps.isQueueItemTransportActive ||
2955
+ deps.isQueueItemTransportActive(item)
2956
+ ) {
2957
+ activeItems.push(item);
2958
+ retainedItems.push(item);
2959
+ } else if ((item.admissionReceipts?.length ?? 0) > 0) {
2960
+ protectedInactiveItems.push(item);
2961
+ retainedItems.push(item);
2962
+ } else {
2963
+ droppedInactiveItemCount += 1;
2964
+ }
2965
+ }
2966
+ if (droppedInactiveItemCount > 0) {
2967
+ deps.setQueuedItems(retainedItems);
2968
+ deps.recordRuntimeEvent?.(
2969
+ "dispatch",
2970
+ new Error(
2971
+ "Dropped queue work from an inactive Telegram transport generation.",
2972
+ ),
2973
+ { phase: "transport-generation" },
2974
+ );
2975
+ }
2976
+ const canDispatch = deps.canDispatch(ctx);
2977
+ let nextActiveIndex = 0;
2978
+ if (canDispatch) {
2979
+ while (nextActiveIndex < activeItems.length) {
2980
+ const candidate = activeItems[nextActiveIndex];
2981
+ if (
2982
+ !candidate ||
2983
+ candidate.kind !== "prompt" ||
2984
+ candidate.reactionSuppressionEmoji === undefined
2985
+ ) {
2986
+ break;
2987
+ }
2988
+ if (deps.hasPendingInboundQueueMutationForItem?.(candidate)) {
2989
+ deps.updateStatus(ctx);
2990
+ return;
2991
+ }
2992
+ if (
2993
+ deps.isQueueItemAdmissionReady &&
2994
+ !deps.isQueueItemAdmissionReady(candidate)
2995
+ ) {
2996
+ deps.updateStatus(ctx);
2997
+ return;
2998
+ }
2999
+ nextActiveIndex += 1;
3000
+ }
3001
+ }
3002
+ const dispatchableItems = activeItems.slice(nextActiveIndex);
3003
+ const nextItem = dispatchableItems[0];
3004
+ if (
3005
+ nextItem &&
3006
+ deps.hasPendingInboundQueueMutationForItem?.(nextItem)
3007
+ ) {
3008
+ deps.updateStatus(ctx);
3009
+ return;
3010
+ }
3011
+ if (
3012
+ nextItem &&
3013
+ deps.isQueueItemAdmissionReady &&
3014
+ !deps.isQueueItemAdmissionReady(nextItem)
3015
+ ) {
3016
+ deps.updateStatus(ctx);
3017
+ return;
3018
+ }
3019
+ const dispatchPlan = planNextTelegramQueueAction(
3020
+ dispatchableItems,
3021
+ canDispatch,
3022
+ );
3023
+ if (nextActiveIndex > 0 || dispatchPlan.kind !== "none") {
3024
+ deps.setQueuedItems([
3025
+ ...dispatchPlan.remainingItems,
3026
+ ...protectedInactiveItems,
3027
+ ]);
3028
+ }
3029
+ executeTelegramQueueDispatchPlan(dispatchPlan, {
3030
+ executeControlItem: (item) => {
3031
+ controlDispatchPending = true;
3032
+ const dispatchGeneration = deps.getDispatchGeneration?.();
3033
+ deps.updateStatus(ctx);
3034
+ void executeTelegramControlItemRuntime(item, {
3035
+ ctx,
3036
+ sendTextReply: deps.sendTextReply,
3037
+ recordRuntimeEvent: deps.recordRuntimeEvent,
3038
+ onSettled: (settledItem) => {
3039
+ try {
3040
+ deps.onControlSettled?.(settledItem, ctx);
3041
+ } catch (error) {
3042
+ deps.recordRuntimeEvent?.("control", error, {
3043
+ phase: "receipt-settlement",
3044
+ controlType: settledItem.controlType,
3045
+ });
3046
+ }
3047
+ controlDispatchPending = false;
3048
+ if (deps.hasDispatchContext && !deps.hasDispatchContext()) return;
3049
+ if (
3050
+ dispatchGeneration !== undefined &&
3051
+ deps.isDispatchGenerationActive &&
3052
+ !deps.isDispatchGenerationActive(dispatchGeneration)
3053
+ ) {
3054
+ return;
3055
+ }
3056
+ deps.updateStatus(ctx);
3057
+ controller.dispatchNext(ctx);
3058
+ },
3059
+ });
3060
+ },
3061
+ onPromptDispatchStart: (chatId) => {
3062
+ deps.onPromptDispatchStart(ctx, chatId);
3063
+ },
3064
+ sendUserMessage: deps.sendUserMessage,
3065
+ onPromptDispatchFailure: (message) => {
3066
+ deps.onPromptDispatchFailure(ctx, message);
3067
+ },
3068
+ onIdle: () => {
3069
+ deps.updateStatus(ctx);
3070
+ },
3071
+ });
3072
+ },
3073
+ };
3074
+ return controller;
3075
+ }