@ouro.bot/cli 0.1.0-alpha.8 → 0.1.0-alpha.800

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 (624) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +249 -176
  3. package/RepairGuide.ouro/agent.json +5 -0
  4. package/RepairGuide.ouro/psyche/IDENTITY.md +19 -0
  5. package/RepairGuide.ouro/psyche/SOUL.md +55 -0
  6. package/RepairGuide.ouro/skills/diagnose-broken-remote.md +63 -0
  7. package/RepairGuide.ouro/skills/diagnose-stacked-typed-issues.md +35 -0
  8. package/RepairGuide.ouro/skills/diagnose-sync-blocked.md +54 -0
  9. package/RepairGuide.ouro/skills/diagnose-vault-expired.md +60 -0
  10. package/SerpentGuide.ouro/agent.json +83 -0
  11. package/SerpentGuide.ouro/psyche/SOUL.md +25 -0
  12. package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/monty.md +2 -2
  13. package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/the-serpent.md +1 -1
  14. package/assets/bluebubbles-host +264 -0
  15. package/assets/ouroboros.png +0 -0
  16. package/changelog.json +5563 -0
  17. package/deploy/unraid/Dockerfile +48 -0
  18. package/deploy/unraid/README.txt +3012 -0
  19. package/deploy/unraid/audit-container-spec.sh +4 -0
  20. package/deploy/unraid/container-runtime.json +1 -0
  21. package/deploy/unraid/docker-man-template-transaction.mjs +526 -0
  22. package/deploy/unraid/docker-man-template-xml.cjs +105 -0
  23. package/deploy/unraid/migrate-sanctuary-bundle.mjs +60 -0
  24. package/deploy/unraid/ouro-events/bootstrap-spool.sh +92 -0
  25. package/deploy/unraid/ouro-events/emit-event.mjs +310 -0
  26. package/deploy/unraid/ouro-events/emit-usenet-event.sh +54 -0
  27. package/deploy/unraid/ouro-events/install-usenet-guard.sh +435 -0
  28. package/deploy/unraid/ouro-events/usenet-health.sh +176 -0
  29. package/deploy/unraid/sanctuary-acceptance-adapter.sh +43 -0
  30. package/deploy/unraid/sanctuary-acceptance-contract.json +198 -0
  31. package/deploy/unraid/sanctuary-acceptance-harness.sh +23 -0
  32. package/deploy/unraid/sanctuary-deployment-target.mjs +709 -0
  33. package/deploy/unraid/sanctuary-safe-rename.py +24 -0
  34. package/deploy/unraid/sanctuary-unit16-host-broker.mjs +2030 -0
  35. package/deploy/unraid/sanctuary-unit16-run.sh +560 -0
  36. package/deploy/unraid/sanctuary-unit18-target-audit.sh +27 -0
  37. package/deploy/unraid/sanctuary.ouro/agent.json +24 -0
  38. package/deploy/unraid/sanctuary.ouro/arc/README.md +3 -0
  39. package/deploy/unraid/sanctuary.ouro/bundle-meta.json +5 -0
  40. package/deploy/unraid/sanctuary.ouro/habits/sanctuary-health.md +8 -0
  41. package/deploy/unraid/sanctuary.ouro/provider-readiness.json +11 -0
  42. package/deploy/unraid/sanctuary.ouro/psyche/ASPIRATIONS.md +5 -0
  43. package/deploy/unraid/sanctuary.ouro/psyche/IDENTITY.md +5 -0
  44. package/deploy/unraid/sanctuary.ouro/psyche/LORE.md +11 -0
  45. package/deploy/unraid/sanctuary.ouro/psyche/SOUL.md +25 -0
  46. package/deploy/unraid/sanctuary.ouro/psyche/TACIT.md +11 -0
  47. package/deploy/unraid/sanctuary.ouro/state/policy/steward.json +7 -0
  48. package/deploy/unraid/sanctuary.ouro/tool-profiles.json +23 -0
  49. package/deploy/unraid/sanctuary.xml +27 -0
  50. package/dist/a2a/card.js +57 -0
  51. package/dist/a2a/client.js +180 -0
  52. package/dist/a2a/config.js +50 -0
  53. package/dist/a2a/delegation-stores.js +56 -0
  54. package/dist/a2a/did-resolution.js +93 -0
  55. package/dist/a2a/identity.js +170 -0
  56. package/dist/a2a/inbound-share.js +107 -0
  57. package/dist/a2a/mission-result-wire.js +196 -0
  58. package/dist/a2a/onboarding.js +115 -0
  59. package/dist/a2a/pin-store.js +132 -0
  60. package/dist/a2a/seen-ledger.js +120 -0
  61. package/dist/a2a/server.js +604 -0
  62. package/dist/a2a/task-store.js +69 -0
  63. package/dist/a2a/transport.js +45 -0
  64. package/dist/a2a/types.js +3 -0
  65. package/dist/arc/attention-types.js +8 -0
  66. package/dist/arc/cares.js +360 -0
  67. package/dist/arc/episodes.js +118 -0
  68. package/dist/arc/evolution.js +490 -0
  69. package/dist/arc/flight-recorder.js +1034 -0
  70. package/dist/arc/intentions.js +134 -0
  71. package/dist/arc/json-store.js +117 -0
  72. package/dist/arc/obligations.js +386 -0
  73. package/dist/arc/packets.js +336 -0
  74. package/dist/arc/presence.js +185 -0
  75. package/dist/arc/task-lifecycle.js +57 -0
  76. package/dist/commerce/store.js +755 -0
  77. package/dist/commerce/types.js +3 -0
  78. package/dist/heart/active-work.js +1009 -0
  79. package/dist/heart/agent-entry.js +252 -3
  80. package/dist/heart/approval-files.js +113 -0
  81. package/dist/heart/approval-store.js +950 -0
  82. package/dist/heart/attachments/image-normalize.js +194 -0
  83. package/dist/heart/attachments/materialize.js +97 -0
  84. package/dist/heart/attachments/originals.js +88 -0
  85. package/dist/heart/attachments/render.js +29 -0
  86. package/dist/heart/attachments/sources/bluebubbles.js +156 -0
  87. package/dist/heart/attachments/sources/cli-local-file.js +78 -0
  88. package/dist/heart/attachments/sources/index.js +18 -0
  89. package/dist/heart/attachments/sources/telegram.js +71 -0
  90. package/dist/heart/attachments/store.js +132 -0
  91. package/dist/heart/attachments/types.js +93 -0
  92. package/dist/heart/auth/auth-flow.js +495 -0
  93. package/dist/heart/autonomy-budget.js +478 -0
  94. package/dist/heart/awaiting/await-alert.js +148 -0
  95. package/dist/heart/awaiting/await-expiry.js +143 -0
  96. package/dist/heart/awaiting/await-loader.js +91 -0
  97. package/dist/heart/awaiting/await-parser.js +149 -0
  98. package/dist/heart/awaiting/await-runtime-state.js +100 -0
  99. package/dist/heart/awaiting/await-scheduler.js +382 -0
  100. package/dist/heart/background-operations.js +281 -0
  101. package/dist/heart/bridges/manager.js +478 -0
  102. package/dist/heart/bridges/state-machine.js +135 -0
  103. package/dist/heart/bridges/store.js +135 -0
  104. package/dist/heart/bundle-state.js +168 -0
  105. package/dist/heart/commitments.js +135 -0
  106. package/dist/heart/config-registry.js +331 -0
  107. package/dist/heart/config.js +195 -137
  108. package/dist/heart/context-loss-gauntlet.js +387 -0
  109. package/dist/heart/context-loss-sentinel.js +1157 -0
  110. package/dist/heart/core.js +2257 -338
  111. package/dist/heart/cross-chat-delivery.js +133 -0
  112. package/dist/heart/daemon/agent-config-check.js +451 -0
  113. package/dist/heart/daemon/agent-discovery.js +263 -0
  114. package/dist/heart/daemon/agent-service.js +542 -0
  115. package/dist/heart/daemon/agentic-repair.js +547 -0
  116. package/dist/heart/daemon/await-private-wake.js +59 -0
  117. package/dist/heart/daemon/bluebubbles-cli-integration.js +71 -0
  118. package/dist/heart/daemon/bluebubbles-health-diagnostics.js +122 -0
  119. package/dist/heart/daemon/bluebubbles-host-protocol.js +433 -0
  120. package/dist/heart/daemon/bluebubbles-host.js +273 -0
  121. package/dist/heart/daemon/boot-sync-probe.js +197 -0
  122. package/dist/heart/daemon/cadence.js +180 -0
  123. package/dist/heart/daemon/cli-defaults.js +836 -0
  124. package/dist/heart/daemon/cli-desk.js +322 -0
  125. package/dist/heart/daemon/cli-exec.js +8730 -0
  126. package/dist/heart/daemon/cli-help.js +710 -0
  127. package/dist/heart/daemon/cli-parse.js +2719 -0
  128. package/dist/heart/daemon/cli-render-doctor.js +57 -0
  129. package/dist/heart/daemon/cli-render.js +777 -0
  130. package/dist/heart/daemon/cli-types.js +8 -0
  131. package/dist/heart/daemon/connect-bay.js +323 -0
  132. package/dist/heart/daemon/container-credential-bootstrap.js +273 -0
  133. package/dist/heart/daemon/container-healthcheck.js +112 -0
  134. package/dist/heart/daemon/container-runtime.js +97 -0
  135. package/dist/heart/daemon/container-spec-auditor-main.js +170 -0
  136. package/dist/heart/daemon/container-spec-auditor.js +236 -0
  137. package/dist/heart/daemon/daemon-bootstrap-startup.js +107 -0
  138. package/dist/heart/daemon/daemon-cli.js +30 -786
  139. package/dist/heart/daemon/daemon-entry.js +971 -13
  140. package/dist/heart/daemon/daemon-health.js +186 -0
  141. package/dist/heart/daemon/daemon-rollup.js +57 -0
  142. package/dist/heart/daemon/daemon-runtime-sync.js +287 -0
  143. package/dist/heart/daemon/daemon-tombstone.js +236 -0
  144. package/dist/heart/daemon/daemon.js +2156 -28
  145. package/dist/heart/daemon/dns-workflow.js +394 -0
  146. package/dist/heart/daemon/doctor-types.js +8 -0
  147. package/dist/heart/daemon/doctor.js +1755 -0
  148. package/dist/heart/daemon/freshness.js +345 -0
  149. package/dist/heart/daemon/habit-private-wake.js +61 -0
  150. package/dist/heart/daemon/health-monitor.js +142 -11
  151. package/dist/heart/daemon/hooks/agent-config-v2.js +33 -0
  152. package/dist/heart/daemon/hooks/bundle-meta.js +226 -0
  153. package/dist/heart/daemon/http-health-probe.js +80 -0
  154. package/dist/heart/daemon/human-command-screens.js +234 -0
  155. package/dist/heart/daemon/human-readiness.js +114 -0
  156. package/dist/heart/daemon/inner-status.js +78 -0
  157. package/dist/heart/daemon/interactive-repair.js +394 -0
  158. package/dist/heart/daemon/launchd.js +188 -0
  159. package/dist/heart/daemon/log-tailer.js +82 -12
  160. package/dist/heart/daemon/logs-prune.js +132 -0
  161. package/dist/heart/daemon/mcp-canary.js +524 -0
  162. package/dist/heart/daemon/message-router.js +80 -11
  163. package/dist/heart/daemon/migrate-to-desk.js +848 -0
  164. package/dist/heart/daemon/os-cron-deps.js +135 -0
  165. package/dist/heart/daemon/os-cron.js +22 -14
  166. package/dist/heart/daemon/ouro-bot-entry.js +4 -2
  167. package/dist/heart/daemon/ouro-entry.js +3 -1
  168. package/dist/heart/daemon/plugin-cli.js +432 -0
  169. package/dist/heart/daemon/process-manager.js +739 -54
  170. package/dist/heart/daemon/provider-discovery.js +137 -0
  171. package/dist/heart/daemon/provider-ping-progress.js +83 -0
  172. package/dist/heart/daemon/prunable-bundle.js +144 -0
  173. package/dist/heart/daemon/pulse.js +511 -0
  174. package/dist/heart/daemon/readiness-repair.js +365 -0
  175. package/dist/heart/daemon/run-hooks.js +39 -0
  176. package/dist/heart/daemon/runtime-logging.js +92 -16
  177. package/dist/heart/daemon/runtime-metadata.js +191 -0
  178. package/dist/{repertoire/tasks/lifecycle.js → heart/daemon/runtime-mode.js} +37 -39
  179. package/dist/heart/daemon/safe-mode.js +161 -0
  180. package/dist/heart/daemon/sanctuary-acceptance-adapter.js +2609 -0
  181. package/dist/heart/daemon/sanctuary-acceptance-harness.js +1866 -0
  182. package/dist/heart/daemon/sanctuary-acceptance-marker.js +267 -0
  183. package/dist/heart/daemon/sanctuary-acceptance-scenarios.js +976 -0
  184. package/dist/heart/daemon/sanctuary-bundle-migration.js +728 -0
  185. package/dist/heart/daemon/sanctuary-package-management.js +103 -0
  186. package/dist/heart/daemon/sanctuary-scheduler-liveness.js +227 -0
  187. package/dist/heart/daemon/sanctuary-scheduler-origin.js +132 -0
  188. package/dist/heart/daemon/sense-manager.js +948 -0
  189. package/dist/heart/daemon/session-id-resolver.js +131 -0
  190. package/dist/heart/daemon/skill-management-installer.js +94 -0
  191. package/dist/heart/daemon/socket-client.js +370 -0
  192. package/dist/heart/daemon/stale-bundle-prune.js +113 -0
  193. package/dist/heart/daemon/startup-tui.js +339 -0
  194. package/dist/heart/daemon/supercronic-supervisor.js +282 -0
  195. package/dist/heart/daemon/task-scheduler.js +117 -39
  196. package/dist/heart/daemon/terminal-ui.js +499 -0
  197. package/dist/heart/daemon/thoughts.js +591 -0
  198. package/dist/heart/daemon/up-progress.js +366 -0
  199. package/dist/heart/daemon/vault-items.js +56 -0
  200. package/dist/heart/delegation.js +59 -0
  201. package/dist/heart/external-events/router.js +1414 -0
  202. package/dist/heart/habits/habit-cancel.js +810 -0
  203. package/dist/heart/habits/habit-lifecycle.js +1327 -0
  204. package/dist/heart/habits/habit-migration.js +194 -0
  205. package/dist/heart/habits/habit-parser.js +278 -0
  206. package/dist/heart/habits/habit-runtime-state.js +93 -0
  207. package/dist/heart/habits/habit-scheduler.js +653 -0
  208. package/dist/heart/habits/habit-session-summary.js +318 -0
  209. package/dist/heart/habits/habit-session.js +636 -0
  210. package/dist/heart/{daemon → hatch}/hatch-animation.js +10 -3
  211. package/dist/heart/{daemon → hatch}/hatch-flow.js +57 -139
  212. package/dist/heart/{daemon → hatch}/hatch-specialist.js +6 -8
  213. package/dist/heart/hatch/specialist-orchestrator.js +129 -0
  214. package/dist/heart/hatch/specialist-prompt.js +102 -0
  215. package/dist/heart/hatch/specialist-tools.js +318 -0
  216. package/dist/heart/identity.js +297 -67
  217. package/dist/heart/kept-notes.js +289 -0
  218. package/dist/heart/kicks.js +2 -20
  219. package/dist/heart/machine-identity.js +161 -0
  220. package/dist/heart/mail-import-discovery.js +353 -0
  221. package/dist/heart/mailbox/mailbox-http-hooks.js +98 -0
  222. package/dist/heart/mailbox/mailbox-http-response.js +7 -0
  223. package/dist/heart/mailbox/mailbox-http-routes.js +398 -0
  224. package/dist/heart/mailbox/mailbox-http-static.js +103 -0
  225. package/dist/heart/mailbox/mailbox-http-transport.js +129 -0
  226. package/dist/heart/mailbox/mailbox-http.js +99 -0
  227. package/dist/heart/mailbox/mailbox-read.js +39 -0
  228. package/dist/heart/mailbox/mailbox-types.js +27 -0
  229. package/dist/heart/mailbox/mailbox-view.js +197 -0
  230. package/dist/heart/mailbox/readers/agent-machine.js +428 -0
  231. package/dist/heart/mailbox/readers/continuity-readers.js +329 -0
  232. package/dist/heart/mailbox/readers/mail.js +375 -0
  233. package/dist/heart/mailbox/readers/runtime-readers.js +823 -0
  234. package/dist/heart/mailbox/readers/sessions.js +232 -0
  235. package/dist/heart/mailbox/readers/shared.js +111 -0
  236. package/dist/heart/mcp/mcp-server.js +696 -0
  237. package/dist/heart/migrate-config.js +100 -0
  238. package/dist/heart/model-capabilities.js +59 -0
  239. package/dist/heart/orientation-frame.js +345 -0
  240. package/dist/heart/platform.js +81 -0
  241. package/dist/heart/primitives.js +1 -1
  242. package/dist/heart/private-runtime/decision-renderer.js +158 -0
  243. package/dist/heart/private-runtime/index.js +18 -0
  244. package/dist/heart/private-runtime/ledger.js +221 -0
  245. package/dist/heart/private-runtime/policy.js +252 -0
  246. package/dist/heart/private-runtime/types.js +2 -0
  247. package/dist/heart/progress-story.js +42 -0
  248. package/dist/heart/provider-attempt.js +137 -0
  249. package/dist/heart/provider-binding-resolver.js +273 -0
  250. package/dist/heart/provider-credentials.js +449 -0
  251. package/dist/heart/provider-failover.js +311 -0
  252. package/dist/heart/provider-models.js +96 -0
  253. package/dist/heart/provider-ping.js +265 -0
  254. package/dist/heart/provider-readiness-cache.js +119 -0
  255. package/dist/heart/provider-visibility.js +188 -0
  256. package/dist/heart/providers/anthropic-token.js +131 -0
  257. package/dist/heart/providers/anthropic.js +236 -58
  258. package/dist/heart/providers/azure.js +104 -13
  259. package/dist/heart/providers/error-classification.js +127 -0
  260. package/dist/heart/providers/github-copilot.js +145 -0
  261. package/dist/heart/providers/minimax-vlm.js +189 -0
  262. package/dist/heart/providers/minimax.js +29 -7
  263. package/dist/heart/providers/openai-codex-token.js +349 -0
  264. package/dist/heart/providers/openai-codex.js +63 -39
  265. package/dist/heart/providers/openai-compatible.js +177 -0
  266. package/dist/heart/run-ledger.js +247 -0
  267. package/dist/heart/runtime-capability-check.js +170 -0
  268. package/dist/heart/runtime-credentials.js +607 -0
  269. package/dist/heart/runtime-cwd.js +87 -0
  270. package/dist/heart/sense-truth.js +79 -0
  271. package/dist/heart/session-activity.js +235 -0
  272. package/dist/heart/session-events.js +1318 -0
  273. package/dist/heart/session-playback-cli-main.js +5 -0
  274. package/dist/heart/session-playback-cli.js +36 -0
  275. package/dist/heart/session-playback.js +231 -0
  276. package/dist/heart/session-stats-cli-main.js +5 -0
  277. package/dist/heart/session-stats.js +182 -0
  278. package/dist/heart/session-transcript.js +133 -0
  279. package/dist/heart/start-of-turn-packet.js +372 -0
  280. package/dist/heart/steward-policy.js +302 -0
  281. package/dist/heart/streaming.js +682 -207
  282. package/dist/heart/structured-output.js +196 -0
  283. package/dist/heart/sync-classification.js +176 -0
  284. package/dist/heart/sync.js +449 -0
  285. package/dist/heart/target-resolution.js +127 -0
  286. package/dist/heart/tempo.js +93 -0
  287. package/dist/heart/temporal-view.js +41 -0
  288. package/dist/heart/timeouts.js +101 -0
  289. package/dist/heart/tool-activity-callbacks.js +59 -0
  290. package/dist/heart/tool-approval.js +342 -0
  291. package/dist/heart/tool-description.js +155 -0
  292. package/dist/heart/tool-friction.js +55 -0
  293. package/dist/heart/tool-loop.js +200 -0
  294. package/dist/heart/turn-context.js +481 -0
  295. package/dist/heart/turn-coordinator.js +28 -0
  296. package/dist/heart/versioning/ouro-bot-global-installer.js +129 -0
  297. package/dist/heart/{daemon → versioning}/ouro-bot-wrapper.js +1 -1
  298. package/dist/heart/versioning/ouro-path-installer.js +432 -0
  299. package/dist/heart/versioning/ouro-recovery-launcher.js +76 -0
  300. package/dist/heart/{daemon → versioning}/ouro-uti.js +11 -2
  301. package/dist/heart/versioning/ouro-version-manager.js +409 -0
  302. package/dist/heart/versioning/staged-restart.js +146 -0
  303. package/dist/heart/versioning/update-checker.js +116 -0
  304. package/dist/heart/versioning/update-hooks.js +154 -0
  305. package/dist/heart/versioning/version-intent.js +114 -0
  306. package/dist/heart/versioning/wrapper-publish-guard.js +86 -0
  307. package/dist/heart/work-card.js +380 -0
  308. package/dist/mailbox-ui/assets/index-Du_9G9WO.css +1 -0
  309. package/dist/mailbox-ui/assets/index-J8zDwHmE.js +1 -0
  310. package/dist/mailbox-ui/assets/vendor-CcN1XpQ9.js +61 -0
  311. package/dist/mailbox-ui/index.html +16 -0
  312. package/dist/mailroom/attention.js +167 -0
  313. package/dist/mailroom/autonomy.js +209 -0
  314. package/dist/mailroom/blob-store.js +889 -0
  315. package/dist/mailroom/body-cache.js +61 -0
  316. package/dist/mailroom/cache-sync-cli.js +58 -0
  317. package/dist/mailroom/core.js +803 -0
  318. package/dist/mailroom/entry.js +160 -0
  319. package/dist/mailroom/file-store.js +568 -0
  320. package/dist/mailroom/hosted-cache-sync.js +384 -0
  321. package/dist/mailroom/mbox-import.js +393 -0
  322. package/dist/mailroom/migration.js +164 -0
  323. package/dist/mailroom/outbound.js +380 -0
  324. package/dist/mailroom/policy.js +263 -0
  325. package/dist/mailroom/reader.js +292 -0
  326. package/dist/mailroom/search-cache.js +513 -0
  327. package/dist/mailroom/search-relevance.js +319 -0
  328. package/dist/mailroom/smtp-ingress.js +176 -0
  329. package/dist/mailroom/source-state.js +176 -0
  330. package/dist/mailroom/thread.js +109 -0
  331. package/dist/mailroom/travel-extract.js +89 -0
  332. package/dist/mind/bundle-manifest.js +93 -1
  333. package/dist/mind/context.js +287 -104
  334. package/dist/mind/desk-section.js +362 -0
  335. package/dist/mind/diary-integrity.js +60 -0
  336. package/dist/mind/{memory.js → diary.js} +90 -98
  337. package/dist/mind/embedding-provider.js +60 -0
  338. package/dist/mind/file-state.js +179 -0
  339. package/dist/mind/first-impressions.js +16 -2
  340. package/dist/mind/{associative-recall.js → note-search.js} +61 -60
  341. package/dist/mind/obligation-steering.js +227 -0
  342. package/dist/mind/pending.js +102 -9
  343. package/dist/mind/phrases.js +1 -0
  344. package/dist/mind/prompt-budget.js +479 -0
  345. package/dist/mind/prompt-refresh.js +3 -2
  346. package/dist/mind/prompt.js +1377 -140
  347. package/dist/mind/provenance-trust.js +26 -0
  348. package/dist/mind/record-paths.js +312 -0
  349. package/dist/mind/scrutiny.js +173 -0
  350. package/dist/mind/session-transaction.js +415 -0
  351. package/dist/mind/token-estimate.js +8 -12
  352. package/dist/nerves/cli-logging.js +22 -3
  353. package/dist/nerves/coverage/audit-rules.js +15 -6
  354. package/dist/nerves/coverage/audit.js +28 -2
  355. package/dist/nerves/coverage/cli.js +1 -1
  356. package/dist/nerves/coverage/contract.js +5 -5
  357. package/dist/nerves/coverage/file-completeness.js +139 -5
  358. package/dist/nerves/coverage/run-artifacts.js +56 -36
  359. package/dist/nerves/coverage/source-scanner.js +4 -4
  360. package/dist/nerves/event-buffer.js +111 -0
  361. package/dist/nerves/index.js +305 -9
  362. package/dist/nerves/observation.js +20 -0
  363. package/dist/nerves/redact.js +79 -0
  364. package/dist/nerves/review/cli-main.js +5 -0
  365. package/dist/nerves/review/cli.js +156 -0
  366. package/dist/nerves/review/core.js +159 -0
  367. package/dist/nerves/runtime.js +17 -1
  368. package/dist/repertoire/ado-client.js +17 -56
  369. package/dist/repertoire/ado-semantic.js +16 -10
  370. package/dist/repertoire/api-client.js +97 -0
  371. package/dist/repertoire/bitwarden-store.js +1045 -0
  372. package/dist/repertoire/bundle-templates.js +71 -0
  373. package/dist/repertoire/bw-installer.js +180 -0
  374. package/dist/repertoire/coding/codex-jsonl.js +64 -0
  375. package/dist/repertoire/coding/context-pack.js +331 -0
  376. package/dist/repertoire/coding/feedback.js +322 -0
  377. package/dist/repertoire/coding/index.js +18 -3
  378. package/dist/repertoire/coding/manager.js +228 -14
  379. package/dist/repertoire/coding/spawner.js +61 -14
  380. package/dist/repertoire/coding/tools.js +279 -12
  381. package/dist/repertoire/coding/workbench-client.js +272 -0
  382. package/dist/repertoire/coding/workbench-manager.js +346 -0
  383. package/dist/repertoire/commerce-errors.js +109 -0
  384. package/dist/repertoire/commerce-self-test.js +156 -0
  385. package/dist/repertoire/credential-access.js +178 -0
  386. package/dist/repertoire/data/ado-endpoints.json +188 -0
  387. package/dist/repertoire/desk/classifier.js +362 -0
  388. package/dist/repertoire/duffel-client.js +185 -0
  389. package/dist/repertoire/github-client.js +14 -55
  390. package/dist/repertoire/graph-client.js +11 -52
  391. package/dist/repertoire/guardrails.js +1083 -0
  392. package/dist/repertoire/mcp-client.js +295 -0
  393. package/dist/repertoire/mcp-manager.js +434 -0
  394. package/dist/repertoire/mcp-tools.js +83 -0
  395. package/dist/repertoire/plugin-mcp.js +175 -0
  396. package/dist/repertoire/plugins.js +253 -0
  397. package/dist/repertoire/relationship-authorization.js +194 -0
  398. package/dist/repertoire/shell-sessions.js +133 -0
  399. package/dist/repertoire/skills.js +45 -24
  400. package/dist/repertoire/stripe-client.js +131 -0
  401. package/dist/repertoire/tool-arguments.js +148 -0
  402. package/dist/repertoire/tool-results.js +29 -0
  403. package/dist/repertoire/tools-a2a.js +699 -0
  404. package/dist/repertoire/tools-attachments.js +322 -0
  405. package/dist/repertoire/tools-awaiting.js +575 -0
  406. package/dist/repertoire/tools-base.js +85 -707
  407. package/dist/repertoire/tools-bluebubbles.js +95 -0
  408. package/dist/repertoire/tools-bridge.js +144 -0
  409. package/dist/repertoire/tools-bundle.js +993 -0
  410. package/dist/repertoire/tools-commerce.js +253 -0
  411. package/dist/repertoire/tools-config.js +186 -0
  412. package/dist/repertoire/tools-continuity.js +575 -0
  413. package/dist/repertoire/tools-credential.js +383 -0
  414. package/dist/repertoire/tools-evolution.js +527 -0
  415. package/dist/repertoire/tools-files.js +344 -0
  416. package/dist/repertoire/tools-flight.js +290 -0
  417. package/dist/repertoire/tools-flow.js +119 -0
  418. package/dist/repertoire/tools-github.js +3 -8
  419. package/dist/repertoire/tools-habits.js +103 -0
  420. package/dist/repertoire/tools-mail.js +1828 -0
  421. package/dist/repertoire/tools-notes.js +485 -0
  422. package/dist/repertoire/tools-obligations.js +182 -0
  423. package/dist/repertoire/tools-orientation.js +31 -0
  424. package/dist/repertoire/tools-record.js +482 -0
  425. package/dist/repertoire/tools-rsvp.js +139 -0
  426. package/dist/repertoire/tools-runtime.js +150 -0
  427. package/dist/repertoire/tools-session.js +1108 -0
  428. package/dist/repertoire/tools-shell.js +133 -0
  429. package/dist/repertoire/tools-steward-policy.js +95 -0
  430. package/dist/repertoire/tools-stripe.js +224 -0
  431. package/dist/repertoire/tools-surface.js +369 -0
  432. package/dist/repertoire/tools-teams.js +67 -61
  433. package/dist/repertoire/tools-telegram-contacts.js +37 -0
  434. package/dist/repertoire/tools-travel.js +125 -0
  435. package/dist/repertoire/tools-trip.js +982 -0
  436. package/dist/repertoire/tools-unraid.js +484 -0
  437. package/dist/repertoire/tools-user-profile.js +146 -0
  438. package/dist/repertoire/tools-vault.js +40 -0
  439. package/dist/repertoire/tools-voice.js +145 -0
  440. package/dist/repertoire/tools.js +441 -102
  441. package/dist/repertoire/travel-api-client.js +360 -0
  442. package/dist/repertoire/unraid-client.js +155 -0
  443. package/dist/repertoire/unraid-restart.js +212 -0
  444. package/dist/repertoire/user-profile.js +131 -0
  445. package/dist/repertoire/vault-setup.js +246 -0
  446. package/dist/repertoire/vault-unlock.js +628 -0
  447. package/dist/rsvp/aisleplanner-client.js +354 -0
  448. package/dist/rsvp/cli.js +926 -0
  449. package/dist/rsvp/config.js +327 -0
  450. package/dist/rsvp/cutover.js +433 -0
  451. package/dist/rsvp/diagnostics.js +232 -0
  452. package/dist/rsvp/diff-renderer.js +176 -0
  453. package/dist/rsvp/habit-policy.js +110 -0
  454. package/dist/rsvp/habit-stage.js +123 -0
  455. package/dist/rsvp/incident-bundle.js +97 -0
  456. package/dist/rsvp/migration.js +204 -0
  457. package/dist/rsvp/native-habit-runner.js +630 -0
  458. package/dist/rsvp/outbound-state.js +628 -0
  459. package/dist/rsvp/query.js +118 -0
  460. package/dist/rsvp/replay.js +233 -0
  461. package/dist/rsvp/snapshot.js +232 -0
  462. package/dist/rsvp/spend-ledger.js +175 -0
  463. package/dist/scripts/claude-code-hook.js +41 -0
  464. package/dist/scripts/claude-code-stop-hook.js +47 -0
  465. package/dist/senses/a2a-entry.js +115 -0
  466. package/dist/senses/attention-queue.js +186 -0
  467. package/dist/senses/await-turn-message.js +58 -0
  468. package/dist/senses/bluebubbles/active-turns.js +216 -0
  469. package/dist/senses/bluebubbles/attachment-cache.js +53 -0
  470. package/dist/senses/bluebubbles/attachment-download.js +137 -0
  471. package/dist/senses/bluebubbles/client.js +799 -0
  472. package/dist/senses/bluebubbles/context-packet.js +207 -0
  473. package/dist/senses/bluebubbles/context-smoke.js +144 -0
  474. package/dist/senses/bluebubbles/entry.js +90 -0
  475. package/dist/senses/bluebubbles/inbound-log.js +127 -0
  476. package/dist/senses/bluebubbles/index.js +3839 -0
  477. package/dist/senses/bluebubbles/latest-turn.js +311 -0
  478. package/dist/senses/bluebubbles/media.js +389 -0
  479. package/dist/senses/bluebubbles/model.js +392 -0
  480. package/dist/senses/{bluebubbles-mutation-log.js → bluebubbles/mutation-log.js} +57 -6
  481. package/dist/senses/bluebubbles/outbound-state.js +308 -0
  482. package/dist/senses/bluebubbles/processed-log.js +133 -0
  483. package/dist/senses/bluebubbles/reaction-policy.js +47 -0
  484. package/dist/senses/bluebubbles/replay.js +284 -0
  485. package/dist/senses/bluebubbles/runtime-state.js +137 -0
  486. package/dist/senses/bluebubbles/semantic-receipts.js +1986 -0
  487. package/dist/senses/bluebubbles/session-cleanup.js +72 -0
  488. package/dist/senses/bluebubbles/webhook-registration.js +228 -0
  489. package/dist/senses/bluebubbles-meta-guard.js +39 -0
  490. package/dist/senses/cli/bracketed-paste.js +82 -0
  491. package/dist/senses/cli/image-paste.js +287 -0
  492. package/dist/senses/cli/image-ref-navigation.js +75 -0
  493. package/dist/senses/cli/ink-app.js +156 -0
  494. package/dist/senses/cli/inline-diff.js +64 -0
  495. package/dist/senses/cli/input-keys.js +174 -0
  496. package/dist/senses/cli/kill-ring.js +86 -0
  497. package/dist/senses/cli/message-list.js +51 -0
  498. package/dist/senses/cli/ouro-tui.js +607 -0
  499. package/dist/senses/cli/spinner-imperative.js +135 -0
  500. package/dist/senses/cli/spinner.js +101 -0
  501. package/dist/senses/cli/status-line.js +60 -0
  502. package/dist/senses/cli/streaming-markdown.js +526 -0
  503. package/dist/senses/cli/tool-display.js +85 -0
  504. package/dist/senses/cli/tool-render.js +85 -0
  505. package/dist/senses/cli/tui-store.js +241 -0
  506. package/dist/senses/cli/virtual-list.js +35 -0
  507. package/dist/senses/cli-entry.js +60 -8
  508. package/dist/senses/cli-layout.js +187 -0
  509. package/dist/senses/cli.js +813 -281
  510. package/dist/senses/commands.js +66 -3
  511. package/dist/senses/context-packet-ledger.js +371 -0
  512. package/dist/senses/context-packets.js +154 -0
  513. package/dist/senses/continuity.js +94 -0
  514. package/dist/senses/habit-lifecycle-guard.js +24 -0
  515. package/dist/senses/habit-turn-message.js +160 -0
  516. package/dist/senses/ingress-evidence.js +2 -0
  517. package/dist/senses/mail-entry.js +66 -0
  518. package/dist/senses/mail.js +524 -0
  519. package/dist/senses/pipeline.js +1213 -0
  520. package/dist/senses/private-runtime-worker.js +859 -0
  521. package/dist/senses/private-runtime.js +1496 -0
  522. package/dist/senses/proactive-content-guard.js +51 -0
  523. package/dist/senses/sanctuary-download-credit-presentation.js +31 -0
  524. package/dist/senses/sanctuary-full-visibility-contract.js +302 -0
  525. package/dist/senses/sanctuary-grounding.js +118 -0
  526. package/dist/senses/sanctuary-health-acceptance-probe-entry.js +12 -0
  527. package/dist/senses/sanctuary-health-acceptance-probe.js +774 -0
  528. package/dist/senses/sanctuary-health-runner.js +101 -0
  529. package/dist/senses/sanctuary-health.js +408 -0
  530. package/dist/senses/sanctuary-interactive-control.js +306 -0
  531. package/dist/senses/sanctuary-media-catalog-contract.js +235 -0
  532. package/dist/senses/sanctuary-media-optimization.js +514 -0
  533. package/dist/senses/sanctuary-runtime.js +338 -0
  534. package/dist/senses/sanctuary-sab.js +85 -0
  535. package/dist/senses/sanctuary-storage-optimization-contract.js +32 -0
  536. package/dist/senses/shared-turn.js +571 -0
  537. package/dist/senses/surface-tool.js +109 -0
  538. package/dist/senses/teams-entry.js +60 -8
  539. package/dist/senses/teams.js +953 -211
  540. package/dist/senses/telegram-admission.js +711 -0
  541. package/dist/senses/telegram-approval-runtime.js +566 -0
  542. package/dist/senses/telegram-attachments.js +182 -0
  543. package/dist/senses/telegram-audit-ledger.js +220 -0
  544. package/dist/senses/telegram-client.js +1677 -0
  545. package/dist/senses/telegram-effect-adapter.js +859 -0
  546. package/dist/senses/telegram-entry.js +83 -0
  547. package/dist/senses/telegram.js +2026 -0
  548. package/dist/senses/trust-gate.js +207 -2
  549. package/dist/senses/voice/audio-playback.js +237 -0
  550. package/dist/senses/voice/audio-routing.js +119 -0
  551. package/dist/senses/voice/elevenlabs.js +202 -0
  552. package/dist/senses/voice/floor-control.js +431 -0
  553. package/dist/senses/voice/floor-controller.js +115 -0
  554. package/dist/senses/voice/golden-path.js +116 -0
  555. package/dist/senses/voice/index.js +29 -0
  556. package/dist/senses/voice/meeting.js +113 -0
  557. package/dist/senses/voice/outbound.js +190 -0
  558. package/dist/senses/voice/phone.js +33 -0
  559. package/dist/senses/voice/playback.js +139 -0
  560. package/dist/senses/voice/realtime-eval.js +496 -0
  561. package/dist/senses/voice/realtime-trace.js +531 -0
  562. package/dist/senses/voice/transcript.js +70 -0
  563. package/dist/senses/voice/turn.js +191 -0
  564. package/dist/senses/voice/twilio-phone-runtime.js +807 -0
  565. package/dist/senses/voice/twilio-phone.js +5081 -0
  566. package/dist/senses/voice/types.js +2 -0
  567. package/dist/senses/voice/whisper.js +161 -0
  568. package/dist/senses/voice-entry.js +81 -0
  569. package/dist/senses/voice-realtime-eval-command.js +99 -0
  570. package/dist/senses/voice-realtime-eval-entry.js +21 -0
  571. package/dist/senses/voice-twilio-entry.js +87 -0
  572. package/dist/trips/core.js +138 -0
  573. package/dist/trips/store.js +265 -0
  574. package/dist/util/frontmatter.js +69 -0
  575. package/npm-shrinkwrap.json +8144 -0
  576. package/package.json +82 -12
  577. package/skills/agent-commerce.md +113 -0
  578. package/skills/browser-navigation.md +117 -0
  579. package/skills/commerce-setup-guide.md +116 -0
  580. package/skills/commerce-setup.md +84 -0
  581. package/skills/configure-dev-tools.md +99 -0
  582. package/skills/travel-planning.md +138 -0
  583. package/AdoptionSpecialist.ouro/agent.json +0 -20
  584. package/AdoptionSpecialist.ouro/psyche/SOUL.md +0 -22
  585. package/dist/heart/daemon/specialist-orchestrator.js +0 -160
  586. package/dist/heart/daemon/specialist-prompt.js +0 -47
  587. package/dist/heart/daemon/specialist-session.js +0 -142
  588. package/dist/heart/daemon/specialist-tools.js +0 -132
  589. package/dist/heart/daemon/subagent-installer.js +0 -125
  590. package/dist/inner-worker-entry.js +0 -4
  591. package/dist/mind/friends/channel.js +0 -49
  592. package/dist/mind/friends/resolver.js +0 -84
  593. package/dist/mind/friends/store-file.js +0 -171
  594. package/dist/mind/friends/store.js +0 -4
  595. package/dist/mind/friends/tokens.js +0 -26
  596. package/dist/mind/friends/types.js +0 -21
  597. package/dist/repertoire/tasks/board.js +0 -122
  598. package/dist/repertoire/tasks/index.js +0 -210
  599. package/dist/repertoire/tasks/middleware.js +0 -65
  600. package/dist/repertoire/tasks/parser.js +0 -173
  601. package/dist/repertoire/tasks/scanner.js +0 -132
  602. package/dist/repertoire/tasks/transitions.js +0 -145
  603. package/dist/senses/bluebubbles-client.js +0 -279
  604. package/dist/senses/bluebubbles-entry.js +0 -11
  605. package/dist/senses/bluebubbles-model.js +0 -253
  606. package/dist/senses/bluebubbles.js +0 -332
  607. package/dist/senses/inner-dialog-worker.js +0 -61
  608. package/dist/senses/inner-dialog.js +0 -231
  609. package/subagents/README.md +0 -73
  610. package/subagents/work-doer.md +0 -233
  611. package/subagents/work-merger.md +0 -624
  612. package/subagents/work-planner.md +0 -373
  613. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/basilisk.md +0 -0
  614. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/jafar.md +0 -0
  615. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/jormungandr.md +0 -0
  616. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/kaa.md +0 -0
  617. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/medusa.md +0 -0
  618. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/nagini.md +0 -0
  619. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/ouroboros.md +0 -0
  620. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/python.md +0 -0
  621. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/quetzalcoatl.md +0 -0
  622. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/sir-hiss.md +0 -0
  623. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/the-snake.md +0 -0
  624. /package/dist/{repertoire/tasks/types.js → heart/attachments/sources/adapter.js} +0 -0
@@ -0,0 +1,1677 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TELEGRAM_APPROVAL_TOMBSTONE_TTL_MS = exports.TELEGRAM_APPROVAL_TERMINAL_EDIT_TIMEOUT_MS = exports.TELEGRAM_APPROVAL_TTL_MS = exports.FileTelegramPendingApprovalStore = exports.FileTelegramUpdateInboxStore = exports.FileTelegramOffsetStore = exports.TelegramApiError = void 0;
4
+ exports.createTelegramLongPoll = createTelegramLongPoll;
5
+ exports.classifyTelegramPersistedApprovalState = classifyTelegramPersistedApprovalState;
6
+ exports.createTelegramApprovalTransport = createTelegramApprovalTransport;
7
+ exports.escapeTelegramHtml = escapeTelegramHtml;
8
+ exports.splitTelegramText = splitTelegramText;
9
+ exports.sendTelegramText = sendTelegramText;
10
+ exports.createTelegramBotApi = createTelegramBotApi;
11
+ const runtime_1 = require("../nerves/runtime");
12
+ const node_fs_1 = require("node:fs");
13
+ const node_path_1 = require("node:path");
14
+ const node_crypto_1 = require("node:crypto");
15
+ class TelegramApiError extends Error {
16
+ status;
17
+ errorCode;
18
+ retryAfterSeconds;
19
+ constructor(message, options = {}) {
20
+ super(message, options.cause === undefined ? undefined : { cause: options.cause });
21
+ this.name = "TelegramApiError";
22
+ this.status = options.status ?? null;
23
+ this.errorCode = options.errorCode ?? null;
24
+ this.retryAfterSeconds = options.retryAfterSeconds ?? null;
25
+ }
26
+ }
27
+ exports.TelegramApiError = TelegramApiError;
28
+ function durableAtomicWrite(path, contents) {
29
+ const directory = (0, node_path_1.dirname)(path);
30
+ (0, node_fs_1.mkdirSync)(directory, { recursive: true });
31
+ const temporaryPath = `${path}.tmp-${process.pid}-${(0, node_crypto_1.randomUUID)()}`;
32
+ (0, node_fs_1.writeFileSync)(temporaryPath, contents, { mode: 0o600 });
33
+ const temporary = (0, node_fs_1.openSync)(temporaryPath, "r");
34
+ try {
35
+ (0, node_fs_1.fsyncSync)(temporary);
36
+ }
37
+ finally {
38
+ (0, node_fs_1.closeSync)(temporary);
39
+ }
40
+ (0, node_fs_1.renameSync)(temporaryPath, path);
41
+ const directoryHandle = (0, node_fs_1.openSync)(directory, "r");
42
+ try {
43
+ (0, node_fs_1.fsyncSync)(directoryHandle);
44
+ }
45
+ finally {
46
+ (0, node_fs_1.closeSync)(directoryHandle);
47
+ }
48
+ }
49
+ class FileTelegramOffsetStore {
50
+ path;
51
+ constructor(path) {
52
+ this.path = path;
53
+ }
54
+ load() {
55
+ let raw;
56
+ try {
57
+ raw = (0, node_fs_1.readFileSync)(this.path, "utf8");
58
+ }
59
+ catch (error) {
60
+ if (error && typeof error === "object" && "code" in error && error.code === "ENOENT")
61
+ return 0;
62
+ throw error;
63
+ }
64
+ try {
65
+ const value = JSON.parse(raw);
66
+ if (!Number.isSafeInteger(value.nextUpdateId) || value.nextUpdateId < 0)
67
+ throw new Error("invalid offset");
68
+ return value.nextUpdateId;
69
+ }
70
+ catch (cause) {
71
+ throw new Error("Telegram offset state is corrupt", { cause });
72
+ }
73
+ }
74
+ save(nextUpdateId) {
75
+ if (!Number.isSafeInteger(nextUpdateId) || nextUpdateId < 0)
76
+ throw new Error("Telegram offset must be a non-negative safe integer");
77
+ durableAtomicWrite(this.path, `${JSON.stringify({ nextUpdateId })}\n`);
78
+ }
79
+ }
80
+ exports.FileTelegramOffsetStore = FileTelegramOffsetStore;
81
+ const DEFAULT_TELEGRAM_INDETERMINATE_RETENTION_MS = 24 * 60 * 60 * 1_000;
82
+ const DEFAULT_TELEGRAM_MAX_INDETERMINATE_RECEIPTS = 1_000;
83
+ const TELEGRAM_UPDATE_DIGEST_DOMAIN = "ouroboros.telegram.update.v1";
84
+ const TELEGRAM_UPDATE_SEQUENCE_DOMAIN = "ouroboros.telegram.update-sequence.v1";
85
+ const TELEGRAM_UPDATE_DIGEST = /^tgu_[A-Za-z0-9_-]{43}$/u;
86
+ const TELEGRAM_UPDATE_SEQUENCE_DIGEST = /^tgs_[A-Za-z0-9_-]{43}$/u;
87
+ const TELEGRAM_ADMISSION_ID = /^[a-f0-9]{20}$/u;
88
+ const TELEGRAM_SESSION_EVENT_ID = /^evt-[0-9]{6,}$/u;
89
+ function boundedTelegramWorkText(value, maxLength) {
90
+ return typeof value === "string" && value.length > 0 && value.length <= maxLength;
91
+ }
92
+ function updateReceipt(update) {
93
+ return {
94
+ digest: `tgu_${(0, node_crypto_1.createHash)("sha256").update(`${TELEGRAM_UPDATE_DIGEST_DOMAIN}\0${JSON.stringify(update)}`, "utf8").digest("base64url")}`,
95
+ sequenceDigest: `tgs_${(0, node_crypto_1.createHash)("sha256").update(`${TELEGRAM_UPDATE_SEQUENCE_DOMAIN}\0${update.update_id}`, "utf8").digest("base64url")}`,
96
+ updateClass: update.callback_query ? "callback" : update.message ? "message" : "other",
97
+ };
98
+ }
99
+ function sameReceipt(left, right) {
100
+ return left.digest === right.digest;
101
+ }
102
+ function uniqueReceipts(records) {
103
+ return records.filter((record, index) => records.findIndex((candidate) => sameReceipt(candidate, record)) === index);
104
+ }
105
+ class FileTelegramUpdateInboxStore {
106
+ path;
107
+ now;
108
+ indeterminateRetentionMs;
109
+ maxIndeterminateReceipts;
110
+ constructor(path, options = {}) {
111
+ this.path = path;
112
+ this.now = options.now ?? Date.now;
113
+ this.indeterminateRetentionMs = options.indeterminateRetentionMs ?? DEFAULT_TELEGRAM_INDETERMINATE_RETENTION_MS;
114
+ this.maxIndeterminateReceipts = options.maxIndeterminateReceipts ?? DEFAULT_TELEGRAM_MAX_INDETERMINATE_RECEIPTS;
115
+ if (!Number.isSafeInteger(this.indeterminateRetentionMs) || this.indeterminateRetentionMs < 1) {
116
+ throw new Error("Telegram indeterminate receipt retention must be a positive safe integer");
117
+ }
118
+ if (!Number.isSafeInteger(this.maxIndeterminateReceipts) || this.maxIndeterminateReceipts < 1) {
119
+ throw new Error("Telegram indeterminate receipt limit must be a positive safe integer");
120
+ }
121
+ }
122
+ timestamp() {
123
+ const value = this.now();
124
+ if (!Number.isSafeInteger(value) || value < 0)
125
+ throw new Error("Telegram inbox clock must return a non-negative safe integer");
126
+ return value;
127
+ }
128
+ prune(records, timestamp) {
129
+ const cutoff = timestamp - this.indeterminateRetentionMs;
130
+ return [...records]
131
+ .filter((record) => record.quarantinedAt >= cutoff)
132
+ .sort((left, right) => left.quarantinedAt - right.quarantinedAt || left.digest.localeCompare(right.digest))
133
+ .slice(-this.maxIndeterminateReceipts);
134
+ }
135
+ read() {
136
+ try {
137
+ const value = JSON.parse((0, node_fs_1.readFileSync)(this.path, "utf8"));
138
+ const validReceipt = (record) => Boolean(record)
139
+ && typeof record === "object"
140
+ && !Array.isArray(record)
141
+ && Object.keys(record).sort().join(",") === "digest,sequenceDigest,updateClass"
142
+ && TELEGRAM_UPDATE_DIGEST.test(record.digest)
143
+ && TELEGRAM_UPDATE_SEQUENCE_DIGEST.test(record.sequenceDigest)
144
+ && ["callback", "message", "other"].includes(record.updateClass);
145
+ const validIndeterminate = (record) => Boolean(record)
146
+ && typeof record === "object"
147
+ && !Array.isArray(record)
148
+ && Object.keys(record).sort().join(",") === "digest,quarantinedAt,sequenceDigest,updateClass,warningAcknowledged"
149
+ && TELEGRAM_UPDATE_DIGEST.test(record.digest)
150
+ && TELEGRAM_UPDATE_SEQUENCE_DIGEST.test(record.sequenceDigest)
151
+ && ["callback", "message", "other"].includes(record.updateClass)
152
+ && Number.isSafeInteger(record.quarantinedAt)
153
+ && record.quarantinedAt >= 0
154
+ && typeof record.warningAcknowledged === "boolean";
155
+ const validAdmitted = (record) => Boolean(record)
156
+ && typeof record === "object"
157
+ && !Array.isArray(record)
158
+ && Object.keys(record).sort().join(",") === "admissionId,eventId,friendId,reference,sessionKey"
159
+ && TELEGRAM_ADMISSION_ID.test(record.admissionId)
160
+ && boundedTelegramWorkText(record.friendId, 512)
161
+ && boundedTelegramWorkText(record.sessionKey, 1_024)
162
+ && TELEGRAM_SESSION_EVENT_ID.test(record.eventId)
163
+ && record.reference === `telegram-admission:${record.admissionId}`;
164
+ const validIndeterminateAdmitted = (record) => {
165
+ if (!record || typeof record !== "object" || Array.isArray(record)
166
+ || Object.keys(record).sort().join(",") !== "admissionId,eventId,friendId,quarantinedAt,reference,sessionKey")
167
+ return false;
168
+ const { quarantinedAt, ...work } = record;
169
+ return Number.isSafeInteger(quarantinedAt) && quarantinedAt >= 0 && validAdmitted(work);
170
+ };
171
+ if (value.version === 5) {
172
+ if (Object.keys(value).sort().join(",") !== "admittedDispatching,admittedIndeterminate,admittedPending,admittedSettled,dispatching,indeterminate,pending,settled,version"
173
+ || !Array.isArray(value.pending) || !Array.isArray(value.dispatching) || !Array.isArray(value.indeterminate) || !Array.isArray(value.settled)
174
+ || !Array.isArray(value.admittedPending) || !Array.isArray(value.admittedDispatching)
175
+ || !Array.isArray(value.admittedSettled) || !Array.isArray(value.admittedIndeterminate)
176
+ || !value.pending.every(validReceipt) || !value.dispatching.every(validReceipt) || !value.settled.every(validReceipt)
177
+ || !value.indeterminate.every(validIndeterminate)
178
+ || !value.admittedPending.every(validAdmitted) || !value.admittedDispatching.every(validAdmitted)
179
+ || !value.admittedSettled.every(validAdmitted) || !value.admittedIndeterminate.every(validIndeterminateAdmitted)) {
180
+ throw new Error("invalid bounded inbox shape");
181
+ }
182
+ const state = structuredClone(value);
183
+ const timestamp = this.timestamp();
184
+ const prunedUpdates = this.prune(state.indeterminate, timestamp);
185
+ const prunedAdmitted = state.admittedIndeterminate
186
+ .filter((work) => work.quarantinedAt >= timestamp - this.indeterminateRetentionMs)
187
+ .sort((left, right) => left.quarantinedAt - right.quarantinedAt || left.admissionId.localeCompare(right.admissionId))
188
+ .slice(-this.maxIndeterminateReceipts);
189
+ if (prunedUpdates.length !== state.indeterminate.length || prunedAdmitted.length !== state.admittedIndeterminate.length) {
190
+ state.indeterminate = prunedUpdates;
191
+ state.admittedIndeterminate = prunedAdmitted;
192
+ this.write(state);
193
+ }
194
+ return state;
195
+ }
196
+ if (value.version === 4) {
197
+ if (Object.keys(value).sort().join(",") !== "dispatching,indeterminate,pending,settled,version"
198
+ || !Array.isArray(value.pending) || !Array.isArray(value.dispatching) || !Array.isArray(value.indeterminate)
199
+ || !Array.isArray(value.settled)
200
+ || !value.pending.every(validReceipt) || !value.dispatching.every(validReceipt)
201
+ || !value.settled.every(validReceipt)
202
+ || !value.indeterminate.every(validIndeterminate))
203
+ throw new Error("invalid bounded inbox shape");
204
+ const state = {
205
+ ...structuredClone(value),
206
+ version: 5,
207
+ admittedPending: [],
208
+ admittedDispatching: [],
209
+ admittedSettled: [],
210
+ admittedIndeterminate: [],
211
+ };
212
+ const pruned = this.prune(state.indeterminate, this.timestamp());
213
+ state.indeterminate = pruned;
214
+ this.write(state);
215
+ return state;
216
+ }
217
+ if (value.version === 3) {
218
+ if (Object.keys(value).sort().join(",") !== "dispatching,indeterminate,pending,version"
219
+ || !Array.isArray(value.pending) || !Array.isArray(value.dispatching) || !Array.isArray(value.indeterminate)
220
+ || !value.pending.every(validReceipt) || !value.dispatching.every(validReceipt)
221
+ || !value.indeterminate.every(validIndeterminate))
222
+ throw new Error("invalid bounded inbox shape");
223
+ const timestamp = this.timestamp();
224
+ const migrated = {
225
+ version: 5,
226
+ pending: structuredClone(value.pending),
227
+ dispatching: structuredClone(value.dispatching),
228
+ settled: [],
229
+ indeterminate: this.prune(structuredClone(value.indeterminate), timestamp),
230
+ admittedPending: [], admittedDispatching: [], admittedSettled: [], admittedIndeterminate: [],
231
+ };
232
+ this.write(migrated);
233
+ return migrated;
234
+ }
235
+ if (value.version === 2) {
236
+ if (Object.keys(value).sort().join(",") !== "dispatching,indeterminate,pending,version")
237
+ throw new Error("invalid opaque inbox shape");
238
+ const arrays = [value.pending, value.dispatching, value.indeterminate];
239
+ if (!arrays.every(Array.isArray))
240
+ throw new Error("invalid opaque inbox shape");
241
+ if (!arrays.flat().every(validReceipt))
242
+ throw new Error("invalid opaque inbox receipt");
243
+ const timestamp = this.timestamp();
244
+ const migrated = {
245
+ version: 5,
246
+ pending: structuredClone(value.pending),
247
+ dispatching: structuredClone(value.dispatching),
248
+ settled: [],
249
+ indeterminate: this.prune(value.indeterminate.map((record) => ({
250
+ ...record,
251
+ quarantinedAt: timestamp,
252
+ warningAcknowledged: false,
253
+ })), timestamp),
254
+ admittedPending: [], admittedDispatching: [], admittedSettled: [], admittedIndeterminate: [],
255
+ };
256
+ this.write(migrated);
257
+ return migrated;
258
+ }
259
+ const pending = value.pending;
260
+ const dispatching = value.dispatching ?? [];
261
+ const completed = value.completedUpdateIds;
262
+ if (!Array.isArray(pending) || !Array.isArray(dispatching) || !Array.isArray(completed))
263
+ throw new Error("invalid legacy inbox shape");
264
+ const validLegacy = (update) => Boolean(update)
265
+ && typeof update === "object"
266
+ && Number.isSafeInteger(update.update_id)
267
+ && update.update_id >= 0;
268
+ if (![...pending, ...dispatching].every(validLegacy)
269
+ || !completed.every((id) => Number.isSafeInteger(id) && id >= 0))
270
+ throw new Error("invalid legacy inbox record");
271
+ const timestamp = this.timestamp();
272
+ const migrated = {
273
+ version: 5,
274
+ pending: [],
275
+ dispatching: [],
276
+ settled: [],
277
+ indeterminate: this.prune(uniqueReceipts([...pending, ...dispatching].map(updateReceipt)).map((record) => ({
278
+ ...record,
279
+ quarantinedAt: timestamp,
280
+ warningAcknowledged: false,
281
+ })), timestamp),
282
+ admittedPending: [], admittedDispatching: [], admittedSettled: [], admittedIndeterminate: [],
283
+ };
284
+ this.write(migrated);
285
+ return migrated;
286
+ }
287
+ catch (error) {
288
+ if (error && typeof error === "object" && "code" in error && error.code === "ENOENT")
289
+ return {
290
+ version: 5, pending: [], dispatching: [], settled: [], indeterminate: [],
291
+ admittedPending: [], admittedDispatching: [], admittedSettled: [], admittedIndeterminate: [],
292
+ };
293
+ throw new Error("Telegram update inbox state is corrupt", { cause: error });
294
+ }
295
+ }
296
+ write(state) {
297
+ durableAtomicWrite(this.path, `${JSON.stringify(state)}\n`);
298
+ }
299
+ load() {
300
+ const state = this.read();
301
+ return [...state.pending, ...state.dispatching, ...state.settled, ...state.indeterminate];
302
+ }
303
+ loadPending() {
304
+ return this.read().pending;
305
+ }
306
+ loadIndeterminate() {
307
+ return this.read().indeterminate;
308
+ }
309
+ quarantineStranded() {
310
+ const state = this.read();
311
+ const stranded = uniqueReceipts([...state.pending, ...state.dispatching]);
312
+ const timestamp = this.timestamp();
313
+ state.pending = [];
314
+ state.dispatching = [];
315
+ const combined = [...state.indeterminate, ...stranded.map((record) => ({
316
+ ...record,
317
+ quarantinedAt: timestamp,
318
+ warningAcknowledged: false,
319
+ }))];
320
+ state.indeterminate = this.prune(combined.filter((record, index) => (combined.findIndex((candidate) => sameReceipt(candidate, record)) === index)), timestamp);
321
+ const warnings = state.indeterminate.filter((record) => !record.warningAcknowledged);
322
+ if (stranded.length > 0)
323
+ this.write(state);
324
+ return warnings.map(({ quarantinedAt: _quarantinedAt, warningAcknowledged: _warningAcknowledged, ...receipt }) => receipt);
325
+ }
326
+ acknowledgeIndeterminateWarning(receipt) {
327
+ const state = this.read();
328
+ const warning = state.indeterminate.find((candidate) => sameReceipt(candidate, receipt));
329
+ if (!warning || warning.warningAcknowledged)
330
+ return false;
331
+ warning.warningAcknowledged = true;
332
+ this.write(state);
333
+ return true;
334
+ }
335
+ capture(update) {
336
+ const state = this.read();
337
+ const receipt = updateReceipt(update);
338
+ const existing = [...state.pending, ...state.dispatching, ...state.settled, ...state.indeterminate]
339
+ .find((candidate) => candidate.sequenceDigest === receipt.sequenceDigest);
340
+ if (existing) {
341
+ if (!sameReceipt(existing, receipt))
342
+ throw new Error("Telegram update inbox has a conflicting opaque receipt");
343
+ return false;
344
+ }
345
+ state.pending.push(receipt);
346
+ this.write(state);
347
+ return true;
348
+ }
349
+ claim(update) {
350
+ const state = this.read();
351
+ const receipt = updateReceipt(update);
352
+ if (state.dispatching.some((candidate) => sameReceipt(candidate, receipt)))
353
+ return false;
354
+ const index = state.pending.findIndex((candidate) => sameReceipt(candidate, receipt));
355
+ if (index < 0)
356
+ return false;
357
+ state.pending.splice(index, 1);
358
+ state.dispatching.push(receipt);
359
+ this.write(state);
360
+ return true;
361
+ }
362
+ complete(update) {
363
+ const state = this.read();
364
+ const receipt = updateReceipt(update);
365
+ const completed = [...state.pending, ...state.dispatching].some((candidate) => sameReceipt(candidate, receipt));
366
+ state.pending = state.pending.filter((candidate) => !sameReceipt(candidate, receipt));
367
+ state.dispatching = state.dispatching.filter((candidate) => !sameReceipt(candidate, receipt));
368
+ if (completed && !state.settled.some((candidate) => sameReceipt(candidate, receipt)))
369
+ state.settled.push(receipt);
370
+ if (completed)
371
+ this.write(state);
372
+ }
373
+ commit(update) {
374
+ const state = this.read();
375
+ const receipt = updateReceipt(update);
376
+ const settled = state.settled.filter((candidate) => !sameReceipt(candidate, receipt));
377
+ if (settled.length === state.settled.length)
378
+ return;
379
+ state.settled = settled;
380
+ this.write(state);
381
+ }
382
+ captureAdmittedWork(work) {
383
+ if (!this.validAdmittedWork(work))
384
+ throw new Error("Telegram admitted work is invalid");
385
+ const state = this.read();
386
+ const existing = [
387
+ ...state.admittedPending,
388
+ ...state.admittedDispatching,
389
+ ...state.admittedSettled,
390
+ ...state.admittedIndeterminate,
391
+ ].find((candidate) => candidate.admissionId === work.admissionId);
392
+ if (existing) {
393
+ const { quarantinedAt: _quarantinedAt, ...canonical } = existing;
394
+ if (JSON.stringify(canonical) !== JSON.stringify(work))
395
+ throw new Error("Telegram admitted work has a conflicting receipt");
396
+ return false;
397
+ }
398
+ state.admittedPending.push(structuredClone(work));
399
+ this.write(state);
400
+ return true;
401
+ }
402
+ loadPendingAdmittedWork() {
403
+ return structuredClone(this.read().admittedPending);
404
+ }
405
+ claimAdmittedWork(admissionId) {
406
+ if (!TELEGRAM_ADMISSION_ID.test(admissionId))
407
+ throw new Error("Telegram admission id is invalid");
408
+ const state = this.read();
409
+ if (state.admittedDispatching.some((candidate) => candidate.admissionId === admissionId)
410
+ || state.admittedSettled.some((candidate) => candidate.admissionId === admissionId)
411
+ || state.admittedIndeterminate.some((candidate) => candidate.admissionId === admissionId))
412
+ return null;
413
+ const index = state.admittedPending.findIndex((candidate) => candidate.admissionId === admissionId);
414
+ if (index < 0)
415
+ return null;
416
+ const [work] = state.admittedPending.splice(index, 1);
417
+ state.admittedDispatching.push(work);
418
+ this.write(state);
419
+ return structuredClone(work);
420
+ }
421
+ completeAdmittedWork(admissionId) {
422
+ if (!TELEGRAM_ADMISSION_ID.test(admissionId))
423
+ throw new Error("Telegram admission id is invalid");
424
+ const state = this.read();
425
+ const index = state.admittedDispatching.findIndex((candidate) => candidate.admissionId === admissionId);
426
+ if (index < 0)
427
+ return;
428
+ const [work] = state.admittedDispatching.splice(index, 1);
429
+ state.admittedSettled.push(work);
430
+ state.admittedSettled = state.admittedSettled.slice(-this.maxIndeterminateReceipts);
431
+ this.write(state);
432
+ }
433
+ quarantineStrandedAdmittedWork() {
434
+ const state = this.read();
435
+ if (state.admittedDispatching.length === 0)
436
+ return [];
437
+ const timestamp = this.timestamp();
438
+ const stranded = state.admittedDispatching.map((work) => ({ ...work, quarantinedAt: timestamp }));
439
+ state.admittedDispatching = [];
440
+ state.admittedIndeterminate = [...state.admittedIndeterminate, ...stranded]
441
+ .filter((work) => work.quarantinedAt >= timestamp - this.indeterminateRetentionMs)
442
+ .sort((left, right) => left.quarantinedAt - right.quarantinedAt || left.admissionId.localeCompare(right.admissionId))
443
+ .slice(-this.maxIndeterminateReceipts);
444
+ this.write(state);
445
+ return stranded.map(({ quarantinedAt: _quarantinedAt, ...work }) => work);
446
+ }
447
+ admittedWorkState(admissionId) {
448
+ if (!TELEGRAM_ADMISSION_ID.test(admissionId))
449
+ throw new Error("Telegram admission id is invalid");
450
+ const state = this.read();
451
+ if (state.admittedPending.some((work) => work.admissionId === admissionId))
452
+ return "pending";
453
+ if (state.admittedDispatching.some((work) => work.admissionId === admissionId))
454
+ return "dispatching";
455
+ if (state.admittedSettled.some((work) => work.admissionId === admissionId))
456
+ return "settled";
457
+ if (state.admittedIndeterminate.some((work) => work.admissionId === admissionId))
458
+ return "indeterminate";
459
+ return null;
460
+ }
461
+ validAdmittedWork(work) {
462
+ return Boolean(work) && typeof work === "object" && !Array.isArray(work)
463
+ && Object.keys(work).sort().join(",") === "admissionId,eventId,friendId,reference,sessionKey"
464
+ && TELEGRAM_ADMISSION_ID.test(work.admissionId)
465
+ && boundedTelegramWorkText(work.friendId, 512)
466
+ && boundedTelegramWorkText(work.sessionKey, 1_024)
467
+ && TELEGRAM_SESSION_EVENT_ID.test(work.eventId)
468
+ && work.reference === `telegram-admission:${work.admissionId}`;
469
+ }
470
+ }
471
+ exports.FileTelegramUpdateInboxStore = FileTelegramUpdateInboxStore;
472
+ function createTelegramLongPoll(options) {
473
+ let nextUpdateId = options.offsetStore.load();
474
+ const shutdown = new AbortController();
475
+ const retryAfterPollError = (signal) => new Promise((resolve, reject) => {
476
+ const timer = setTimeout(resolve, 1_000);
477
+ signal.addEventListener("abort", () => {
478
+ clearTimeout(timer);
479
+ reject(signal.reason);
480
+ }, { once: true });
481
+ });
482
+ const inboundAttachments = (message) => {
483
+ const result = [];
484
+ const push = (media, kind, fallbackName, fallbackMimeType) => {
485
+ if (typeof media?.file_id !== "string" || !media.file_id.trim())
486
+ return;
487
+ result.push({
488
+ fileId: media.file_id,
489
+ kind,
490
+ displayName: media.file_name?.trim() || fallbackName,
491
+ ...((media.mime_type?.trim() || fallbackMimeType) ? { mimeType: media.mime_type?.trim().toLowerCase() || fallbackMimeType } : {}),
492
+ ...(Number.isSafeInteger(media.file_size) ? { byteCount: media.file_size } : {}),
493
+ });
494
+ };
495
+ const photo = message.photo?.filter((candidate) => typeof candidate.file_id === "string" && candidate.file_id.trim())
496
+ .sort((left, right) => (left.file_size ?? left.width ?? 0) - (right.file_size ?? right.width ?? 0)).at(-1);
497
+ if (photo)
498
+ result.push({ fileId: photo.file_id, kind: "image", displayName: "telegram-photo.jpg", mimeType: "image/jpeg", ...(Number.isSafeInteger(photo.file_size) ? { byteCount: photo.file_size } : {}) });
499
+ push(message.document, "document", "telegram-document");
500
+ push(message.audio, "audio", "telegram-audio", "audio/mpeg");
501
+ push(message.voice, "audio", "telegram-voice.ogg", "audio/ogg");
502
+ push(message.video, "binary", "telegram-video.mp4", "video/mp4");
503
+ push(message.animation, "binary", "telegram-animation.mp4", "video/mp4");
504
+ push(message.sticker, message.sticker?.is_animated || message.sticker?.is_video ? "binary" : "image", message.sticker?.is_animated ? "telegram-sticker.tgs" : message.sticker?.is_video ? "telegram-sticker.webm" : "telegram-sticker.webp", message.sticker?.is_animated ? "application/x-tgsticker" : message.sticker?.is_video ? "video/webm" : "image/webp");
505
+ return result;
506
+ };
507
+ const rawAttachmentCount = (message) => [
508
+ message.document, message.audio, message.video, message.voice, message.animation, message.sticker,
509
+ ].filter(Boolean).length + (message.photo?.length ? 1 : 0);
510
+ const authorizedMessage = (update) => {
511
+ const message = update.message;
512
+ const userId = message?.from ? String(message.from.id) : "";
513
+ const chatId = message ? String(message.chat.id) : "";
514
+ if (!message || message.chat.type !== "private" || userId !== options.expectedUserId || chatId !== options.expectedChatId)
515
+ return null;
516
+ const attachments = inboundAttachments(message);
517
+ const attachmentCount = rawAttachmentCount(message);
518
+ const attachmentNotices = attachmentCount > attachments.length
519
+ ? ["attachment unavailable: Telegram media metadata was incomplete"]
520
+ : [];
521
+ const text = message.text ?? message.caption ?? "";
522
+ if (typeof text !== "string" || (!text && attachmentCount === 0))
523
+ return null;
524
+ return {
525
+ updateId: update.update_id,
526
+ messageId: String(message.message_id),
527
+ userId,
528
+ chatId,
529
+ text,
530
+ attachments,
531
+ ...(attachmentNotices.length ? { attachmentNotices } : {}),
532
+ ...(Number.isSafeInteger(message.reply_to_message?.message_id) && message.reply_to_message.message_id > 0
533
+ ? { replyToMessageId: String(message.reply_to_message.message_id) }
534
+ : {}),
535
+ };
536
+ };
537
+ const unknownMessage = (update) => {
538
+ const message = update.message;
539
+ if (!options.onUnknownMessage || !options.botId || !message?.from || message.chat.type !== "private")
540
+ return null;
541
+ const userId = String(message.from.id);
542
+ const chatId = String(message.chat.id);
543
+ if (userId === options.expectedUserId && chatId === options.expectedChatId)
544
+ return null;
545
+ const displayLabel = [message.from.first_name, message.from.last_name].filter(Boolean).join(" ")
546
+ || (message.from.username ? `@${message.from.username}` : `Telegram user ${userId}`);
547
+ const attachments = inboundAttachments(message);
548
+ const attachmentCount = rawAttachmentCount(message);
549
+ return {
550
+ updateId: update.update_id,
551
+ messageId: message.message_id,
552
+ botId: options.botId,
553
+ userId,
554
+ chatId,
555
+ text: message.text ?? message.caption ?? "",
556
+ displayLabel,
557
+ hasAttachments: attachmentCount > 0,
558
+ attachments,
559
+ ...(attachmentCount > attachments.length ? { attachmentNotices: ["attachment unavailable: Telegram media metadata was incomplete"] } : {}),
560
+ };
561
+ };
562
+ const authorizedCallback = (update) => {
563
+ const callback = update.callback_query;
564
+ return Boolean(callback?.message
565
+ && String(callback.from.id) === options.expectedUserId
566
+ && String(callback.message.chat.id) === options.expectedChatId);
567
+ };
568
+ const dispatch = async (update) => {
569
+ const handled = !update.callback_query || authorizedCallback(update)
570
+ ? await options.onUpdate?.(update) ?? false
571
+ : false;
572
+ if (handled)
573
+ return;
574
+ const message = authorizedMessage(update);
575
+ if (message) {
576
+ await options.onMessage(message);
577
+ return;
578
+ }
579
+ const stranger = unknownMessage(update);
580
+ if (stranger) {
581
+ await options.onUnknownMessage(stranger);
582
+ return;
583
+ }
584
+ const distinctAccount = Boolean(update.message?.from && String(update.message.from.id) !== options.expectedUserId);
585
+ (0, runtime_1.emitNervesEvent)({
586
+ component: "senses",
587
+ event: "telegram.update_dropped",
588
+ message: "Telegram update dropped before dispatch",
589
+ meta: {
590
+ updateClass: update.message ? "message" : "other",
591
+ reason: "unauthorized_or_unsupported",
592
+ distinctAccount,
593
+ ...options.acceptanceEventMeta?.(update, distinctAccount),
594
+ },
595
+ });
596
+ };
597
+ const pollOnce = async (signal) => {
598
+ const requestSignal = signal ? AbortSignal.any([shutdown.signal, signal]) : shutdown.signal;
599
+ if (requestSignal.aborted)
600
+ throw new Error("Telegram long poll stopped");
601
+ options.onBeforeDispatch?.();
602
+ const stranded = options.inboxStore?.quarantineStranded?.() ?? options.inboxStore?.loadIndeterminate() ?? [];
603
+ for (const indeterminate of stranded) {
604
+ (0, runtime_1.emitNervesEvent)({
605
+ level: "warn",
606
+ component: "senses",
607
+ event: "telegram.update_dropped",
608
+ message: "Telegram update dispatch outcome is indeterminate after restart",
609
+ meta: { updateClass: indeterminate.updateClass, reason: "dispatch_indeterminate", ...options.acceptanceEventMeta?.() },
610
+ });
611
+ options.onDispatchSettled?.();
612
+ options.inboxStore?.acknowledgeIndeterminateWarning(indeterminate);
613
+ }
614
+ const updates = await options.api.request("getUpdates", {
615
+ offset: nextUpdateId,
616
+ timeout: 50,
617
+ allowed_updates: ["message", "callback_query"],
618
+ }, requestSignal);
619
+ if (!Array.isArray(updates))
620
+ throw new Error("Telegram getUpdates result must be an array");
621
+ for (const update of updates) {
622
+ if (!update || !Number.isSafeInteger(update.update_id) || update.update_id < nextUpdateId)
623
+ continue;
624
+ options.onBeforeDispatch?.();
625
+ const next = update.update_id + 1;
626
+ const requiresDurableDispatch = Boolean(authorizedCallback(update) || authorizedMessage(update) || unknownMessage(update));
627
+ const newlyCaptured = requiresDurableDispatch ? (options.inboxStore?.capture(update) ?? true) : true;
628
+ if (newlyCaptured) {
629
+ if (requiresDurableDispatch && options.inboxStore && !options.inboxStore.claim(update)) {
630
+ options.onDispatchSettled?.();
631
+ options.offsetStore.save(next);
632
+ nextUpdateId = next;
633
+ options.inboxStore.commit?.(update);
634
+ continue;
635
+ }
636
+ let dispatchError;
637
+ try {
638
+ await dispatch(update);
639
+ if (requiresDurableDispatch)
640
+ options.inboxStore?.complete(update);
641
+ }
642
+ catch (error) {
643
+ options.inboxStore?.quarantineStranded?.();
644
+ dispatchError = error;
645
+ }
646
+ try {
647
+ options.onDispatchSettled?.();
648
+ }
649
+ catch (auditError) {
650
+ if (dispatchError !== undefined)
651
+ throw new AggregateError([dispatchError, auditError], "Telegram dispatch and acceptance audit verification failed");
652
+ throw auditError;
653
+ }
654
+ if (dispatchError !== undefined)
655
+ throw dispatchError;
656
+ }
657
+ else
658
+ options.onDispatchSettled?.();
659
+ options.offsetStore.save(next);
660
+ nextUpdateId = next;
661
+ if (requiresDurableDispatch)
662
+ options.inboxStore?.commit?.(update);
663
+ }
664
+ return nextUpdateId;
665
+ };
666
+ return {
667
+ pollOnce,
668
+ async run(signal) {
669
+ const runSignal = signal ? AbortSignal.any([shutdown.signal, signal]) : shutdown.signal;
670
+ while (!runSignal.aborted) {
671
+ try {
672
+ await pollOnce(runSignal);
673
+ }
674
+ catch (error) {
675
+ if (shutdown.signal.aborted || signal?.aborted)
676
+ return;
677
+ if (!(error instanceof TelegramApiError))
678
+ throw error;
679
+ (0, runtime_1.emitNervesEvent)({
680
+ level: "warn",
681
+ component: "senses",
682
+ event: "telegram.poll_retry",
683
+ message: "Telegram long poll request failed; retrying",
684
+ meta: { status: error.status, errorCode: error.errorCode },
685
+ });
686
+ await retryAfterPollError(runSignal);
687
+ }
688
+ }
689
+ },
690
+ stop() {
691
+ shutdown.abort(new Error("Telegram long poll stopped"));
692
+ },
693
+ };
694
+ }
695
+ function classifyTelegramPersistedApprovalState(pending) {
696
+ const hasAttempt = pending.decisionAttempt !== undefined;
697
+ const hasTerminal = pending.terminal !== undefined;
698
+ const hasTerminalMac = pending.terminalMac !== undefined;
699
+ const hasReceipt = pending.settlementReceipt !== undefined;
700
+ const hasAuthority = hasAttempt || hasTerminalMac || hasReceipt;
701
+ const hasTombstoneState = pending.terminalizedAt !== undefined
702
+ || pending.tombstoneExpiresAt !== undefined
703
+ || pending.tombstoneMac !== undefined
704
+ || pending.staleTap !== undefined;
705
+ const hasCompleteTombstoneState = pending.terminalizedAt !== undefined
706
+ && pending.tombstoneExpiresAt !== undefined
707
+ && pending.tombstoneMac !== undefined
708
+ && pending.expiryObservation !== undefined;
709
+ const hasExclusiveTerminalState = pending.expiryObservation !== undefined || hasTombstoneState;
710
+ const messageIdNumber = typeof pending.messageId === "string" ? Number(pending.messageId) : Number.NaN;
711
+ const hasCanonicalMessageId = typeof pending.messageId === "string"
712
+ && /^[1-9][0-9]*$/u.test(pending.messageId)
713
+ && Number.isSafeInteger(messageIdNumber)
714
+ && String(messageIdNumber) === pending.messageId;
715
+ const validTerminal = hasTerminal
716
+ && typeof pending.terminal.accepted === "boolean"
717
+ && typeof pending.terminal.terminalText === "string"
718
+ && pending.terminal.terminalText.length > 0
719
+ && pending.terminal.terminalText.length <= 4_096;
720
+ const exactDeliveryInterruption = pending.terminalKind === "delivery_interruption"
721
+ && pending.deliveryState === "delivery_indeterminate"
722
+ && pending.messageId === null
723
+ && validTerminal
724
+ && pending.terminal.accepted === false
725
+ && !hasAuthority
726
+ && !hasExclusiveTerminalState;
727
+ if (pending.terminalKind !== undefined) {
728
+ if (!exactDeliveryInterruption)
729
+ throw new Error("Telegram persisted delivery-interruption terminal state is invalid");
730
+ return "delivery_interruption";
731
+ }
732
+ if (hasAuthority && hasExclusiveTerminalState) {
733
+ throw new Error("Telegram persisted action authority cannot coexist with expiry or tombstone state");
734
+ }
735
+ if (pending.deliveryState === "terminal_tombstone") {
736
+ if (!hasCompleteTombstoneState)
737
+ throw new Error("Telegram persisted terminal tombstone is structurally incomplete");
738
+ }
739
+ else if (hasTombstoneState) {
740
+ throw new Error("Telegram persisted tombstone state has invalid delivery routing");
741
+ }
742
+ if (hasAttempt && (pending.deliveryState !== "bound" || !hasCanonicalMessageId)) {
743
+ throw new Error("Telegram persisted decision attempt has invalid delivery routing");
744
+ }
745
+ if (hasReceipt && (!hasAttempt || !hasTerminal || !hasTerminalMac)) {
746
+ throw new Error("Telegram persisted settlement receipt is structurally incomplete");
747
+ }
748
+ if (hasTerminalMac && (!hasAttempt || !hasTerminal)) {
749
+ throw new Error("Telegram persisted terminal authentication is structurally incomplete");
750
+ }
751
+ if (hasTerminal) {
752
+ if (!validTerminal || !hasAttempt || !hasTerminalMac) {
753
+ throw new Error("Telegram persisted action terminal state is structurally incomplete");
754
+ }
755
+ return "action_terminal";
756
+ }
757
+ if (pending.deliveryState === "terminal_tombstone")
758
+ return "terminal_tombstone";
759
+ if (pending.expiryObservation !== undefined) {
760
+ if (pending.deliveryState !== "bound" || !hasCanonicalMessageId) {
761
+ throw new Error("Telegram persisted expiry observation has invalid delivery routing");
762
+ }
763
+ return "expiry_observed";
764
+ }
765
+ return hasAttempt ? "decision_attempt" : "ordinary";
766
+ }
767
+ class FileTelegramPendingApprovalStore {
768
+ path;
769
+ constructor(path) {
770
+ this.path = path;
771
+ }
772
+ load() {
773
+ try {
774
+ const value = JSON.parse((0, node_fs_1.readFileSync)(this.path, "utf8"));
775
+ if (!Array.isArray(value))
776
+ throw new Error("pending approvals must be an array");
777
+ return structuredClone(value);
778
+ }
779
+ catch (error) {
780
+ if (error && typeof error === "object" && "code" in error && error.code === "ENOENT")
781
+ return [];
782
+ throw new Error("Telegram pending approval state is corrupt", { cause: error });
783
+ }
784
+ }
785
+ save(records) {
786
+ durableAtomicWrite(this.path, `${JSON.stringify(records)}\n`);
787
+ }
788
+ }
789
+ exports.FileTelegramPendingApprovalStore = FileTelegramPendingApprovalStore;
790
+ exports.TELEGRAM_APPROVAL_TTL_MS = 300_000;
791
+ exports.TELEGRAM_APPROVAL_TERMINAL_EDIT_TIMEOUT_MS = 30_000;
792
+ exports.TELEGRAM_APPROVAL_TOMBSTONE_TTL_MS = 600_000;
793
+ function assertTelegramCallbackData(value) {
794
+ const bytes = Buffer.byteLength(value, "utf8");
795
+ if (bytes < 1 || bytes > 64)
796
+ throw new Error("Telegram callback_data must be 1 to 64 bytes");
797
+ }
798
+ function createTelegramApprovalTransport(options) {
799
+ const now = options.now ?? Date.now;
800
+ const effectBarrier = options.effectBarrier ?? (() => undefined);
801
+ const pendingByCallback = new Map();
802
+ const approvalOperations = new Map();
803
+ const uniquePending = () => [...new Set(pendingByCallback.values())];
804
+ const persist = () => {
805
+ effectBarrier();
806
+ options.pendingStore.save(uniquePending().map(({ decisionToken: _secret, ...record }) => record));
807
+ };
808
+ const persistMutation = (pending, mutate) => {
809
+ const before = structuredClone(pending);
810
+ try {
811
+ mutate();
812
+ persist();
813
+ }
814
+ catch (error) {
815
+ for (const key of Object.keys(pending))
816
+ delete pending[key];
817
+ Object.assign(pending, before);
818
+ throw error;
819
+ }
820
+ };
821
+ const add = (pending) => {
822
+ pendingByCallback.set(pending.approveCallbackData, pending);
823
+ pendingByCallback.set(pending.denyCallbackData, pending);
824
+ };
825
+ const remove = (pending) => {
826
+ pendingByCallback.delete(pending.approveCallbackData);
827
+ pendingByCallback.delete(pending.denyCallbackData);
828
+ };
829
+ const persistRemoval = (pending) => {
830
+ options.pendingStore.save(uniquePending().filter((record) => record !== pending).map(({ decisionToken: _secret, ...record }) => record));
831
+ remove(pending);
832
+ };
833
+ const acceptanceEvidenceUnsigned = (pending, fields) => ({ ...options.acceptanceEventMeta?.(), approvalId: pending.approvalId, ...pending.acceptanceBinding, ...fields });
834
+ const emitAcceptanceEvidence = (event, pending, fields) => {
835
+ if (!pending.acceptanceBinding)
836
+ return;
837
+ if (!options.signAcceptanceEvidence)
838
+ throw new Error("Telegram acceptance evidence signer is unavailable");
839
+ const unsigned = acceptanceEvidenceUnsigned(pending, fields);
840
+ const meta = { ...unsigned, evidenceMac: options.signAcceptanceEvidence(event, unsigned) };
841
+ (0, runtime_1.emitNervesEvent)({ component: "senses", event, message: "Telegram approval acceptance evidence recorded", meta });
842
+ options.onAcceptanceEvidence?.(event, meta);
843
+ };
844
+ const withApprovalExclusive = async (approvalId, wait, operation) => {
845
+ while (approvalOperations.has(approvalId)) {
846
+ if (!wait)
847
+ return undefined;
848
+ await approvalOperations.get(approvalId).done;
849
+ }
850
+ let release;
851
+ const done = new Promise((resolve) => { release = resolve; });
852
+ const claim = { done, release };
853
+ approvalOperations.set(approvalId, claim);
854
+ try {
855
+ return await operation();
856
+ }
857
+ finally {
858
+ approvalOperations.delete(approvalId);
859
+ release();
860
+ }
861
+ };
862
+ const expiryObservationFields = (pending, observedAt) => ({
863
+ expiryObservationSchemaVersion: "telegram-approval-expiry-observation-v1",
864
+ expiryDeadlineAt: pending.expiresAt,
865
+ expiryObservedAt: observedAt,
866
+ });
867
+ const terminalTombstoneFields = (pending) => ({
868
+ ...expiryObservationFields(pending, pending.expiryObservation.observedAt),
869
+ terminalizedAt: pending.terminalizedAt,
870
+ tombstoneExpiresAt: pending.tombstoneExpiresAt,
871
+ });
872
+ const staleTapFields = (pending, staleTap) => ({
873
+ ...terminalTombstoneFields(pending),
874
+ staleTapSchemaVersion: staleTap.schemaVersion,
875
+ staleTapState: staleTap.state,
876
+ queryIdDigest: staleTap.queryIdDigest,
877
+ attemptedAt: staleTap.attemptedAt,
878
+ consumedAt: staleTap.consumedAt,
879
+ });
880
+ const signState = (event, pending, fields) => {
881
+ const evidenceMac = options.signAcceptanceEvidence?.(event, acceptanceEvidenceUnsigned(pending, fields)) ?? null;
882
+ return evidenceMac;
883
+ };
884
+ const decisionAttemptPayload = (pending, attempt) => ({
885
+ approvalId: pending.approvalId,
886
+ messageId: pending.messageId,
887
+ expiresAt: pending.expiresAt,
888
+ approveCallbackData: pending.approveCallbackData,
889
+ denyCallbackData: pending.denyCallbackData,
890
+ acceptanceBinding: pending.acceptanceBinding,
891
+ schemaVersion: attempt.schemaVersion,
892
+ decision: attempt.decision,
893
+ queryIdDigest: attempt.queryIdDigest,
894
+ attemptedAt: attempt.attemptedAt,
895
+ });
896
+ const decisionAttemptMac = (decisionToken, pending, attempt) => (0, node_crypto_1.createHmac)("sha256", decisionToken).update(JSON.stringify(decisionAttemptPayload(pending, attempt))).digest("hex");
897
+ const resolveDecisionToken = async (pending) => {
898
+ const decisionToken = pending.decisionToken ?? await options.resolveDecisionToken?.(pending.approvalId);
899
+ if (!decisionToken)
900
+ throw new Error("Telegram approval restart requires a decision token resolver");
901
+ return decisionToken;
902
+ };
903
+ const validateDecisionAttempt = async (pending, expected) => {
904
+ const attempt = pending.decisionAttempt;
905
+ const lowerBound = pending.acceptanceBinding?.boundAt ?? pending.expiresAt - exports.TELEGRAM_APPROVAL_TTL_MS;
906
+ const unsigned = { schemaVersion: attempt.schemaVersion, decision: attempt.decision, queryIdDigest: attempt.queryIdDigest, attemptedAt: attempt.attemptedAt };
907
+ const token = pending.decisionToken ?? await options.resolveDecisionToken?.(pending.approvalId);
908
+ const expectedMac = token ? decisionAttemptMac(token, pending, unsigned) : null;
909
+ const suppliedMac = typeof attempt.evidenceMac === "string" ? Buffer.from(attempt.evidenceMac, "hex") : Buffer.alloc(0);
910
+ const calculatedMac = expectedMac ? Buffer.from(expectedMac, "hex") : Buffer.alloc(0);
911
+ const tokenMacValid = calculatedMac.length > 0 && suppliedMac.length === calculatedMac.length && (0, node_crypto_1.timingSafeEqual)(suppliedMac, calculatedMac);
912
+ const recoveryMacValid = typeof attempt.recoveryMac === "string"
913
+ && options.signAcceptanceEvidence !== undefined
914
+ && attempt.recoveryMac === options.signAcceptanceEvidence("telegram.approval_decision_attempt_recovery_state", acceptanceEvidenceUnsigned(pending, decisionAttemptPayload(pending, unsigned)));
915
+ if (attempt.schemaVersion !== "telegram-approval-decision-attempt-v1"
916
+ || !["approve", "deny"].includes(attempt.decision)
917
+ || !/^[0-9a-f]{64}$/u.test(attempt.queryIdDigest)
918
+ || !Number.isSafeInteger(attempt.attemptedAt) || attempt.attemptedAt < lowerBound || attempt.attemptedAt >= pending.expiresAt
919
+ || (!tokenMacValid && !recoveryMacValid)
920
+ || (expected !== undefined && (attempt.decision !== expected.decision || attempt.queryIdDigest !== expected.queryIdDigest))) {
921
+ throw new Error("Telegram persisted decision attempt is invalid");
922
+ }
923
+ return attempt;
924
+ };
925
+ const settlementReceiptFields = (receipt) => ({
926
+ settlementReceiptSchemaVersion: receipt.schemaVersion,
927
+ settlementKind: receipt.kind,
928
+ callbackAt: receipt.callbackAt,
929
+ accepted: receipt.accepted,
930
+ reason: receipt.reason,
931
+ acknowledgementState: receipt.acknowledgementState,
932
+ recoveredAt: receipt.recoveredAt ?? null,
933
+ decisionAttemptDigest: receipt.decisionAttemptDigest,
934
+ });
935
+ const terminalOutcomeFields = (pending) => ({
936
+ accepted: pending.terminal?.accepted,
937
+ terminalText: pending.terminal?.terminalText,
938
+ decisionAttemptDigest: (0, node_crypto_1.createHash)("sha256").update(JSON.stringify(pending.decisionAttempt)).digest("hex"),
939
+ });
940
+ const validateTerminalOutcome = (pending) => {
941
+ if (!pending.terminal || typeof pending.terminal.accepted !== "boolean" || typeof pending.terminal.terminalText !== "string"
942
+ || pending.terminal.terminalText.length === 0 || pending.terminal.terminalText.length > 4_096
943
+ || pending.terminalMac !== signState("telegram.approval_terminal_outcome_state", pending, terminalOutcomeFields(pending))) {
944
+ throw new Error("Telegram persisted terminal outcome is invalid");
945
+ }
946
+ };
947
+ const validateSettlementReceipt = (pending) => {
948
+ const receipt = pending.settlementReceipt;
949
+ validateTerminalOutcome(pending);
950
+ const decisionAttemptDigest = (0, node_crypto_1.createHash)("sha256").update(JSON.stringify(pending.decisionAttempt)).digest("hex");
951
+ if (!receipt || !pending.terminal || !pending.decisionAttempt || receipt.schemaVersion !== "telegram-approval-settlement-receipt-v1"
952
+ || !["live", "recovery"].includes(receipt.kind)
953
+ || !Number.isSafeInteger(receipt.callbackAt)
954
+ || typeof receipt.accepted !== "boolean"
955
+ || receipt.reason !== (receipt.accepted ? "accepted" : "decision_refused")
956
+ || (receipt.kind === "live" ? !["acknowledged", "rejected_as_stale"].includes(receipt.acknowledgementState) : receipt.acknowledgementState !== "indeterminate_after_restart")
957
+ || (receipt.kind === "live" ? receipt.recoveredAt !== null : !Number.isSafeInteger(receipt.recoveredAt))
958
+ || receipt.callbackAt !== pending.decisionAttempt.attemptedAt
959
+ || !/^[0-9a-f]{64}$/u.test(receipt.decisionAttemptDigest) || receipt.decisionAttemptDigest !== decisionAttemptDigest
960
+ || receipt.evidenceMac !== signState("telegram.approval_settlement_receipt_state", pending, settlementReceiptFields(receipt))) {
961
+ throw new Error("Telegram persisted settlement receipt is invalid");
962
+ }
963
+ return receipt;
964
+ };
965
+ const emitSettlementReceipt = async (pending) => {
966
+ const receipt = validateSettlementReceipt(pending);
967
+ const fields = receipt.kind === "live"
968
+ ? { callbackAt: receipt.callbackAt, acknowledged: receipt.acknowledgementState === "acknowledged", acknowledgementState: receipt.acknowledgementState, accepted: receipt.accepted, reason: receipt.reason, decisionAttemptDigest: receipt.decisionAttemptDigest }
969
+ : { callbackAt: receipt.callbackAt, acknowledgementState: receipt.acknowledgementState, accepted: receipt.accepted, reason: receipt.reason, recoveredAt: receipt.recoveredAt, decisionAttemptDigest: receipt.decisionAttemptDigest };
970
+ const event = receipt.kind === "live" ? "telegram.callback_settled" : "telegram.callback_recovery_settled";
971
+ if (options.signAcceptanceEvidence) {
972
+ const unsigned = acceptanceEvidenceUnsigned(pending, fields);
973
+ const meta = { ...unsigned, evidenceMac: options.signAcceptanceEvidence(event, unsigned) };
974
+ if (options.commitAcceptanceEvidence)
975
+ await options.commitAcceptanceEvidence(event, meta);
976
+ else
977
+ (0, runtime_1.emitNervesEvent)({ component: "senses", event, message: "Telegram approval acceptance evidence recorded", meta });
978
+ options.onAcceptanceEvidence?.(event, meta);
979
+ }
980
+ else
981
+ (0, runtime_1.emitNervesEvent)({ component: "senses", event, message: "Telegram approval callback settled", meta: { approvalId: pending.approvalId, ...fields, ...options.acceptanceEventMeta?.() } });
982
+ };
983
+ const validateExpiryObservation = (pending) => {
984
+ const observation = pending.expiryObservation;
985
+ const fields = expiryObservationFields(pending, observation.observedAt);
986
+ const expectedMac = pending.acceptanceBinding
987
+ ? options.signAcceptanceEvidence?.("telegram.approval_expiry_observed", acceptanceEvidenceUnsigned(pending, fields)) ?? null
988
+ : null;
989
+ if (observation.schemaVersion !== "telegram-approval-expiry-observation-v1" || observation.deadlineAt !== pending.expiresAt
990
+ || !Number.isSafeInteger(observation.observedAt) || observation.observedAt < observation.deadlineAt
991
+ || observation.evidenceMac !== expectedMac)
992
+ throw new Error("Telegram persisted expiry observation is invalid");
993
+ return observation;
994
+ };
995
+ const ensureExpiryObservation = (pending, observedAt = now()) => {
996
+ let observation = pending.expiryObservation;
997
+ if (!observation) {
998
+ const fields = expiryObservationFields(pending, observedAt);
999
+ const evidenceMac = pending.acceptanceBinding
1000
+ ? options.signAcceptanceEvidence?.("telegram.approval_expiry_observed", acceptanceEvidenceUnsigned(pending, fields)) ?? null
1001
+ : null;
1002
+ if (pending.acceptanceBinding && evidenceMac === null)
1003
+ throw new Error("Telegram acceptance evidence signer is unavailable");
1004
+ observation = { schemaVersion: "telegram-approval-expiry-observation-v1", deadlineAt: pending.expiresAt, observedAt, evidenceMac };
1005
+ persistMutation(pending, () => { pending.expiryObservation = observation; });
1006
+ }
1007
+ observation = validateExpiryObservation(pending);
1008
+ const fields = expiryObservationFields(pending, observation.observedAt);
1009
+ emitAcceptanceEvidence("telegram.approval_expiry_observed", pending, fields);
1010
+ return observation;
1011
+ };
1012
+ const validateStaleTap = (pending) => {
1013
+ if (!pending.staleTap)
1014
+ return;
1015
+ if (pending.staleTap.schemaVersion !== "telegram-approval-stale-tap-v1"
1016
+ || !["attempted", "consumed"].includes(pending.staleTap.state)
1017
+ || !/^[0-9a-f]{64}$/u.test(pending.staleTap.queryIdDigest)
1018
+ || !Number.isSafeInteger(pending.staleTap.attemptedAt)
1019
+ || (pending.staleTap.state === "attempted" && pending.staleTap.consumedAt !== null)
1020
+ || (pending.staleTap.state === "consumed" && (!Number.isSafeInteger(pending.staleTap.consumedAt)
1021
+ || Number(pending.staleTap.consumedAt) < pending.staleTap.attemptedAt))
1022
+ || pending.staleTap.evidenceMac !== signState("telegram.approval_stale_tap_state", pending, staleTapFields(pending, pending.staleTap))) {
1023
+ throw new Error("Telegram persisted stale-tap record is invalid");
1024
+ }
1025
+ };
1026
+ const validateTerminalTombstone = (pending) => {
1027
+ try {
1028
+ if (pending.deliveryState !== "terminal_tombstone" || !pending.acceptanceBinding || pending.messageId === null
1029
+ || pending.decisionToken !== undefined || !Number.isSafeInteger(pending.terminalizedAt)
1030
+ || !Number.isSafeInteger(pending.tombstoneExpiresAt)
1031
+ || pending.tombstoneExpiresAt !== Number(pending.terminalizedAt) + exports.TELEGRAM_APPROVAL_TOMBSTONE_TTL_MS) {
1032
+ throw new Error("shape");
1033
+ }
1034
+ const observation = validateExpiryObservation(pending);
1035
+ if (Number(pending.terminalizedAt) < observation.observedAt
1036
+ || pending.tombstoneMac !== signState("telegram.approval_terminal_tombstone_state", pending, terminalTombstoneFields(pending)))
1037
+ throw new Error("order");
1038
+ validateStaleTap(pending);
1039
+ }
1040
+ catch {
1041
+ throw new Error("Telegram persisted terminal tombstone is invalid");
1042
+ }
1043
+ };
1044
+ for (const loaded of options.pendingStore.load()) {
1045
+ const pending = {
1046
+ ...loaded,
1047
+ deliveryState: loaded.deliveryState ?? "bound",
1048
+ };
1049
+ assertTelegramCallbackData(pending.approveCallbackData);
1050
+ assertTelegramCallbackData(pending.denyCallbackData);
1051
+ if (pendingByCallback.has(pending.approveCallbackData) || pendingByCallback.has(pending.denyCallbackData)) {
1052
+ throw new Error("Telegram persisted approval callback handle collision");
1053
+ }
1054
+ add(pending);
1055
+ }
1056
+ const editTerminal = async (pending, terminalText, expiryObservedAt) => {
1057
+ if (pending.messageId === null)
1058
+ return now();
1059
+ const terminalEditStartedAt = now();
1060
+ try {
1061
+ effectBarrier();
1062
+ await options.effects.edit({ idempotencyKey: `approval:${pending.approvalId}:edit:${(0, node_crypto_1.createHash)("sha256").update(terminalText).digest("hex")}`, chatId: options.expectedChatId, messageId: Number(pending.messageId), text: terminalText });
1063
+ }
1064
+ catch (error) {
1065
+ const alreadyTerminal = error instanceof TelegramApiError && error.status === 400 && /message is not modified/i.test(error.message);
1066
+ if (!alreadyTerminal)
1067
+ throw error;
1068
+ }
1069
+ const terminalizedAt = now();
1070
+ if (pending.acceptanceBinding)
1071
+ emitAcceptanceEvidence("telegram.approval_prompt_terminalized", pending, { ...(expiryObservedAt === undefined ? {} : { expiryDeadlineAt: pending.expiresAt, expiryObservedAt }), terminalEditStartedAt, terminalizedAt, buttonsRemoved: true });
1072
+ else
1073
+ (0, runtime_1.emitNervesEvent)({ component: "senses", event: "telegram.approval_prompt_terminalized", message: "Telegram approval prompt was terminalized", meta: { approvalId: pending.approvalId, buttonsRemoved: true, ...options.acceptanceEventMeta?.() } });
1074
+ return terminalizedAt;
1075
+ };
1076
+ const retainTerminalTombstone = (pending, terminalizedAt) => {
1077
+ pending.deliveryState = "terminal_tombstone";
1078
+ pending.decisionToken = undefined;
1079
+ pending.terminalizedAt = terminalizedAt;
1080
+ pending.tombstoneExpiresAt = terminalizedAt + exports.TELEGRAM_APPROVAL_TOMBSTONE_TTL_MS;
1081
+ pending.tombstoneMac = signState("telegram.approval_terminal_tombstone_state", pending, terminalTombstoneFields(pending)) ?? undefined;
1082
+ };
1083
+ const acknowledge = async (callbackQueryId, invalid) => {
1084
+ try {
1085
+ effectBarrier();
1086
+ await options.effects.acknowledge({
1087
+ idempotencyKey: `approval-callback:${(0, node_crypto_1.createHash)("sha256").update(callbackQueryId).digest("hex")}`,
1088
+ callbackQueryId,
1089
+ ...(invalid ? { text: "This approval is no longer valid.", showAlert: true } : {}),
1090
+ });
1091
+ return "acknowledged";
1092
+ }
1093
+ catch (error) {
1094
+ const stale = error instanceof TelegramApiError
1095
+ && error.status === 400
1096
+ && /query is too old|query ID is invalid/i.test(error.message);
1097
+ if (!stale)
1098
+ throw error;
1099
+ return "rejected_as_stale";
1100
+ }
1101
+ };
1102
+ const settleDecisionAttempt = async (pending, expected) => {
1103
+ if (pending.settlementReceipt) {
1104
+ const receipt = validateSettlementReceipt(pending);
1105
+ await emitSettlementReceipt(pending);
1106
+ await options.onSettlementComplete?.(pending.approvalId);
1107
+ persistRemoval(pending);
1108
+ return { accepted: receipt.accepted, terminalText: pending.terminal.terminalText, callbackAt: receipt.callbackAt };
1109
+ }
1110
+ let decisionToken;
1111
+ if (!pending.decisionAttempt) {
1112
+ const observed = expected;
1113
+ decisionToken = await resolveDecisionToken(pending);
1114
+ const fencingToken = decisionToken;
1115
+ const unsigned = {
1116
+ schemaVersion: "telegram-approval-decision-attempt-v1",
1117
+ decision: observed.decision,
1118
+ queryIdDigest: observed.queryIdDigest,
1119
+ attemptedAt: observed.observedAt,
1120
+ };
1121
+ if (unsigned.attemptedAt < (pending.acceptanceBinding?.boundAt ?? pending.expiresAt - exports.TELEGRAM_APPROVAL_TTL_MS)
1122
+ || unsigned.attemptedAt >= pending.expiresAt)
1123
+ throw new Error("Telegram decision attempt was observed outside its deadline");
1124
+ persistMutation(pending, () => {
1125
+ pending.decisionAttempt = {
1126
+ ...unsigned,
1127
+ evidenceMac: decisionAttemptMac(fencingToken, pending, unsigned),
1128
+ recoveryMac: signState("telegram.approval_decision_attempt_recovery_state", pending, decisionAttemptPayload(pending, unsigned)),
1129
+ };
1130
+ });
1131
+ }
1132
+ const attempt = await validateDecisionAttempt(pending, expected);
1133
+ let outcome = pending.terminal;
1134
+ if (outcome)
1135
+ validateTerminalOutcome(pending);
1136
+ else {
1137
+ decisionToken ??= await resolveDecisionToken(pending);
1138
+ outcome = await options.onDecision({
1139
+ approvalId: pending.approvalId,
1140
+ decisionToken,
1141
+ decision: attempt.decision,
1142
+ decisionAt: attempt.attemptedAt,
1143
+ requesterId: options.expectedUserId,
1144
+ transport: "telegram",
1145
+ transportChatId: options.expectedChatId,
1146
+ transportMessageId: pending.messageId,
1147
+ ...(pending.acceptanceBinding ? { acceptanceBinding: pending.acceptanceBinding } : {}),
1148
+ });
1149
+ persistMutation(pending, () => {
1150
+ pending.terminal = outcome;
1151
+ pending.terminalMac = signState("telegram.approval_terminal_outcome_state", pending, terminalOutcomeFields(pending));
1152
+ });
1153
+ }
1154
+ const acknowledgementState = expected ? await acknowledge(expected.callbackQueryId, false) : "indeterminate_after_restart";
1155
+ await editTerminal(pending, outcome.terminalText);
1156
+ const reason = outcome.accepted ? "accepted" : "decision_refused";
1157
+ persistMutation(pending, () => {
1158
+ const receipt = {
1159
+ schemaVersion: "telegram-approval-settlement-receipt-v1",
1160
+ kind: expected ? "live" : "recovery",
1161
+ callbackAt: attempt.attemptedAt,
1162
+ accepted: outcome.accepted,
1163
+ reason,
1164
+ acknowledgementState,
1165
+ recoveredAt: expected ? null : now(),
1166
+ decisionAttemptDigest: (0, node_crypto_1.createHash)("sha256").update(JSON.stringify(attempt)).digest("hex"),
1167
+ evidenceMac: null,
1168
+ };
1169
+ receipt.evidenceMac = signState("telegram.approval_settlement_receipt_state", pending, settlementReceiptFields(receipt));
1170
+ pending.settlementReceipt = receipt;
1171
+ });
1172
+ await emitSettlementReceipt(pending);
1173
+ await options.onSettlementComplete?.(pending.approvalId);
1174
+ persistRemoval(pending);
1175
+ return { ...outcome, callbackAt: attempt.attemptedAt };
1176
+ };
1177
+ const reconcileExpired = async () => {
1178
+ let firstFailure;
1179
+ for (const pending of uniquePending()) {
1180
+ try {
1181
+ const persistedState = classifyTelegramPersistedApprovalState(pending);
1182
+ if (persistedState === "ordinary" && pending.deliveryState !== "terminal_tombstone" && now() < pending.expiresAt)
1183
+ continue;
1184
+ await withApprovalExclusive(pending.approvalId, false, async () => {
1185
+ const current = uniquePending().find((candidate) => candidate.approvalId === pending.approvalId);
1186
+ if (!current)
1187
+ return;
1188
+ if (current.deliveryState === "terminal_tombstone") {
1189
+ validateTerminalTombstone(current);
1190
+ if (now() >= current.tombstoneExpiresAt) {
1191
+ remove(current);
1192
+ persist();
1193
+ }
1194
+ return;
1195
+ }
1196
+ const persistedState = classifyTelegramPersistedApprovalState(current);
1197
+ if (persistedState === "decision_attempt" || persistedState === "action_terminal") {
1198
+ await settleDecisionAttempt(current);
1199
+ return;
1200
+ }
1201
+ if (persistedState === "ordinary" && !current.terminal && now() < current.expiresAt)
1202
+ return;
1203
+ if (current.terminal) {
1204
+ await editTerminal(current, current.terminal.terminalText);
1205
+ remove(current);
1206
+ persist();
1207
+ return;
1208
+ }
1209
+ const observation = ensureExpiryObservation(current);
1210
+ await options.onExpire?.(current.approvalId);
1211
+ const terminalizedAt = await editTerminal(current, "⚠️ Approval expired", observation.observedAt);
1212
+ if (current.acceptanceBinding)
1213
+ persistMutation(current, () => retainTerminalTombstone(current, terminalizedAt));
1214
+ else {
1215
+ remove(current);
1216
+ persist();
1217
+ }
1218
+ });
1219
+ }
1220
+ catch (error) {
1221
+ firstFailure ??= error;
1222
+ }
1223
+ }
1224
+ if (firstFailure !== undefined)
1225
+ throw firstFailure;
1226
+ };
1227
+ return {
1228
+ async sendApproval(input) {
1229
+ effectBarrier();
1230
+ const approveCallbackData = `a:${options.createOpaqueHandle()}`;
1231
+ const denyCallbackData = `d:${options.createOpaqueHandle()}`;
1232
+ assertTelegramCallbackData(approveCallbackData);
1233
+ assertTelegramCallbackData(denyCallbackData);
1234
+ if (pendingByCallback.has(approveCallbackData) || pendingByCallback.has(denyCallbackData)) {
1235
+ throw new Error("Telegram approval callback handle collision");
1236
+ }
1237
+ const pending = {
1238
+ approvalId: input.approvalId,
1239
+ decisionToken: input.decisionToken,
1240
+ messageId: null,
1241
+ deliveryState: "pending",
1242
+ approveCallbackData,
1243
+ denyCallbackData,
1244
+ expiresAt: now() + exports.TELEGRAM_APPROVAL_TTL_MS,
1245
+ prompt: input.prompt,
1246
+ };
1247
+ add(pending);
1248
+ try {
1249
+ persist();
1250
+ }
1251
+ catch (error) {
1252
+ remove(pending);
1253
+ throw error;
1254
+ }
1255
+ pending.deliveryState = "send_attempting";
1256
+ try {
1257
+ persist();
1258
+ }
1259
+ catch (error) {
1260
+ pending.deliveryState = "pending";
1261
+ remove(pending);
1262
+ throw error;
1263
+ }
1264
+ try {
1265
+ effectBarrier();
1266
+ const messageId = await options.effects.sendCard({
1267
+ idempotencyKey: `approval:${input.approvalId}:prompt`,
1268
+ chatId: options.expectedChatId,
1269
+ text: input.prompt,
1270
+ buttons: [[{ text: "Approve", callbackData: approveCallbackData }, { text: "Deny", callbackData: denyCallbackData }]],
1271
+ });
1272
+ if (!Number.isSafeInteger(messageId) || messageId <= 0)
1273
+ throw new Error("Telegram sendMessage response did not include a canonical message_id");
1274
+ pending.messageId = String(messageId);
1275
+ pending.deliveryState = "bound";
1276
+ if (input.acceptanceBinding) {
1277
+ const boundAt = now();
1278
+ pending.acceptanceBinding = {
1279
+ ...input.acceptanceBinding,
1280
+ messageIdDigest: options.acceptanceMessageIdDigest?.(pending.messageId)
1281
+ ?? (0, node_crypto_1.createHash)("sha256").update(pending.messageId, "utf8").digest("hex"),
1282
+ boundAt,
1283
+ };
1284
+ pending.expiresAt = boundAt + exports.TELEGRAM_APPROVAL_TTL_MS;
1285
+ }
1286
+ persist();
1287
+ emitAcceptanceEvidence("senses.telegram_approval_prompt_bound", pending, { boundAt: pending.acceptanceBinding?.boundAt });
1288
+ }
1289
+ catch (error) {
1290
+ pending.messageId = null;
1291
+ pending.deliveryState = "delivery_indeterminate";
1292
+ persist();
1293
+ throw error;
1294
+ }
1295
+ return { messageId: pending.messageId, approveCallbackData, denyCallbackData, expiresAt: pending.expiresAt };
1296
+ },
1297
+ async handleUpdate(update) {
1298
+ effectBarrier();
1299
+ const callback = update.callback_query;
1300
+ if (!callback)
1301
+ return { handled: false, accepted: false, reason: "not_callback" };
1302
+ const observedAt = now();
1303
+ const initiallyPending = pendingByCallback.get(callback.data ?? "");
1304
+ if (initiallyPending) {
1305
+ const result = await withApprovalExclusive(initiallyPending.approvalId, true, async () => handleUpdateUnlocked(update, observedAt));
1306
+ return result;
1307
+ }
1308
+ return handleUpdateUnlocked(update, observedAt);
1309
+ },
1310
+ reconcileExpired,
1311
+ async recoverDecisionAttempt(approvalId) {
1312
+ const result = await withApprovalExclusive(approvalId, true, async () => {
1313
+ const pending = uniquePending().find((record) => record.approvalId === approvalId);
1314
+ if (!pending)
1315
+ return false;
1316
+ const persistedState = classifyTelegramPersistedApprovalState(pending);
1317
+ if (persistedState !== "decision_attempt" && persistedState !== "action_terminal")
1318
+ return false;
1319
+ await settleDecisionAttempt(pending);
1320
+ return true;
1321
+ });
1322
+ return result;
1323
+ },
1324
+ async terminalizeOrphaned(approvalId, terminalText) {
1325
+ const result = await withApprovalExclusive(approvalId, true, async () => {
1326
+ const pending = uniquePending().find((record) => record.approvalId === approvalId);
1327
+ if (!pending)
1328
+ return { terminalEditSucceeded: true };
1329
+ const persistedState = classifyTelegramPersistedApprovalState(pending);
1330
+ if (persistedState === "expiry_observed")
1331
+ validateExpiryObservation(pending);
1332
+ if (persistedState === "decision_attempt" || persistedState === "action_terminal") {
1333
+ throw new Error("Telegram persisted approval authority cannot be orphan-cleaned");
1334
+ }
1335
+ let terminalEditSucceeded = true;
1336
+ try {
1337
+ await editTerminal(pending, terminalText);
1338
+ }
1339
+ catch {
1340
+ terminalEditSucceeded = false;
1341
+ }
1342
+ remove(pending);
1343
+ persist();
1344
+ return { terminalEditSucceeded };
1345
+ });
1346
+ return result;
1347
+ },
1348
+ async terminalizeRecovered(approvalId, terminalText) {
1349
+ await withApprovalExclusive(approvalId, true, async () => {
1350
+ const pending = uniquePending().find((record) => record.approvalId === approvalId);
1351
+ if (!pending)
1352
+ return;
1353
+ const persistedState = classifyTelegramPersistedApprovalState(pending);
1354
+ if (persistedState === "expiry_observed")
1355
+ validateExpiryObservation(pending);
1356
+ if (persistedState === "decision_attempt" || persistedState === "action_terminal") {
1357
+ await settleDecisionAttempt(pending);
1358
+ return;
1359
+ }
1360
+ if (pending.messageId === null && (pending.deliveryState === "send_attempting" || pending.deliveryState === "delivery_indeterminate")) {
1361
+ pending.deliveryState = "delivery_indeterminate";
1362
+ pending.terminal = { accepted: false, terminalText };
1363
+ pending.terminalKind = "delivery_interruption";
1364
+ persist();
1365
+ return;
1366
+ }
1367
+ await editTerminal(pending, terminalText);
1368
+ remove(pending);
1369
+ persist();
1370
+ });
1371
+ },
1372
+ listPendingDeliveries() {
1373
+ return uniquePending().filter((record) => record.deliveryState !== "terminal_tombstone")
1374
+ .map(({ decisionToken: _secret, ...record }) => structuredClone(record));
1375
+ },
1376
+ };
1377
+ async function handleUpdateUnlocked(update, observedAt) {
1378
+ const callback = update.callback_query;
1379
+ const pending = pendingByCallback.get(callback.data ?? "");
1380
+ const userId = String(callback.from.id);
1381
+ const chatId = callback.message ? String(callback.message.chat.id) : "";
1382
+ const messageId = callback.message ? String(callback.message.message_id) : "";
1383
+ const persistedState = pending ? classifyTelegramPersistedApprovalState(pending) : undefined;
1384
+ let invalidReason = null;
1385
+ let expiryObservedAt;
1386
+ let expiryObservation;
1387
+ if (pending && persistedState === "expiry_observed") {
1388
+ expiryObservation = validateExpiryObservation(pending);
1389
+ expiryObservedAt = expiryObservation.observedAt;
1390
+ }
1391
+ if (!pending)
1392
+ invalidReason = "stale_callback";
1393
+ else if (userId !== options.expectedUserId)
1394
+ invalidReason = "foreign_user";
1395
+ else if (chatId !== options.expectedChatId)
1396
+ invalidReason = "foreign_chat";
1397
+ else if (pending.deliveryState === "terminal_tombstone") {
1398
+ if (pending.messageId === null || messageId !== pending.messageId)
1399
+ invalidReason = "foreign_message";
1400
+ else
1401
+ invalidReason = "stale_callback";
1402
+ }
1403
+ else if (pending.deliveryState === "send_attempting" || pending.deliveryState === "delivery_indeterminate")
1404
+ invalidReason = "delivery_indeterminate";
1405
+ else if (pending.deliveryState !== "bound" || pending.messageId === null)
1406
+ invalidReason = "prompt_not_bound";
1407
+ else if (messageId !== pending.messageId)
1408
+ invalidReason = "foreign_message";
1409
+ else if (persistedState === "expiry_observed")
1410
+ invalidReason = "expired";
1411
+ else {
1412
+ if (!pending.decisionAttempt && observedAt >= pending.expiresAt) {
1413
+ invalidReason = "expired";
1414
+ expiryObservedAt = observedAt;
1415
+ }
1416
+ }
1417
+ if (invalidReason) {
1418
+ if (pending?.deliveryState === "terminal_tombstone" && invalidReason === "stale_callback") {
1419
+ validateTerminalTombstone(pending);
1420
+ const queryIdDigest = (0, node_crypto_1.createHash)("sha256").update(callback.id).digest("hex");
1421
+ if (pending.staleTap) {
1422
+ validateStaleTap(pending);
1423
+ if (pending.staleTap.state === "attempted") {
1424
+ if (pending.staleTap.queryIdDigest !== queryIdDigest)
1425
+ throw new Error("Telegram stale-tap recovery query mismatch");
1426
+ const staleAt = pending.staleTap.attemptedAt;
1427
+ persistMutation(pending, () => {
1428
+ const consumed = { ...pending.staleTap, state: "consumed", consumedAt: now(), evidenceMac: null };
1429
+ consumed.evidenceMac = signState("telegram.approval_stale_tap_state", pending, staleTapFields(pending, consumed));
1430
+ pending.staleTap = consumed;
1431
+ });
1432
+ await acknowledge(callback.id, true);
1433
+ emitAcceptanceEvidence("telegram.approval_stale_callback_settled", pending, { staleAt, acknowledged: true, accepted: false, reason: "stale_callback" });
1434
+ }
1435
+ else {
1436
+ await acknowledge(callback.id, true);
1437
+ }
1438
+ return { handled: true, accepted: false, reason: invalidReason };
1439
+ }
1440
+ const staleAt = now();
1441
+ persistMutation(pending, () => {
1442
+ const attempted = { schemaVersion: "telegram-approval-stale-tap-v1", state: "attempted", queryIdDigest, attemptedAt: staleAt, consumedAt: null, evidenceMac: null };
1443
+ attempted.evidenceMac = signState("telegram.approval_stale_tap_state", pending, staleTapFields(pending, attempted));
1444
+ pending.staleTap = attempted;
1445
+ });
1446
+ persistMutation(pending, () => {
1447
+ const consumed = { ...pending.staleTap, state: "consumed", consumedAt: now(), evidenceMac: null };
1448
+ consumed.evidenceMac = signState("telegram.approval_stale_tap_state", pending, staleTapFields(pending, consumed));
1449
+ pending.staleTap = consumed;
1450
+ });
1451
+ await acknowledge(callback.id, true);
1452
+ emitAcceptanceEvidence("telegram.approval_stale_callback_settled", pending, { staleAt, acknowledged: true, accepted: false, reason: "stale_callback" });
1453
+ return { handled: true, accepted: false, reason: invalidReason };
1454
+ }
1455
+ if (pending && invalidReason === "expired") {
1456
+ expiryObservation = ensureExpiryObservation(pending, expiryObservedAt);
1457
+ expiryObservedAt = expiryObservation.observedAt;
1458
+ }
1459
+ await acknowledge(callback.id, true);
1460
+ if (pending && invalidReason === "delivery_indeterminate" && callback.message) {
1461
+ pending.messageId = messageId;
1462
+ await editTerminal(pending, pending.terminal?.terminalText ?? "⚠️ Approval prompt delivery was interrupted — no action was taken");
1463
+ remove(pending);
1464
+ persist();
1465
+ }
1466
+ if (pending && invalidReason === "expired") {
1467
+ await options.onExpire?.(pending.approvalId);
1468
+ const terminalizedAt = await editTerminal(pending, "⚠️ Approval expired", expiryObservation.observedAt);
1469
+ if (pending.acceptanceBinding)
1470
+ persistMutation(pending, () => retainTerminalTombstone(pending, terminalizedAt));
1471
+ else {
1472
+ remove(pending);
1473
+ persist();
1474
+ }
1475
+ }
1476
+ if (pending?.acceptanceBinding && invalidReason === "expired")
1477
+ emitAcceptanceEvidence("telegram.callback_settled", pending, { callbackAt: expiryObservedAt, acknowledged: true, accepted: false, reason: invalidReason });
1478
+ else
1479
+ (0, runtime_1.emitNervesEvent)({ component: "senses", event: "telegram.callback_settled", message: "Telegram approval callback settled", meta: { approvalId: pending?.approvalId ?? null, reason: invalidReason, accepted: false, ...options.acceptanceEventMeta?.() } });
1480
+ return { handled: true, accepted: false, reason: invalidReason };
1481
+ }
1482
+ const decision = callback.data === pending.approveCallbackData ? "approve" : "deny";
1483
+ const queryIdDigest = (0, node_crypto_1.createHash)("sha256").update(callback.id).digest("hex");
1484
+ const outcome = await settleDecisionAttempt(pending, { decision, queryIdDigest, callbackQueryId: callback.id, observedAt });
1485
+ const reason = outcome.accepted ? "accepted" : "decision_refused";
1486
+ return { handled: true, accepted: outcome.accepted, reason };
1487
+ }
1488
+ }
1489
+ function escapeTelegramHtml(value) {
1490
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
1491
+ }
1492
+ function sourceIndexForRenderedLimit(value, limit) {
1493
+ let renderedUnits = 0;
1494
+ let sourceIndex = 0;
1495
+ for (const character of value) {
1496
+ const escaped = escapeTelegramHtml(character);
1497
+ if (renderedUnits + escaped.length > limit)
1498
+ break;
1499
+ renderedUnits += escaped.length;
1500
+ sourceIndex += character.length;
1501
+ }
1502
+ return sourceIndex;
1503
+ }
1504
+ function preferredBoundary(value, limit, minimum) {
1505
+ const paragraph = value.lastIndexOf("\n\n", limit - 1);
1506
+ if (paragraph >= minimum)
1507
+ return paragraph + 2;
1508
+ const newline = value.lastIndexOf("\n", limit - 1);
1509
+ if (newline >= minimum)
1510
+ return newline + 1;
1511
+ for (let index = limit - 1; index >= minimum; index -= 1) {
1512
+ if (/\s/u.test(value[index]))
1513
+ return index + 1;
1514
+ }
1515
+ return limit;
1516
+ }
1517
+ function splitTelegramText(value, options = {}) {
1518
+ const targetUnits = options.targetUnits ?? 1_200;
1519
+ const maxUnits = options.maxUnits ?? 4_000;
1520
+ if (!Number.isInteger(targetUnits) || !Number.isInteger(maxUnits) || targetUnits < 1 || maxUnits < targetUnits) {
1521
+ throw new Error("Telegram chunk limits must be positive integers with targetUnits <= maxUnits");
1522
+ }
1523
+ if (!value)
1524
+ return [""];
1525
+ const chunks = [];
1526
+ let remaining = value;
1527
+ while (escapeTelegramHtml(remaining).length > maxUnits || escapeTelegramHtml(remaining).length > targetUnits) {
1528
+ const targetIndex = sourceIndexForRenderedLimit(remaining, targetUnits);
1529
+ const maxIndex = sourceIndexForRenderedLimit(remaining, maxUnits);
1530
+ const candidate = targetIndex > 0 ? targetIndex : maxIndex;
1531
+ if (candidate <= 0)
1532
+ throw new Error("Telegram chunk limit cannot fit one character");
1533
+ const splitAt = preferredBoundary(remaining, candidate, Math.floor(candidate / 2));
1534
+ chunks.push(remaining.slice(0, splitAt));
1535
+ remaining = remaining.slice(splitAt);
1536
+ }
1537
+ chunks.push(remaining);
1538
+ return chunks;
1539
+ }
1540
+ async function sendTelegramText(api, chatId, text, options = {}) {
1541
+ const renderHtml = options.renderHtml ?? escapeTelegramHtml;
1542
+ const results = [];
1543
+ const recordMessageId = (value, chunk) => {
1544
+ const messageId = value && typeof value === "object" && !Array.isArray(value)
1545
+ ? value.message_id
1546
+ : undefined;
1547
+ if (!Number.isSafeInteger(messageId) || messageId < 1)
1548
+ throw new TelegramApiError("Telegram sendMessage result omitted a canonical message_id");
1549
+ results.push(messageId);
1550
+ options.onMessageDelivered?.(messageId, chunk);
1551
+ };
1552
+ for (const chunk of splitTelegramText(text)) {
1553
+ try {
1554
+ recordMessageId(await api.request("sendMessage", {
1555
+ chat_id: chatId,
1556
+ text: renderHtml(chunk),
1557
+ parse_mode: "HTML",
1558
+ }, options.signal), chunk);
1559
+ }
1560
+ catch (error) {
1561
+ if (!(error instanceof TelegramApiError) || error.status !== 400)
1562
+ throw error;
1563
+ recordMessageId(await api.request("sendMessage", { chat_id: chatId, text: chunk }, options.signal), chunk);
1564
+ }
1565
+ }
1566
+ return results;
1567
+ }
1568
+ function safeErrorMessage(message, token) {
1569
+ return message.split(token).join("[redacted]");
1570
+ }
1571
+ function parseEnvelope(raw, status, token) {
1572
+ let value;
1573
+ try {
1574
+ value = JSON.parse(raw);
1575
+ }
1576
+ catch (cause) {
1577
+ throw new TelegramApiError(`Telegram returned invalid JSON (HTTP ${status})`, { status, cause });
1578
+ }
1579
+ if (!value || typeof value !== "object" || Array.isArray(value) || typeof value.ok !== "boolean") {
1580
+ throw new TelegramApiError("Telegram returned an invalid response envelope", { status });
1581
+ }
1582
+ const envelope = value;
1583
+ if (!envelope.ok) {
1584
+ throw new TelegramApiError(safeErrorMessage(envelope.description ?? "Telegram request failed", token), {
1585
+ status,
1586
+ errorCode: typeof envelope.error_code === "number" ? envelope.error_code : null,
1587
+ retryAfterSeconds: typeof envelope.parameters?.retry_after === "number" ? envelope.parameters.retry_after : null,
1588
+ });
1589
+ }
1590
+ if (!("result" in envelope)) {
1591
+ throw new TelegramApiError("Telegram success response omitted result", { status });
1592
+ }
1593
+ return envelope;
1594
+ }
1595
+ function createTelegramBotApi(options) {
1596
+ const fetchImpl = options.fetch ?? globalThis.fetch;
1597
+ const apiRoot = options.apiRoot ?? "https://api.telegram.org";
1598
+ const baseUrl = `${apiRoot.replace(/\/$/, "")}/bot${options.token}`;
1599
+ const shutdown = new AbortController();
1600
+ const sleep = options.sleep ?? ((milliseconds, signal) => new Promise((resolve, reject) => {
1601
+ const timer = setTimeout(resolve, milliseconds);
1602
+ signal.addEventListener("abort", () => {
1603
+ clearTimeout(timer);
1604
+ reject(signal.reason);
1605
+ }, { once: true });
1606
+ }));
1607
+ return {
1608
+ async request(method, body, signal) {
1609
+ (0, runtime_1.emitNervesEvent)({
1610
+ component: "senses",
1611
+ event: "telegram.request_start",
1612
+ message: "Telegram Bot API request started",
1613
+ meta: { method },
1614
+ });
1615
+ try {
1616
+ const requestSignal = signal
1617
+ ? AbortSignal.any([shutdown.signal, signal])
1618
+ : shutdown.signal;
1619
+ let retries = 0;
1620
+ for (;;) {
1621
+ requestSignal.throwIfAborted();
1622
+ try {
1623
+ const response = await fetchImpl(`${baseUrl}/${method}`, {
1624
+ method: "POST",
1625
+ headers: { "content-type": "application/json" },
1626
+ body: JSON.stringify(body),
1627
+ signal: requestSignal,
1628
+ });
1629
+ const envelope = parseEnvelope(await response.text(), response.status, options.token);
1630
+ if (!response.ok) {
1631
+ throw new TelegramApiError(`Telegram request failed (HTTP ${response.status})`, { status: response.status });
1632
+ }
1633
+ (0, runtime_1.emitNervesEvent)({
1634
+ component: "senses",
1635
+ event: "telegram.request_end",
1636
+ message: "Telegram Bot API request completed",
1637
+ meta: { method, status: response.status },
1638
+ });
1639
+ return envelope.result;
1640
+ }
1641
+ catch (caught) {
1642
+ const retryAfter = caught instanceof TelegramApiError ? caught.retryAfterSeconds : null;
1643
+ const canRetry = caught instanceof TelegramApiError
1644
+ && caught.status === 429
1645
+ && Number.isInteger(retryAfter)
1646
+ && retries < 3;
1647
+ if (!canRetry)
1648
+ throw caught;
1649
+ retries += 1;
1650
+ const boundedSeconds = Math.min(Math.max(retryAfter, 1), 30);
1651
+ await sleep(boundedSeconds * 1_000, requestSignal);
1652
+ }
1653
+ }
1654
+ }
1655
+ catch (caught) {
1656
+ const error = caught instanceof TelegramApiError
1657
+ ? new TelegramApiError(safeErrorMessage(caught.message, options.token), {
1658
+ status: caught.status,
1659
+ errorCode: caught.errorCode,
1660
+ retryAfterSeconds: caught.retryAfterSeconds,
1661
+ })
1662
+ : new TelegramApiError(safeErrorMessage(caught instanceof Error ? caught.message : String(caught), options.token));
1663
+ (0, runtime_1.emitNervesEvent)({
1664
+ level: "error",
1665
+ component: "senses",
1666
+ event: "telegram.request_error",
1667
+ message: "Telegram Bot API request failed",
1668
+ meta: { method, status: error.status, errorCode: error.errorCode },
1669
+ });
1670
+ throw error;
1671
+ }
1672
+ },
1673
+ stop() {
1674
+ shutdown.abort(new Error("Telegram Bot API client stopped"));
1675
+ },
1676
+ };
1677
+ }