@ouro.bot/cli 0.1.0-alpha.83 → 0.1.0-alpha.831

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