approval-md 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (416) hide show
  1. package/README.md +629 -559
  2. package/SPEC.md +99 -24
  3. package/dist/src/adapters/agentmail.d.ts +426 -0
  4. package/dist/src/adapters/agentmail.js +2 -2
  5. package/dist/src/adapters/conformance.d.ts +149 -0
  6. package/dist/src/adapters/contract.d.ts +628 -0
  7. package/dist/src/adapters/contract.js +110 -16
  8. package/dist/src/adapters/contract.js.map +1 -1
  9. package/dist/src/adapters/email.d.ts +324 -0
  10. package/dist/src/adapters/env-passphrase.d.ts +93 -0
  11. package/dist/src/adapters/public.d.ts +11 -0
  12. package/dist/src/adapters/public.js +11 -0
  13. package/dist/src/adapters/public.js.map +1 -0
  14. package/dist/src/adapters/registry.d.ts +59 -0
  15. package/dist/src/adapters/registry.js +2 -1
  16. package/dist/src/adapters/registry.js.map +1 -1
  17. package/dist/src/adapters/smtp.d.ts +213 -0
  18. package/dist/src/adapters/vault-provider.d.ts +114 -0
  19. package/dist/src/adapters/vault-provider.js +3 -3
  20. package/dist/src/adapters/zzz.d.ts +66 -0
  21. package/dist/src/adapters/zzz.js +299 -0
  22. package/dist/src/adapters/zzz.js.map +1 -0
  23. package/dist/src/channels/batch.d.ts +109 -0
  24. package/dist/src/channels/cli.d.ts +193 -0
  25. package/dist/src/channels/conformance.d.ts +92 -0
  26. package/dist/src/channels/contract.d.ts +656 -0
  27. package/dist/src/channels/contract.js +200 -7
  28. package/dist/src/channels/contract.js.map +1 -1
  29. package/dist/src/channels/payload-view.d.ts +35 -0
  30. package/dist/src/channels/render-queue.d.ts +149 -0
  31. package/dist/src/channels/tagging.d.ts +196 -0
  32. package/dist/src/channels/telegram.d.ts +1944 -0
  33. package/dist/src/channels/telegram.js +218 -23
  34. package/dist/src/channels/telegram.js.map +1 -1
  35. package/dist/src/channels/web.d.ts +350 -0
  36. package/dist/src/channels/web.js +17 -0
  37. package/dist/src/channels/web.js.map +1 -1
  38. package/dist/src/cli/adapter.d.ts +90 -0
  39. package/dist/src/cli/adapter.js +25 -15
  40. package/dist/src/cli/adapter.js.map +1 -1
  41. package/dist/src/cli/amend.d.ts +59 -0
  42. package/dist/src/cli/amend.js +214 -30
  43. package/dist/src/cli/amend.js.map +1 -1
  44. package/dist/src/cli/args.d.ts +43 -0
  45. package/dist/src/cli/attest.d.ts +50 -0
  46. package/dist/src/cli/attest.js +134 -7
  47. package/dist/src/cli/attest.js.map +1 -1
  48. package/dist/src/cli/audit-card.d.ts +62 -0
  49. package/dist/src/cli/audit.d.ts +59 -0
  50. package/dist/src/cli/channel-telegram.d.ts +879 -0
  51. package/dist/src/cli/channel-telegram.js +311 -13
  52. package/dist/src/cli/channel-telegram.js.map +1 -1
  53. package/dist/src/cli/channel-web.d.ts +131 -0
  54. package/dist/src/cli/channel.d.ts +80 -0
  55. package/dist/src/cli/channel.js +9 -0
  56. package/dist/src/cli/channel.js.map +1 -1
  57. package/dist/src/cli/checkpoint-tap.d.ts +169 -0
  58. package/dist/src/cli/codex-bridge.d.ts +819 -0
  59. package/dist/src/cli/codex-bridge.js +1607 -0
  60. package/dist/src/cli/codex-bridge.js.map +1 -0
  61. package/dist/src/cli/codex.d.ts +2 -0
  62. package/dist/src/cli/codex.js +469 -0
  63. package/dist/src/cli/codex.js.map +1 -0
  64. package/dist/src/cli/coverage.d.ts +61 -0
  65. package/dist/src/cli/daemon.d.ts +120 -0
  66. package/dist/src/cli/daemon.js +4 -1
  67. package/dist/src/cli/daemon.js.map +1 -1
  68. package/dist/src/cli/doctor.d.ts +129 -0
  69. package/dist/src/cli/doctor.js +586 -17
  70. package/dist/src/cli/doctor.js.map +1 -1
  71. package/dist/src/cli/env.d.ts +65 -0
  72. package/dist/src/cli/execute.d.ts +202 -0
  73. package/dist/src/cli/execute.js +25 -2
  74. package/dist/src/cli/execute.js.map +1 -1
  75. package/dist/src/cli/exit-codes.d.ts +73 -0
  76. package/dist/src/cli/feedback.d.ts +60 -0
  77. package/dist/src/cli/gate-window.d.ts +40 -0
  78. package/dist/src/cli/gate.d.ts +68 -0
  79. package/dist/src/cli/git-scope.d.ts +190 -0
  80. package/dist/src/cli/gloss-attach.d.ts +85 -0
  81. package/dist/src/cli/gloss-codex-child.d.ts +9 -0
  82. package/dist/src/cli/gloss-codex.d.ts +24 -0
  83. package/dist/src/cli/gloss-options.d.ts +42 -0
  84. package/dist/src/cli/gloss.d.ts +265 -0
  85. package/dist/src/cli/help.d.ts +107 -0
  86. package/dist/src/cli/help.js +320 -93
  87. package/dist/src/cli/help.js.map +1 -1
  88. package/dist/src/cli/hook-codex.d.ts +126 -0
  89. package/dist/src/cli/hook-codex.js +226 -0
  90. package/dist/src/cli/hook-codex.js.map +1 -0
  91. package/dist/src/cli/hook.d.ts +787 -0
  92. package/dist/src/cli/hook.js +1235 -181
  93. package/dist/src/cli/hook.js.map +1 -1
  94. package/dist/src/cli/import.d.ts +35 -0
  95. package/dist/src/cli/import.js +1 -1
  96. package/dist/src/cli/import.js.map +1 -1
  97. package/dist/src/cli/init.d.ts +84 -0
  98. package/dist/src/cli/init.js +2 -2
  99. package/dist/src/cli/init.js.map +1 -1
  100. package/dist/src/cli/instructions.d.ts +23 -0
  101. package/dist/src/cli/journal.d.ts +41 -0
  102. package/dist/src/cli/log-advance.d.ts +287 -0
  103. package/dist/src/cli/log-advance.js +102 -11
  104. package/dist/src/cli/log-advance.js.map +1 -1
  105. package/dist/src/cli/log-anchor.d.ts +176 -0
  106. package/dist/src/cli/log-checkpoint.d.ts +22 -0
  107. package/dist/src/cli/log-sync.d.ts +243 -0
  108. package/dist/src/cli/log-verbs.d.ts +16 -0
  109. package/dist/src/cli/log-verbs.js +7 -1
  110. package/dist/src/cli/log-verbs.js.map +1 -1
  111. package/dist/src/cli/long-help.d.ts +70 -0
  112. package/dist/src/cli/main.d.ts +77 -0
  113. package/dist/src/cli/main.js +159 -7
  114. package/dist/src/cli/main.js.map +1 -1
  115. package/dist/src/cli/mcp.d.ts +52 -0
  116. package/dist/src/cli/paths.d.ts +56 -0
  117. package/dist/src/cli/payload.d.ts +58 -0
  118. package/dist/src/cli/policy-apply.d.ts +195 -0
  119. package/dist/src/cli/policy-apply.js +573 -0
  120. package/dist/src/cli/policy-apply.js.map +1 -0
  121. package/dist/src/cli/policy.d.ts +43 -0
  122. package/dist/src/cli/policy.js +14 -1
  123. package/dist/src/cli/policy.js.map +1 -1
  124. package/dist/src/cli/preflight.d.ts +501 -0
  125. package/dist/src/cli/preflight.js +689 -45
  126. package/dist/src/cli/preflight.js.map +1 -1
  127. package/dist/src/cli/progress.d.ts +78 -0
  128. package/dist/src/cli/prompt.d.ts +209 -0
  129. package/dist/src/cli/quickstart.d.ts +46 -0
  130. package/dist/src/cli/quickstart.js +297 -0
  131. package/dist/src/cli/quickstart.js.map +1 -0
  132. package/dist/src/cli/records.d.ts +34 -0
  133. package/dist/src/cli/render.d.ts +22 -0
  134. package/dist/src/cli/sandbox.d.ts +51 -0
  135. package/dist/src/cli/sandbox.js +17 -1
  136. package/dist/src/cli/sandbox.js.map +1 -1
  137. package/dist/src/cli/scaffold.d.ts +79 -0
  138. package/dist/src/cli/scaffold.js +1 -1
  139. package/dist/src/cli/setup-adapter.d.ts +137 -0
  140. package/dist/src/cli/setup-adapter.js +38 -4
  141. package/dist/src/cli/setup-adapter.js.map +1 -1
  142. package/dist/src/cli/setup-channel.d.ts +126 -0
  143. package/dist/src/cli/setup-channel.js +28 -1
  144. package/dist/src/cli/setup-channel.js.map +1 -1
  145. package/dist/src/cli/setup-checkpoint.d.ts +57 -0
  146. package/dist/src/cli/setup-common.d.ts +277 -0
  147. package/dist/src/cli/setup-common.js +3 -2
  148. package/dist/src/cli/setup-common.js.map +1 -1
  149. package/dist/src/cli/setup-flow.d.ts +287 -0
  150. package/dist/src/cli/setup-service.d.ts +96 -0
  151. package/dist/src/cli/setup.d.ts +204 -0
  152. package/dist/src/cli/setup.js +94 -2
  153. package/dist/src/cli/setup.js.map +1 -1
  154. package/dist/src/cli/style.d.ts +320 -0
  155. package/dist/src/cli/token.d.ts +39 -0
  156. package/dist/src/cli/up.d.ts +155 -0
  157. package/dist/src/cli/up.js +119 -53
  158. package/dist/src/cli/up.js.map +1 -1
  159. package/dist/src/cli/usage.d.ts +37 -0
  160. package/dist/src/cli/values.d.ts +40 -0
  161. package/dist/src/cli/values.js +3 -4
  162. package/dist/src/cli/values.js.map +1 -1
  163. package/dist/src/cli/vault.d.ts +59 -0
  164. package/dist/src/cli/vault.js +2 -2
  165. package/dist/src/cli/vault.js.map +1 -1
  166. package/dist/src/cli/verb-registry.d.ts +76 -0
  167. package/dist/src/cli/verb-registry.js +344 -11
  168. package/dist/src/cli/verb-registry.js.map +1 -1
  169. package/dist/src/cli/wordmark.d.ts +31 -0
  170. package/dist/src/cli/wordmark.js +2 -2
  171. package/dist/src/codex/broker.d.ts +229 -0
  172. package/dist/src/codex/broker.js +548 -0
  173. package/dist/src/codex/broker.js.map +1 -0
  174. package/dist/src/codex/doctor.d.ts +13 -0
  175. package/dist/src/codex/doctor.js +41 -0
  176. package/dist/src/codex/doctor.js.map +1 -0
  177. package/dist/src/codex/manifest.d.ts +49 -0
  178. package/dist/src/codex/manifest.js +103 -0
  179. package/dist/src/codex/manifest.js.map +1 -0
  180. package/dist/src/codex/runner.d.ts +178 -0
  181. package/dist/src/codex/runner.js +231 -0
  182. package/dist/src/codex/runner.js.map +1 -0
  183. package/dist/src/codex/serve.d.ts +56 -0
  184. package/dist/src/codex/serve.js +98 -0
  185. package/dist/src/codex/serve.js.map +1 -0
  186. package/dist/src/codex/templates.d.ts +41 -0
  187. package/dist/src/codex/templates.js +319 -0
  188. package/dist/src/codex/templates.js.map +1 -0
  189. package/dist/src/codex/trust.d.ts +19 -0
  190. package/dist/src/codex/trust.js +183 -0
  191. package/dist/src/codex/trust.js.map +1 -0
  192. package/dist/src/codex/workspace-commit.d.ts +219 -0
  193. package/dist/src/codex/workspace-commit.js +549 -0
  194. package/dist/src/codex/workspace-commit.js.map +1 -0
  195. package/dist/src/codex/workspace-plan.d.ts +131 -0
  196. package/dist/src/codex/workspace-plan.js +561 -0
  197. package/dist/src/codex/workspace-plan.js.map +1 -0
  198. package/dist/src/core/actor.d.ts +2 -0
  199. package/dist/src/core/actor.js +5 -0
  200. package/dist/src/core/actor.js.map +1 -0
  201. package/dist/src/core/advance-cycle.d.ts +221 -0
  202. package/dist/src/core/advance-cycle.js +66 -2
  203. package/dist/src/core/advance-cycle.js.map +1 -1
  204. package/dist/src/core/agents-md.d.ts +278 -0
  205. package/dist/src/core/agents-md.js +33 -31
  206. package/dist/src/core/agents-md.js.map +1 -1
  207. package/dist/src/core/apply-patch.d.ts +49 -0
  208. package/dist/src/core/apply-patch.js +266 -0
  209. package/dist/src/core/apply-patch.js.map +1 -0
  210. package/dist/src/core/attest.d.ts +635 -0
  211. package/dist/src/core/attest.js +326 -4
  212. package/dist/src/core/attest.js.map +1 -1
  213. package/dist/src/core/audit.d.ts +510 -0
  214. package/dist/src/core/audit.js +13 -0
  215. package/dist/src/core/audit.js.map +1 -1
  216. package/dist/src/core/budgets.d.ts +238 -0
  217. package/dist/src/core/channel-owner.d.ts +213 -0
  218. package/dist/src/core/channel-owner.js +358 -0
  219. package/dist/src/core/channel-owner.js.map +1 -0
  220. package/dist/src/core/checkpoint.d.ts +500 -0
  221. package/dist/src/core/child-env.d.ts +88 -0
  222. package/dist/src/core/clock.d.ts +52 -0
  223. package/dist/src/core/command-class.d.ts +697 -0
  224. package/dist/src/core/command-class.js +713 -25
  225. package/dist/src/core/command-class.js.map +1 -1
  226. package/dist/src/core/commit-guard.d.ts +272 -0
  227. package/dist/src/core/commit-guard.js +424 -0
  228. package/dist/src/core/commit-guard.js.map +1 -0
  229. package/dist/src/core/coverage-sources/adapter.d.ts +40 -0
  230. package/dist/src/core/coverage-sources/gh.d.ts +48 -0
  231. package/dist/src/core/coverage-sources/git.d.ts +101 -0
  232. package/dist/src/core/coverage.d.ts +217 -0
  233. package/dist/src/core/credential-spec.d.ts +72 -0
  234. package/dist/src/core/daemon-actor.d.ts +45 -0
  235. package/dist/src/core/daemon-actor.js +54 -0
  236. package/dist/src/core/daemon-actor.js.map +1 -0
  237. package/dist/src/core/dark-session.d.ts +432 -0
  238. package/dist/src/core/dark-session.js +266 -82
  239. package/dist/src/core/dark-session.js.map +1 -1
  240. package/dist/src/core/decision-refusal.d.ts +206 -0
  241. package/dist/src/core/decision-refusal.js +24 -2
  242. package/dist/src/core/decision-refusal.js.map +1 -1
  243. package/dist/src/core/env-file.d.ts +455 -0
  244. package/dist/src/core/env-file.js +60 -1
  245. package/dist/src/core/env-file.js.map +1 -1
  246. package/dist/src/core/execute.d.ts +871 -0
  247. package/dist/src/core/execute.js +59 -8
  248. package/dist/src/core/execute.js.map +1 -1
  249. package/dist/src/core/frontmatter.d.ts +78 -0
  250. package/dist/src/core/gate-window.d.ts +312 -0
  251. package/dist/src/core/gate.d.ts +1449 -0
  252. package/dist/src/core/gate.js +149 -14
  253. package/dist/src/core/gate.js.map +1 -1
  254. package/dist/src/core/gesture-refusal.d.ts +166 -0
  255. package/dist/src/core/gesture-refusal.js +188 -0
  256. package/dist/src/core/gesture-refusal.js.map +1 -0
  257. package/dist/src/core/git-run.d.ts +73 -0
  258. package/dist/src/core/harness-version.d.ts +157 -0
  259. package/dist/src/core/harness-version.js +4 -1
  260. package/dist/src/core/harness-version.js.map +1 -1
  261. package/dist/src/core/harness-wait.d.ts +55 -0
  262. package/dist/src/core/head-retry.d.ts +107 -0
  263. package/dist/src/core/instance.d.ts +310 -0
  264. package/dist/src/core/instance.js +113 -0
  265. package/dist/src/core/instance.js.map +1 -1
  266. package/dist/src/core/intake-limits.d.ts +247 -0
  267. package/dist/src/core/jcs.d.ts +52 -0
  268. package/dist/src/core/journal.d.ts +144 -0
  269. package/dist/src/core/live-draw.d.ts +436 -0
  270. package/dist/src/core/log-reconcile.d.ts +89 -0
  271. package/dist/src/core/log-subscribe.d.ts +36 -0
  272. package/dist/src/core/log-subscribe.js +162 -0
  273. package/dist/src/core/log-subscribe.js.map +1 -0
  274. package/dist/src/core/log.d.ts +316 -0
  275. package/dist/src/core/log.js.map +1 -1
  276. package/dist/src/core/loop.d.ts +274 -0
  277. package/dist/src/core/loop.js +11 -0
  278. package/dist/src/core/loop.js.map +1 -1
  279. package/dist/src/core/md-fence.d.ts +41 -0
  280. package/dist/src/core/money.d.ts +147 -0
  281. package/dist/src/core/payload-census.d.ts +74 -0
  282. package/dist/src/core/payload-store.d.ts +175 -0
  283. package/dist/src/core/payload.d.ts +71 -0
  284. package/dist/src/core/policy-diff.d.ts +292 -0
  285. package/dist/src/core/policy-diff.js +27 -4
  286. package/dist/src/core/policy-diff.js.map +1 -1
  287. package/dist/src/core/policy-expectations.d.ts +199 -0
  288. package/dist/src/core/policy-explain.d.ts +160 -0
  289. package/dist/src/core/policy-explain.js +63 -3
  290. package/dist/src/core/policy-explain.js.map +1 -1
  291. package/dist/src/core/policy-load.d.ts +567 -0
  292. package/dist/src/core/policy-load.js +36 -6
  293. package/dist/src/core/policy-load.js.map +1 -1
  294. package/dist/src/core/policy-match.d.ts +324 -0
  295. package/dist/src/core/policy-match.js +72 -9
  296. package/dist/src/core/policy-match.js.map +1 -1
  297. package/dist/src/core/policy-proposal.d.ts +317 -0
  298. package/dist/src/core/policy-proposal.js +102 -2
  299. package/dist/src/core/policy-proposal.js.map +1 -1
  300. package/dist/src/core/prompt-layout.d.ts +221 -0
  301. package/dist/src/core/protected-path-guard.d.ts +566 -0
  302. package/dist/src/core/protected-path-guard.js +848 -55
  303. package/dist/src/core/protected-path-guard.js.map +1 -1
  304. package/dist/src/core/question-preempted.d.ts +141 -0
  305. package/dist/src/core/question-preempted.js +152 -0
  306. package/dist/src/core/question-preempted.js.map +1 -0
  307. package/dist/src/core/read-scope.d.ts +172 -0
  308. package/dist/src/core/read-scope.js +252 -0
  309. package/dist/src/core/read-scope.js.map +1 -0
  310. package/dist/src/core/registration.d.ts +25 -0
  311. package/dist/src/core/reindex.d.ts +99 -0
  312. package/dist/src/core/sampler.d.ts +313 -0
  313. package/dist/src/core/sandbox.d.ts +371 -0
  314. package/dist/src/core/sandbox.js +190 -1
  315. package/dist/src/core/sandbox.js.map +1 -1
  316. package/dist/src/core/seal.d.ts +165 -0
  317. package/dist/src/core/sender-identity.d.ts +476 -0
  318. package/dist/src/core/sender-identity.js +572 -0
  319. package/dist/src/core/sender-identity.js.map +1 -0
  320. package/dist/src/core/shlex.d.ts +102 -0
  321. package/dist/src/core/shlex.js +159 -0
  322. package/dist/src/core/shlex.js.map +1 -0
  323. package/dist/src/core/state.d.ts +505 -0
  324. package/dist/src/core/task-file.d.ts +185 -0
  325. package/dist/src/core/telegram-config.d.ts +93 -0
  326. package/dist/src/core/token.d.ts +409 -0
  327. package/dist/src/core/token.js +21 -38
  328. package/dist/src/core/token.js.map +1 -1
  329. package/dist/src/core/validate.d.ts +138 -0
  330. package/dist/src/core/values.d.ts +147 -0
  331. package/dist/src/core/values.js +36 -1
  332. package/dist/src/core/values.js.map +1 -1
  333. package/dist/src/core/vault.d.ts +291 -0
  334. package/dist/src/core/verified-snapshot.d.ts +204 -0
  335. package/dist/src/core/verify.d.ts +336 -0
  336. package/dist/src/core/version.d.ts +8 -0
  337. package/dist/src/core/wysiwys.d.ts +370 -0
  338. package/dist/src/daemon/advance-child.d.ts +39 -0
  339. package/dist/src/daemon/advance.d.ts +476 -0
  340. package/dist/src/daemon/advance.js +25 -4
  341. package/dist/src/daemon/advance.js.map +1 -1
  342. package/dist/src/daemon/audit.d.ts +87 -0
  343. package/dist/src/daemon/daemon.d.ts +1180 -0
  344. package/dist/src/daemon/daemon.js +9 -0
  345. package/dist/src/daemon/daemon.js.map +1 -1
  346. package/dist/src/daemon/dark-session.d.ts +64 -0
  347. package/dist/src/daemon/draw-child.d.ts +36 -0
  348. package/dist/src/daemon/draw.d.ts +154 -0
  349. package/dist/src/daemon/git-evidence.d.ts +173 -0
  350. package/dist/src/daemon/git-evidence.js +1 -1
  351. package/dist/src/daemon/projection.d.ts +180 -0
  352. package/dist/src/daemon/prune.d.ts +207 -0
  353. package/dist/src/mcp/http.d.ts +113 -0
  354. package/dist/src/mcp/server.d.ts +265 -0
  355. package/dist/src/mcp/server.js +17 -1
  356. package/dist/src/mcp/server.js.map +1 -1
  357. package/docs/adapter-api.md +106 -0
  358. package/docs/cli-reference.md +1316 -63
  359. package/docs/codex-enforced-session.md +103 -0
  360. package/docs/codex-workspace-broker.md +118 -0
  361. package/package.json +14 -2
  362. package/schema/codex-instance.schema.json +82 -0
  363. package/schema/event.schema.json +539 -9
  364. package/schema/fixtures/codex-instance/invalid/unpinned-codex-version.json +40 -0
  365. package/schema/fixtures/codex-instance/valid/canonical.json +40 -0
  366. package/schema/fixtures/event/invalid/approval-granted-sender-hashed-false.json +20 -0
  367. package/schema/fixtures/event/invalid/approval-granted-sender-hashed-raw-id.json +20 -0
  368. package/schema/fixtures/event/invalid/audit-gesture-refused-human-actor.json +16 -0
  369. package/schema/fixtures/event/invalid/audit-gesture-refused-no-actor-no-sender.json +15 -0
  370. package/schema/fixtures/event/invalid/audit-gesture-refused-unknown-gesture.json +16 -0
  371. package/schema/fixtures/event/invalid/audit-question-preempted-agent-actor.json +16 -0
  372. package/schema/fixtures/event/invalid/audit-question-preempted-no-question-id.json +16 -0
  373. package/schema/fixtures/event/invalid/audit-question-preempted-unknown-source.json +15 -0
  374. package/schema/fixtures/event/invalid/gate-path-signed-off-absolute-path.json +14 -0
  375. package/schema/fixtures/event/invalid/gate-path-signed-off-agent-actor.json +14 -0
  376. package/schema/fixtures/event/invalid/gate-path-signed-off-missing-path.json +13 -0
  377. package/schema/fixtures/event/valid/approval-granted-sender-hashed.json +20 -0
  378. package/schema/fixtures/event/valid/audit-gesture-refused-review-note.json +21 -0
  379. package/schema/fixtures/event/valid/audit-gesture-refused-sender-key-unavailable.json +19 -0
  380. package/schema/fixtures/event/valid/audit-gesture-refused.json +19 -0
  381. package/schema/fixtures/event/valid/audit-question-preempted-no-verdict.json +16 -0
  382. package/schema/fixtures/event/valid/audit-question-preempted.json +20 -0
  383. package/schema/fixtures/event/valid/gate-path-signed-off.json +14 -0
  384. package/schema/fixtures/event/valid/harness-kind-claude-code.json +23 -0
  385. package/schema/fixtures/event/valid/harness-kind-codex.json +23 -0
  386. package/schema/fixtures/event/valid/harness-kind-cursor.json +23 -0
  387. package/schema/fixtures/event/valid/harness-kind-grok.json +23 -0
  388. package/schema/fixtures/event/valid/harness-kind-muse.json +23 -0
  389. package/schema/fixtures/policy/invalid/senders-half-keyed.json +20 -0
  390. package/schema/fixtures/policy/valid/canonical.json +1 -1
  391. package/schema/fixtures/policy/valid/senders-keyed.json +24 -0
  392. package/schema/fixtures/policy-md/valid/canonical.md +1 -1
  393. package/schema/fixtures/policy-md/valid/with-values.md +5 -7
  394. package/schema/fixtures/values/invalid/class-shaped.json +1 -1
  395. package/schema/fixtures/values/invalid/duplicate-entry.json +1 -1
  396. package/schema/fixtures/values/invalid/non-string-item.json +1 -1
  397. package/schema/fixtures/values/invalid/over-cap.json +1 -1
  398. package/schema/fixtures/values/invalid/unknown-key.json +1 -1
  399. package/schema/fixtures/values/invalid/version-float.json +1 -0
  400. package/schema/fixtures/values/invalid/version-integer.json +1 -0
  401. package/schema/fixtures/values/invalid/version-wrong-string.json +1 -0
  402. package/schema/fixtures/values/valid/empty-lists.json +2 -3
  403. package/schema/fixtures/values/valid/full.json +5 -7
  404. package/schema/fixtures/values/valid/minimal.json +1 -1
  405. package/schema/fixtures/values-md/invalid/schema-invalid.md +5 -3
  406. package/schema/fixtures/values-md/invalid/two-blocks.md +3 -3
  407. package/schema/fixtures/values-md/invalid/unterminated.md +2 -2
  408. package/schema/fixtures/values-md/invalid/version-1.md +69 -0
  409. package/schema/fixtures/values-md/invalid/version-unquoted.md +64 -0
  410. package/schema/fixtures/values-md/invalid/yaml-error.md +2 -2
  411. package/schema/fixtures/values-md/valid/absent.md +1 -1
  412. package/schema/fixtures/values-md/valid/with-values.md +5 -7
  413. package/schema/policy.schema.json +75 -3
  414. package/schema/values.schema.json +7 -11
  415. package/templates/codex/README.md +9 -0
  416. package/schema/fixtures/values/invalid/version-string.json +0 -1
@@ -0,0 +1,1944 @@
1
+ /**
2
+ * The Telegram push channel (SPEC.md §10.3, APRV-26).
3
+ *
4
+ * A Telegram bot is the reference *push* channel: the runtime sends the pending
5
+ * request into a chat the approver already reads, and the approver answers with
6
+ * one tap. Everything the contract says about a channel still holds here and is
7
+ * worth restating, because a network channel is where the temptations live:
8
+ *
9
+ * - **It decides nothing.** A `callback_query` becomes a {@link ChannelDecision}
10
+ * and is handed to the handler the runtime registered. That handler calls
11
+ * `recordChannelDecision`, which calls the human-only `decide()`. There is no
12
+ * second path, so TTL lapse, budget re-check, attestation, idempotency and
13
+ * compare-and-append all still apply to a button press.
14
+ * - **It never sees a token.** A grant mints a single-use execution token, and
15
+ * `recordChannelDecision` hands it to *its* caller, not to the channel. See
16
+ * "The token never goes back into the chat" below.
17
+ * - **It holds no decision state.** The only thing kept in memory is the map
18
+ * from a callback nonce to the action key it was issued for, which is
19
+ * delivery bookkeeping, not authorization. It is lost on restart, and a
20
+ * restarted listener re-notifies the pending queue. Since APRV-196 a button
21
+ * also carries a digest of its action key, so a tap on a pre-restart copy
22
+ * resolves to the request the new process is holding open and decides it;
23
+ * what a lost map costs is a duplicate message, not a dead button. The trade
24
+ * is unchanged and is the reason that works at all: an approval that survives
25
+ * a restart lives in the log, never in a channel's memory, so the thing a
26
+ * stale button resolves against is a request the LOG still calls pending.
27
+ *
28
+ * ## Zero dependencies
29
+ *
30
+ * The Bot API is plain HTTPS with JSON bodies, so this module uses `fetch`
31
+ * (global since Node 18) and nothing else. No SDK, no polling library, no
32
+ * webhook framework. `fetch` is injectable ({@link TelegramConfig.fetch}) and
33
+ * `apiBase` is injectable, which is how the test suite runs the whole channel —
34
+ * notify, long-poll, callbacks, failure modes — against a local mock Bot API
35
+ * server and never touches the real network.
36
+ *
37
+ * ## Config-declared identity — SPEC.md §11
38
+ *
39
+ * > Human identity in v0.1 is config-declared (an environment variable or
40
+ * > flag); the trust boundary is the local machine, and anyone who can set that
41
+ * > configuration and write to the log is inside it.
42
+ *
43
+ * What this channel authenticates, exactly: that the callback arrived **from
44
+ * the configured chat id**, and which **Telegram account** the Bot API
45
+ * attributes the tap to (`callback_query.from.id`). It authenticates no
46
+ * person, because no transport can: an account id is evidence about an
47
+ * account.
48
+ *
49
+ * What is done with the second fact is the operator's to decide, and since
50
+ * APRV-324 there are two settings:
51
+ *
52
+ * - **No `senders` block in the policy.** Nothing changes from every build
53
+ * before it. The decision is recorded against the human actor the *runtime*
54
+ * was configured with (`APPROVAL_HUMAN` / `--as`), so the guarantee is
55
+ * "someone with access to the configured chat, on a runtime configured by
56
+ * someone with local control, tapped Approve" — not "alice tapped Approve".
57
+ * Anyone in that chat can approve as the configured actor. Use a private chat
58
+ * with the bot, and treat the chat's membership as part of the trust
59
+ * boundary.
60
+ * - **A `senders` block mapping account ids to approvers.** The decision is
61
+ * recorded against the person the operator attested that account to, and a
62
+ * tap from an account the policy does not name is REFUSED rather than
63
+ * recorded under the listener's identity. The guarantee becomes "the account
64
+ * the operator attested to alice tapped Approve", which is a statement about
65
+ * Telegram's session handling and the operator's assertion, and still not a
66
+ * proof of personhood. Cryptographic identity is future work and is not a
67
+ * v0.1 claim.
68
+ *
69
+ * The channel itself resolves nothing either way. It reports the account it
70
+ * saw; `channels/contract.ts` resolves it against the attested policy, and
71
+ * `core/gate.ts` decides. See `design/channel-sender-identity.md`.
72
+ *
73
+ * ## Formatting: HTML, not MarkdownV2 — a deliberate choice
74
+ *
75
+ * Messages use `parse_mode: "HTML"`. MarkdownV2 requires escaping eighteen
76
+ * characters (`_*[]()~\`>#+-=|{}.!`) in every text position, with different
77
+ * rules inside code spans, and a single missed one is not a cosmetic bug: it is
78
+ * agent-authored text (a summary, a payload body) changing the *structure* of
79
+ * the message a human is about to approve. HTML mode needs exactly three
80
+ * escapes — `&`, `<`, `>` — applied uniformly to every interpolated value by
81
+ * {@link escapeHtml}, and `<pre>` carries the payload bytes without any
82
+ * character being special inside it beyond those three. A narrower escape rule
83
+ * is a narrower injection surface, and the untrusted input here is precisely
84
+ * the claimed fields and the payload.
85
+ *
86
+ * ## The token never goes back into the chat — flagged for human review
87
+ *
88
+ * `recordChannelDecision` returns the raw execution token to the runtime on a
89
+ * grant. The runtime (`cli/channel.ts`) prints it on the **listener's stdout**
90
+ * and nowhere else. It is never sent as a Telegram message, never put in an
91
+ * `answerCallbackQuery` text, and never logged by this module. A chat
92
+ * transcript is stored on someone else's servers, is backed up to phones, and
93
+ * is readable by anyone who is later added to the chat; a single-use execution
94
+ * token in it would be a credential in a place with none of the properties a
95
+ * credential store has. The consequence is real and is the reason this is
96
+ * flagged: the human who approves on their phone does not get the token on
97
+ * their phone — the agent or operator at the terminal running `approval channel
98
+ * telegram listen` does. For v0.1's local-first, single-operator model that is
99
+ * the right side of the trade; a deployment where the approver and the runtime
100
+ * are different people needs a token-delivery design, not a chat message.
101
+ *
102
+ * ## Reject collects no free-text reason — flagged for human review
103
+ *
104
+ * Telegram inline keyboards have no text input: a button press returns only its
105
+ * `callback_data`. Collecting the approver's reason would require a
106
+ * `ForceReply` round trip (send a prompt, wait for the *next* message in the
107
+ * chat, correlate it), which means holding a second piece of per-request state
108
+ * and deciding what to do when the reply never comes. This task records the
109
+ * rejection immediately with the note `rejected via telegram (callback <id>)`,
110
+ * so the audit trail says how the refusal was collected and which callback it
111
+ * came from, and says nothing about why. A follow-up may add the ForceReply
112
+ * flow; until then, a reason belongs in `approval reject --note`.
113
+ *
114
+ * ## Batching (B7): the digest (APRV-115)
115
+ *
116
+ * SPEC.md §10.3 lets a channel collect one gesture over a set, and until
117
+ * APRV-115 this channel took that option **degenerately**: one message per
118
+ * member, each with its own keyboard, all sharing one batch delivery id. The
119
+ * semantics were right and the ergonomics were the incident. A research session
120
+ * once produced forty near-identical `network.call` prompts in twenty minutes,
121
+ * one message each, and a channel that behaves like a notification hose is a
122
+ * channel a human learns to swipe away.
123
+ *
124
+ * A group of similar pending requests (the grouping key is
125
+ * {@link digestKeyOf}, applied by the listener) is now delivered as a
126
+ * **digest**: every member's full prompt and full payload first, in its own
127
+ * messages and with no buttons, then ONE trailing message carrying the
128
+ * headline, one summary line per member, and the keyboard — a per-member
129
+ * Approve/Reject row for each, plus an "all" row.
130
+ *
131
+ * Four properties hold it together:
132
+ *
133
+ * - **The payloads come first.** The buttons are on the LAST message, and
134
+ * every member's `<pre>` payload region has already been sent above it. An
135
+ * approver cannot reach an "Approve all" without the bytes it covers having
136
+ * been put in front of them (SPEC.md §10.4).
137
+ * - **It fails toward more messages.** A group whose digest text would not fit
138
+ * inside {@link TELEGRAM_MAX_MESSAGE_CHARS}, or that has fewer than two
139
+ * members, falls back to the old one-message-per-member delivery, and so
140
+ * does a group `assembleBatch` refuses. The listener caps a digest at
141
+ * {@link TELEGRAM_DIGEST_MAX_MEMBERS} and splits a larger burst into
142
+ * several. Never a grant covering an unseen payload.
143
+ * - **"All" is N decisions, not one.** An all-button hands the runtime's
144
+ * handler one {@link ChannelDecision} per still-armed member, in order, and
145
+ * the handler records each through the gate's compare-and-append on its own.
146
+ * The log never learns the word "batch": it gets N `approval.granted` or
147
+ * `approval.rejected` events, each bound to its own action and payload hash,
148
+ * each carrying the shared batch delivery id (SPEC.md §10.3).
149
+ * - **Annotation is per member.** A decided, expired or withdrawn member marks
150
+ * its own line on the digest and loses its own buttons; the others stay
151
+ * armed. A partially decided digest therefore shows mixed state, which is
152
+ * what {@link TelegramChannel.annotate} redraws it to.
153
+ *
154
+ * The digest bookkeeping is delivery state of exactly the kind the nonce map
155
+ * already was: what was sent where, never what was decided. Every outcome word
156
+ * on it comes from the verified log or from the record the gate appended, and
157
+ * losing the map to a restart degrades to a stale message whose buttons the
158
+ * gate refuses, never to a wrong one.
159
+ *
160
+ * ## Every terminal state edits its message (APRV-113)
161
+ *
162
+ * A decided prompt used to look exactly like a pending one: the tap toasted,
163
+ * and the message kept its text and its live buttons. So did a request answered
164
+ * at the CLI or the web queue while the chat prompt was up, and so did one the
165
+ * daemon expired. The chat transcript — the thing the approver actually scrolls
166
+ * — said "APPROVAL REQUIRED" about a question that had been settled hours ago.
167
+ *
168
+ * Every terminal state this process observes for a message it delivered now
169
+ * edits that message: {@link TelegramChannel.annotate} replaces the text with
170
+ * the outcome and clears the keyboard in ONE `editMessageText`, and forgets the
171
+ * delivery so a tap on a button the edit did not remove refuses rather than
172
+ * decides. {@link TelegramChannel.retract} is the withdrawal case of it.
173
+ *
174
+ * Two properties this keeps, deliberately:
175
+ *
176
+ * - **It is not state.** The map this consults is delivery bookkeeping, and
177
+ * annotating removes from it rather than adding. Losing it (a restart)
178
+ * degrades to a message that is never annotated — stale text in front of a
179
+ * human whose gate still refuses every tap on it — and never to a message
180
+ * annotated with the wrong outcome, because every outcome word comes from the
181
+ * verified log at the moment it is written.
182
+ * - **The token is never in an edit.** An annotation carries the outcome word,
183
+ * the action key, who decided, when, and the record's seq. It never carries
184
+ * the execution token, for the reason spelled out above.
185
+ *
186
+ * ## The bookkeeping is swept (APRV-135)
187
+ *
188
+ * Both maps used to be released only by process exit. Annotating a delivery
189
+ * removes its nonces, but nothing removes a delivery that is never annotated
190
+ * (a request that simply lapsed) or a digest whose members were each settled
191
+ * individually, so a listener left running for weeks held memory proportional
192
+ * to every prompt it had ever sent — and APRV-110's ambient runtime makes
193
+ * week-long listeners the normal case rather than the exception.
194
+ *
195
+ * {@link TelegramChannel.sweep} drops an entry when every member of it is
196
+ * terminal AND the entry is older than the policy's approval TTL. Both halves
197
+ * matter and the pair is what makes the drop safe: past the TTL the gate
198
+ * refuses every decision on the request, so a button referencing a dropped
199
+ * entry could not have been honoured anyway, and it is answered by the
200
+ * stale-callback path that a restarted listener's buttons already take. It is
201
+ * process memory and nothing else: no event is appended, no message is edited,
202
+ * and the log is not opened.
203
+ */
204
+ import type { ChannelBatch, ChannelDecision, ChannelHealth, ChannelRequest, DecisionOutcome, DeliveryId, RenderedRequest, TaggedField, TestableChannel } from "./contract.js";
205
+ import type { ChannelSender } from "../core/sender-identity.js";
206
+ import { type Reaction, type ReviewVerdict } from "../core/audit.js";
207
+ import { type PromptLayout } from "../core/prompt-layout.js";
208
+ export { TELEGRAM_CHAT_ENV, TELEGRAM_TOKEN_ENV, telegramChatEnvFor, telegramTokenEnvFor, } from "../core/telegram-config.js";
209
+ /** The real Bot API. Overridden only by tests, against a local mock. */
210
+ export declare const TELEGRAM_DEFAULT_API_BASE = "https://api.telegram.org";
211
+ /** Telegram's hard limit on a message's text. */
212
+ export declare const TELEGRAM_MAX_MESSAGE_CHARS = 4096;
213
+ /** Telegram's hard limit on `callback_data`, in bytes. */
214
+ export declare const TELEGRAM_MAX_CALLBACK_BYTES = 64;
215
+ /** The note recorded on a rejection collected from a button. */
216
+ export declare const TELEGRAM_REJECT_NOTE = "rejected via telegram";
217
+ /**
218
+ * The toast a tap gets when no branch produced one of its own (APRV-196).
219
+ *
220
+ * It is deliberately about the tap and not about the request: this text is only
221
+ * ever reached when the handler threw or forgot, which are exactly the states
222
+ * in which this process does not know what became of the request. Saying so is
223
+ * the honest answer, and it is still infinitely better than a button that spins.
224
+ */
225
+ export declare const TELEGRAM_ACK_FALLBACK = "Received \u2014 this listener could not finish reading your tap. Nothing was recorded by it; check the message above for the outcome.";
226
+ /**
227
+ * The toast a tap gets the instant it is recognized, BEFORE the gate runs
228
+ * (APRV-206).
229
+ *
230
+ * Telegram gives a callback query exactly one answer, and until it arrives the
231
+ * button spins on the approver's phone. Sending it after the decision made the
232
+ * spinner as long as the decision — which grew with the log — and the human,
233
+ * with no way to tell a slow tap from a swallowed one, tapped again.
234
+ *
235
+ * So this is what the single answer says, and its wording is load-bearing: it
236
+ * claims only that the tap ARRIVED. It must never say granted, rejected,
237
+ * approved, recorded, or anything else a reader could take as "the log now says
238
+ * so", because at the moment it is sent nothing has been appended and the gate
239
+ * may still refuse. What became of the request is said by the message edit that
240
+ * follows, which is written from the record the gate actually appended (or from
241
+ * its refusal). The toast vanishes; the message stays.
242
+ */
243
+ export declare const TELEGRAM_ACK_HEARD = "Heard \u2014 deciding. The message will say what the log recorded.";
244
+ /**
245
+ * The headline on a message whose tap the gate refused (APRV-206).
246
+ *
247
+ * Before the early ack, a refusal was a toast and the message was left alone.
248
+ * Now that the single answer is spent on "heard", the refusal has to reach the
249
+ * approver here or nowhere. The buttons go with it ({@link annotate} disarms),
250
+ * which is the right outcome in both directions: a request the gate calls
251
+ * terminal has no live decision left to collect, and a request that is still
252
+ * pending is re-delivered by the next dispatch cycle as a fresh prompt.
253
+ */
254
+ export declare const TELEGRAM_NOT_RECORDED = "\u2717 NOT RECORDED";
255
+ /**
256
+ * The detail line under {@link TELEGRAM_NOT_RECORDED} when the runtime's
257
+ * decision handler threw (APRV-206).
258
+ *
259
+ * The wording is careful about what it does not know: a handler that threw may
260
+ * have thrown before or after its append, so this says where to look rather
261
+ * than what happened. The log is the thing that knows.
262
+ */
263
+ export declare const TELEGRAM_HANDLER_FAILED = "This listener failed while recording your tap. Check `approval queue` \u2014 the log is what says whether anything was recorded.";
264
+ /** Prefixed to the toast when the tap arrived on a pre-restart copy (APRV-196). */
265
+ export declare const TELEGRAM_STALE_COPY_PREFIX = "Earlier copy of this request \u2014 ";
266
+ /**
267
+ * The toast for a tap on a copy of an action this process is not holding open,
268
+ * when no verified-log probe is configured to say more (APRV-196).
269
+ */
270
+ export declare const TELEGRAM_STALE_UNKNOWN = "This request is not open here \u2014 it was already decided, it lapsed, or another listener holds it. Nothing was recorded.";
271
+ /**
272
+ * The headline of an ordinary single-request prompt.
273
+ *
274
+ * Exported because the mock Bot API and several tests key on it, and because a
275
+ * digest member's header deliberately does NOT use it: a member prompt carries
276
+ * no buttons, so calling it "APPROVAL REQUIRED" would point a reader at a
277
+ * message that cannot take their answer.
278
+ */
279
+ export declare const TELEGRAM_PROMPT_HEADING = "APPROVAL REQUIRED";
280
+ /**
281
+ * What the label over the payload chunks names (APRV-162).
282
+ *
283
+ * The chunks carry the canonical rendering, which is a deterministic function
284
+ * of the bytes and not the bytes themselves; calling it "the exact bytes" told
285
+ * the reader that a diff view and a JSON file were the same object. The
286
+ * rendering names its own `display_hash`, and the store path inside it is the
287
+ * route back to the bytes.
288
+ */
289
+ export declare const PAYLOAD_CHUNK_LABEL_TAIL = "the canonical rendering this approval's display_hash names; raw bytes at the store path inside";
290
+ export declare const PAYLOAD_CHUNK_LABEL = "PAYLOAD \u2014 the canonical rendering this approval's display_hash names; raw bytes at the store path inside";
291
+ /**
292
+ * What the claimed block is headed, and what a second claimed message is headed
293
+ * when a rationale overflows one (APRV-165).
294
+ *
295
+ * Both say CLAIMED and both say NOT verified, because a continuation is a
296
+ * message a reader may see first, and a claimed line that arrives under no
297
+ * heading at all reads as the runtime's own.
298
+ */
299
+ export declare const TELEGRAM_CLAIMED_HEADING_PREFIX = "WHAT THIS DOES \u2014 CLAIMED by";
300
+ export declare const TELEGRAM_CLAIMED_HEADING_SUFFIX = "NOT verified by the runtime";
301
+ export declare const TELEGRAM_CLAIMED_CONTINUED_HEADING = "WHAT THIS DOES (continued) \u2014 CLAIMED, NOT verified by the runtime";
302
+ /**
303
+ * The most members one digest may carry (APRV-115).
304
+ *
305
+ * Not a rendering limit — {@link renderDigest} checks the real one against
306
+ * {@link TELEGRAM_MAX_MESSAGE_CHARS} — but a *reading* one: a keyboard of
307
+ * twenty rows is a wall, and the failure this feature exists to fix is a human
308
+ * who stops reading. A burst larger than this becomes several digests, which is
309
+ * the direction this whole design fails in.
310
+ */
311
+ export declare const TELEGRAM_DIGEST_MAX_MEMBERS = 8;
312
+ /**
313
+ * The headline each terminal state puts on the message it settles (APRV-113).
314
+ *
315
+ * Keyed by `core/state.ts`'s `RequestState` names for the terminal states, so
316
+ * the caller that derived the state from the verified log picks a word by
317
+ * indexing rather than by re-deciding what happened.
318
+ *
319
+ * Glyphs, not emoji: `✓`/`✗` are the vocabulary `cli/style.ts` uses for the
320
+ * same ok/fail distinction, and every line of *message text* this channel
321
+ * writes ("APPROVAL REQUIRED", "PAYLOAD", "WITHDRAWN") is emoji-free. The
322
+ * emoji live on the button labels, which are a different surface and stay as
323
+ * they are. `withdrawn` keeps the exact wording APRV-106 shipped.
324
+ */
325
+ export declare const TELEGRAM_TERMINAL_HEADLINES: {
326
+ readonly granted: "✓ APPROVED";
327
+ readonly rejected: "✗ REJECTED";
328
+ readonly revoked: "✗ REVOKED — the grant was taken back";
329
+ readonly expired: "✗ EXPIRED — the approval window closed";
330
+ readonly withdrawn: "WITHDRAWN — no decision is needed";
331
+ };
332
+ /** A state {@link TELEGRAM_TERMINAL_HEADLINES} has a word for. */
333
+ export type TelegramTerminalState = keyof typeof TELEGRAM_TERMINAL_HEADLINES;
334
+ /** Whether a derived request state is one an annotation can settle a message on. */
335
+ export declare function isTelegramTerminalState(state: string): state is TelegramTerminalState;
336
+ /**
337
+ * `HH:MM UTC`, or the raw instant when it does not parse.
338
+ *
339
+ * UTC and not a local zone: the listener, the approver's phone and the log can
340
+ * all be in different places, and the log's own timestamps are UTC. A clock a
341
+ * reader can line up against `approval log` beats one that matches their wrist.
342
+ */
343
+ export declare function utcClock(ts: string): string;
344
+ /** The "who decided, when, and which record says so" line of an annotation. */
345
+ export declare function decidedLine(actor: string, ts: string, seq: number): string;
346
+ /**
347
+ * How long a settled delivery is remembered when the policy declares no
348
+ * `defaults.approval_ttl` (APRV-135).
349
+ *
350
+ * A policy with no TTL bounds nothing, so "past the approval TTL" can never
351
+ * become true and a sweep keyed on it alone would never fire — which is the
352
+ * unbounded map this task exists to remove. The retention floor takes over
353
+ * there, and it applies only to entries whose every member this process has
354
+ * seen settled: with no TTL an undecided request stays answerable forever, and
355
+ * forgetting its button would take a live decision away from an approver.
356
+ *
357
+ * A day, because the point of remembering a settled delivery at all is that an
358
+ * approver may still tap a button on a message already scrolled past, and the
359
+ * answer they should get is the stale-callback reply either way.
360
+ */
361
+ export declare const TELEGRAM_DEFAULT_RETENTION_MS: number;
362
+ /** Least time between two sweeps. A sweep is O(map); once a minute is plenty. */
363
+ export declare const TELEGRAM_SWEEP_INTERVAL_MS = 60000;
364
+ /**
365
+ * The slice of `fetch` this module uses, structurally.
366
+ *
367
+ * Declared here rather than imported so the channel depends on a shape, not on
368
+ * a lib: a test can hand over a stub, and the default is the global `fetch`
369
+ * that Node ≥ 20 ships.
370
+ */
371
+ export type TelegramFetch = (url: string, init: {
372
+ method: string;
373
+ headers: Record<string, string>;
374
+ body: string;
375
+ signal: AbortSignal;
376
+ }) => Promise<{
377
+ ok: boolean;
378
+ status: number;
379
+ text(): Promise<string>;
380
+ }>;
381
+ export interface TelegramConfig {
382
+ /**
383
+ * The bot token. Resolved by the *verb* from the variable
384
+ * {@link telegramTokenEnvFor} names ({@link TELEGRAM_TOKEN_ENV} by default);
385
+ * this constructor takes the value, so nothing in the channel reads the
386
+ * environment and a test cannot accidentally pick up a real token.
387
+ */
388
+ token: string;
389
+ /** The approver chat id, as a string. Callbacks from any other chat are ignored. */
390
+ chatId: string;
391
+ /** Bot API base. Defaults to {@link TELEGRAM_DEFAULT_API_BASE}. */
392
+ apiBase?: string;
393
+ /** Injectable `fetch`, for tests. Defaults to the global. */
394
+ fetch?: TelegramFetch;
395
+ /** `getUpdates` long-poll timeout, in seconds. */
396
+ pollTimeoutSeconds?: number;
397
+ /**
398
+ * Transport timeout for one call, in milliseconds. Defaults to the long-poll
399
+ * timeout plus ten seconds, which is the only sane default: a `getUpdates`
400
+ * that is *supposed* to hang for 25s must not be aborted at 30s of total
401
+ * silence for the wrong reason. Overridable because a server that accepts a
402
+ * request and then says nothing at all is a real failure mode, and both an
403
+ * operator on a flaky link and this repo's test suite want to bound it.
404
+ */
405
+ requestTimeoutMs?: number;
406
+ /** First backoff step after a failed poll, in milliseconds. */
407
+ backoffMs?: number;
408
+ /** Backoff ceiling, in milliseconds. */
409
+ maxBackoffMs?: number;
410
+ /**
411
+ * Where operational complaints go. Defaults to stderr. Every message passes
412
+ * through {@link redact} first, so a token cannot reach it even by accident.
413
+ */
414
+ log?: (message: string) => void;
415
+ /** Injectable nonce source, for deterministic tests. */
416
+ nonce?: () => string;
417
+ /**
418
+ * The policy's `defaults.approval_ttl` in milliseconds, or `null` when it
419
+ * declares none (APRV-135).
420
+ *
421
+ * Passed in by the verb, which has already loaded the policy; the channel
422
+ * neither reads a policy file nor holds an opinion about what the TTL should
423
+ * be. It is used for one thing: deciding when a delivery this process
424
+ * remembers can no longer be the subject of a decision, and can therefore be
425
+ * forgotten. See {@link TelegramChannel.sweep}.
426
+ */
427
+ approvalTtlMs?: number | null;
428
+ /**
429
+ * Injectable monotonic-ish clock, in milliseconds, for the sweep.
430
+ *
431
+ * Defaults to `Date.now`. It exists so a test can run a week of deliveries in
432
+ * a millisecond; nothing else in this class reads a clock, and nothing that
433
+ * reaches a human or the log reads this one.
434
+ */
435
+ now?: () => number;
436
+ /**
437
+ * What to tell a human who tapped a button for an action this process is not
438
+ * holding open (APRV-196). One sentence, or `null` for "nothing is known".
439
+ *
440
+ * Supplied by the listener, which reads the VERIFIED log and can therefore
441
+ * say whether the request was granted, rejected, revoked, expired or
442
+ * withdrawn. The channel asks the question and repeats the answer; it does
443
+ * not derive one, does not cache one, and could not, because the only thing
444
+ * that knows is the log.
445
+ *
446
+ * The argument is an {@link actionRefOf} digest rather than an action key,
447
+ * for the same reason the button carries one: the string came off the
448
+ * network, and the probe's job is to look for a record whose key hashes to
449
+ * it, never to trust a name it was handed. Optional, and absent by default —
450
+ * a channel with no probe falls back to a toast that names no outcome.
451
+ */
452
+ describeAction?: (actionRef: string) => string | null;
453
+ /**
454
+ * Which rows the prompt shows (APRV-218), from `channels.telegram.prompt`.
455
+ *
456
+ * Passed in by the verb, which has already loaded the policy, for the reason
457
+ * {@link TelegramConfig.approvalTtlMs} is: this channel neither reads a
458
+ * policy file nor holds an opinion about what an operator should see.
459
+ * Defaults to {@link TELEGRAM_PROMPT_LAYOUT}, the layout APRV-143 and
460
+ * APRV-163 left behind, so a channel constructed without one renders exactly
461
+ * what it rendered before the key existed.
462
+ */
463
+ layout?: PromptLayout;
464
+ }
465
+ /**
466
+ * Why a callback was ignored.
467
+ *
468
+ * Every one of these is counted and complained about on stderr, and **none of
469
+ * them reaches the decision path or the log**. An ignored callback is not an
470
+ * event: writing "someone we do not answer to pressed a button" into an
471
+ * append-only approval log would let any stranger who guessed the bot's handle
472
+ * grow the record a human is asked to trust.
473
+ */
474
+ export declare const TELEGRAM_ANOMALY_KINDS: readonly [
475
+ /** The callback came from a chat that is not the configured one. */
476
+ "foreign-chat",
477
+ /** `callback_data` did not parse as one of ours. */
478
+ "malformed-callback",
479
+ /** A well-formed nonce this listener never issued (or issued before a restart). */
480
+ "unknown-callback",
481
+ /** The action key carried in `callback_data` disagrees with the issued nonce. */
482
+ "key-mismatch",
483
+ /**
484
+ * A tap on a copy of a request this process is no longer holding open
485
+ * (APRV-196): the nonce is not one of ours, and the action it names is not
486
+ * pending here either — it was decided, it lapsed, or another process owns
487
+ * it. Distinct from `unknown-callback` because the operator's question is
488
+ * different: nothing is wrong with the button, the question behind it is
489
+ * over. Always answered with a toast that names the state.
490
+ */
491
+ "stale-copy",
492
+ /**
493
+ * A message in the approver chat that began with `/` and named no command
494
+ * this channel answers (APRV-216). Counted rather than replied to: the chat
495
+ * belongs to a human, other bots and other slash commands live in it, and a
496
+ * channel that answered every unrecognised one would be noise in the one
497
+ * place an approver's attention is supposed to be scarce.
498
+ */
499
+ "unknown-command"];
500
+ export type TelegramAnomalyKind = (typeof TELEGRAM_ANOMALY_KINDS)[number];
501
+ export interface TelegramStats {
502
+ /** Messages successfully delivered by `notify`. */
503
+ notified: number;
504
+ /** Updates received from `getUpdates`, of any kind. */
505
+ updates: number;
506
+ /** Callbacks handed to the runtime's decision handler. */
507
+ decisions: number;
508
+ /** Failed `getUpdates` attempts the loop recovered from. */
509
+ pollErrors: number;
510
+ /** Ignored callbacks, by reason. Never a decision, never a log event. */
511
+ anomalies: Record<TelegramAnomalyKind, number>;
512
+ /**
513
+ * Taps that arrived on a copy whose nonce this process never issued and were
514
+ * carried to the gate anyway, because the action they reference is one this
515
+ * process is holding open (APRV-196). Not an anomaly: it is the duplicate-copy
516
+ * trap being defused, and it is counted so an operator can see how often a
517
+ * restart is costing the approver a wrong tap.
518
+ */
519
+ staleCopyDecisions: number;
520
+ /**
521
+ * Bot commands handed to the runtime's command handler (APRV-216). Never a
522
+ * decision and never a log event: a command reorders what this process shows
523
+ * and nothing else.
524
+ */
525
+ commands: number;
526
+ /**
527
+ * Review taps handed to the runtime's review handler (APRV-299). Counted
528
+ * whether the handler recorded or refused, because what this number measures
529
+ * is how much of the retrospective backlog reached a human's thumb; what
530
+ * became of each one is in the log and nowhere else.
531
+ */
532
+ reviews: number;
533
+ }
534
+ /**
535
+ * The bot commands the paced listener answers (APRV-216).
536
+ *
537
+ * A closed set, and deliberately a small one. Each is a verb about ATTENTION —
538
+ * what to put in front of the approver next — and none of them is a verb about
539
+ * the log: there is no `/grant`, and there will not be one, because a decision
540
+ * must name the request it decides and a typed command names nothing the
541
+ * runtime could bind a payload hash to. Buttons decide; commands navigate.
542
+ */
543
+ /**
544
+ * The prompt a checkpoint tap is drawn on (APRV-257).
545
+ *
546
+ * `head` is the `(seq, hash)` the human is being asked to sign and `lines` is
547
+ * what they read. They are separate fields because the head must survive into
548
+ * the signature unchanged while the text is free to be reworded, and because
549
+ * the runtime — not this channel — decides both.
550
+ */
551
+ export interface CheckpointPrompt {
552
+ head: {
553
+ seq: number;
554
+ hash: string;
555
+ };
556
+ lines: string[];
557
+ }
558
+ /** What the runtime did with a tap, as this channel reports it back. */
559
+ export interface CheckpointTapResponse {
560
+ /** Whether a `log.checkpoint` landed. */
561
+ ok: boolean;
562
+ /** The headline for the edited message. */
563
+ headline: string;
564
+ /** The lines under it. */
565
+ detail: string[];
566
+ /** The toast on the button, which Telegram caps at a short sentence. */
567
+ toast: string;
568
+ }
569
+ /**
570
+ * What the runtime does with a checkpoint tap. `sign` is false for "Not now",
571
+ * which appends nothing and is not a refusal of anything.
572
+ */
573
+ export type CheckpointTapHandler = (tap: {
574
+ sign: boolean;
575
+ head: {
576
+ seq: number;
577
+ hash: string;
578
+ };
579
+ /**
580
+ * The account the transport authenticated (APRV-324 follow-up), when it
581
+ * authenticated one. The channel resolves nothing: the runtime's handler
582
+ * asks the attested policy who this is, refuses an account it does not name,
583
+ * and signs as the person it does.
584
+ */
585
+ sender?: ChannelSender;
586
+ }) => CheckpointTapResponse | Promise<CheckpointTapResponse>;
587
+ export declare const TELEGRAM_COMMANDS: readonly ["queue", "skip", "next"];
588
+ export type TelegramCommand = (typeof TELEGRAM_COMMANDS)[number];
589
+ /**
590
+ * The command a message's text names, or `null` (APRV-216).
591
+ *
592
+ * Pure, and exported so the listener's tests can exercise the grammar without
593
+ * a transport. Telegram delivers a command in a group chat as `/skip@thebot`,
594
+ * so the `@suffix` is stripped; the bot's own username is not checked, because
595
+ * this channel only ever reads ONE chat and a message in it that says `/skip`
596
+ * to some other bot is a message the approver still meant as a skip more often
597
+ * than not. Anything after the command word is ignored: none of these three
598
+ * takes an argument, and silently discarding one is better than refusing a
599
+ * command a human typed with a stray word on the end.
600
+ */
601
+ export declare function parseBotCommand(text: unknown): TelegramCommand | null;
602
+ /** The three characters Telegram's HTML mode treats as markup. */
603
+ export declare function escapeHtml(text: string): string;
604
+ /**
605
+ * The suffix the model-authored line carries, on the line itself (APRV-144).
606
+ *
607
+ * One constant, shared with the terminal channel since APRV-197: this name is
608
+ * kept because the tests and the help text pin it, and it now resolves to
609
+ * {@link GLOSS_UNVERIFIED_SUFFIX} so the two surfaces cannot drift apart.
610
+ */
611
+ export declare const TELEGRAM_GLOSS_SUFFIX = "(model, unverified)";
612
+ /**
613
+ * The prefix a health row carries when it is the reason to look (APRV-163).
614
+ *
615
+ * Only the abnormal state of `autonomy`, `budgets` and the attestation renders
616
+ * at all, so the mark is never routine: a row bearing it is a row the reader
617
+ * has not seen on the last twenty prompts.
618
+ */
619
+ export declare const TELEGRAM_ANOMALY_MARK = "\u26A0 ";
620
+ /** One line of the message, and the request member it came from. */
621
+ interface Line {
622
+ field: string;
623
+ kind: "computed" | "claimed";
624
+ label: string;
625
+ text: string;
626
+ origin: string;
627
+ }
628
+ /**
629
+ * The message body, split into the two regions SPEC.md §9 requires a channel to
630
+ * keep visibly apart.
631
+ *
632
+ * The split is the whole point: computed lines sit under a heading that names
633
+ * the runtime as their author, claimed lines under one that names the agent and
634
+ * says "not verified". The `lastRendered()` report is built from *this* value,
635
+ * not from a parallel description of it, so the conformance suite is checking
636
+ * the thing that was actually sent.
637
+ */
638
+ export interface TelegramRendering {
639
+ /** Every line, in the order it appears, tagged as the request tagged it. */
640
+ lines: Line[];
641
+ /** The header segment: heading, action key, computed block. */
642
+ header: string;
643
+ /** The payload region, verbatim, or `null` when the request carries none. */
644
+ payloadText: string | null;
645
+ /**
646
+ * The claimed segment, sent LAST so it sits beside the buttons (APRV-165).
647
+ *
648
+ * The claimed lines are what the act means to a human — what this sends, to
649
+ * whom, why — and the approver decides on that, so it is the thing the thumb
650
+ * should be next to rather than the metadata above it. SPEC §10.3 permits
651
+ * claimed material around the canonical block on the condition this keeps:
652
+ * visibly separated, and headed by a label that names the claiming party and
653
+ * says the runtime did not check them.
654
+ *
655
+ * Never empty. A request with no gloss, no summary and no rationale still
656
+ * gets this message, because an absent description of the act is itself
657
+ * something the approver has to see, and because the keyboard needs one
658
+ * message that is always there to ride on.
659
+ */
660
+ claimedText: string;
661
+ }
662
+ /**
663
+ * The subset of a request's fields a review card also carries (APRV-299).
664
+ *
665
+ * A retrospective review card is not a request and must never be rendered as
666
+ * one, but the five rows below say exactly what they say on a prompt: which
667
+ * class this was, what the command did, which task it belonged to, what the
668
+ * agent claimed it would do, and the model's sentence about it where a listener
669
+ * attached one. Naming the subset as a type is what lets {@link reviewRow} and
670
+ * {@link telegramRow} share one implementation of those five without either
671
+ * side casting: a card supplies exactly these fields, and the compiler refuses
672
+ * a card that reaches for `budgets`, `fullPayload`, or anything else that only
673
+ * a pending question has.
674
+ */
675
+ export type ReviewCardFields = Pick<ChannelRequest, "action_key" | "class" | "task" | "summary"> & Partial<Pick<ChannelRequest, "command_breakdown" | "gloss">>;
676
+ /** The rows a review card renders, in the order it renders them (APRV-299). */
677
+ export declare const REVIEW_CARD_ROWS: readonly ["class", "command_breakdown", "task", "summary", "gloss"];
678
+ export type ReviewCardRow = (typeof REVIEW_CARD_ROWS)[number];
679
+ /**
680
+ * Build the two regions and the line list. Pure: no I/O, no clock.
681
+ *
682
+ * `heading` is the message's first line. It is a parameter for exactly one
683
+ * reason (APRV-115): a digest member's prompt carries no buttons, and telling
684
+ * a reader "APPROVAL REQUIRED" above a message they cannot answer on is the
685
+ * kind of small lie that costs a channel its legibility. Everything below the
686
+ * first line is identical either way, computed/claimed split included.
687
+ *
688
+ * `layout` is the policy's answer to which rows this channel shows (APRV-218).
689
+ * It defaults to {@link TELEGRAM_PROMPT_LAYOUT}, which is the slimmed prompt
690
+ * APRV-143 and APRV-163 left behind, so a policy that declares no
691
+ * `channels.telegram.prompt` renders byte for byte what it rendered before the
692
+ * key existed. Rendering stays a pure function of (request, layout): the layout
693
+ * chooses among facts the request already carries and teaches this channel
694
+ * nothing about the log.
695
+ *
696
+ * The computed/claimed split survives ANY ordering, and that is a property
697
+ * rather than a convention. `layout.order` decides the sequence rows are
698
+ * considered in; the partition below is by `Line.kind`, which comes from the
699
+ * `TaggedField` the row was built from. A `rows` list that puts `summary`
700
+ * first therefore puts it first among the CLAIMED lines, and never above the
701
+ * computed heading.
702
+ */
703
+ export declare function renderTelegram(request: ChannelRequest, heading?: string, layout?: PromptLayout): TelegramRendering;
704
+ /**
705
+ * Split `text` so every chunk survives HTML escaping inside the message limit.
706
+ *
707
+ * Splitting is by *escaped* length, because `&` becomes five characters and a
708
+ * payload full of them would otherwise produce a message Telegram rejects. The
709
+ * payload is never truncated to fit: the bytes a human is asked to approve are
710
+ * the bytes the token will execute, so an oversized payload becomes several
711
+ * messages, never a shortened one.
712
+ */
713
+ export declare function chunkForTelegram(text: string, budget?: number): string[];
714
+ /**
715
+ * Split an already-marked-up segment so every chunk is valid HTML on its own.
716
+ *
717
+ * {@link chunkForTelegram} may cut anywhere because its caller escapes each
718
+ * chunk and wraps it in `<pre>`; the claimed segment carries markup, so a cut
719
+ * inside `<b>` or inside `&amp;` would reach Telegram as a parse error, and a
720
+ * cut between an opening tag and its close would reach it as unbalanced HTML.
721
+ * Tags and entities are therefore atomic here, and the break is taken at the
722
+ * last line boundary in the chunk when there is one, which keeps each bullet
723
+ * whole and balanced. A bullet longer than the budget on its own (a rationale
724
+ * is unbounded agent text) splits inside its text, between tags, never within
725
+ * one — and it splits rather than being shortened, for the same reason a
726
+ * payload does.
727
+ */
728
+ export declare function chunkClaimedForTelegram(text: string, budget?: number): string[];
729
+ /**
730
+ * The shape token of a payload, for grouping.
731
+ *
732
+ * A shell command groups by its `argv[0]`, because that is what makes forty
733
+ * `network.call` prompts "the same question forty times" to the human reading
734
+ * them: forty `curl`s are one decision with forty URLs in it, and a `curl` next
735
+ * to an `rm` is not. Everything else groups by its top-level key set, which is
736
+ * the structural sense in which two payloads are the same shape.
737
+ *
738
+ * Structural, never self-declared: nothing here reads a `kind` or `type` field,
739
+ * for the reason `payload-view.ts` spells out — a field authored by the party
740
+ * under oversight must not choose how the party's requests are presented.
741
+ */
742
+ export declare function payloadShapeKey(value: unknown): string;
743
+ /**
744
+ * The grouping key: requests that share it are the same question asked twice.
745
+ *
746
+ * Signed off 2026-08-25 as (class, origin session/task, argv[0] or payload
747
+ * shape). The requesting actor rides along too, which can only ever SPLIT a
748
+ * group — two agents working the same task get two digests — and splitting is
749
+ * the safe direction: it costs a message and never merges two things a human
750
+ * would have wanted to weigh separately.
751
+ *
752
+ * `"\0"` as the separator because every component is agent-influenced text
753
+ * and a separator that can appear inside one would let a crafted task name
754
+ * collide two classes into one group. Written as the escape, never the raw
755
+ * byte: a literal NUL in the source turns this file into "binary" for grep,
756
+ * diff tooling, and editors, and the escape compiles to the same string.
757
+ */
758
+ export declare function digestKeyOf(request: ChannelRequest): string;
759
+ export declare function groupForDigest(requests: ChannelRequest[], max?: number): ChannelRequest[][];
760
+ /** One button on a digest keyboard. */
761
+ interface InlineButton {
762
+ text: string;
763
+ callback_data: string;
764
+ }
765
+ /** A digest member, as the delivering process remembers it. Never a decision. */
766
+ export interface DigestMemberState {
767
+ actionKey: string;
768
+ /** The nonce this member's own two buttons were issued under. */
769
+ nonce: string;
770
+ /** The agent's one-line description of the effect. Claimed. */
771
+ summary: string;
772
+ /** The agent's cost estimate, formatted. Claimed. */
773
+ cost: string;
774
+ /**
775
+ * The terminal outcome, once one has been observed for this member. Written
776
+ * only from a gate record or the verified log, never inferred here.
777
+ */
778
+ settled: {
779
+ headline: string;
780
+ detail: string[];
781
+ } | null;
782
+ }
783
+ /**
784
+ * The lines a COLLAPSED delivery leads with, and the fact it is one (APRV-287).
785
+ *
786
+ * A listener starting or reconnecting re-derives the pending set from the
787
+ * verified log and re-delivers it, which is right for a queue somebody is
788
+ * waiting on and was a flood for a queue nobody is: on 2026-09-06 a restarted
789
+ * daemon put a dozen requests whose hooks had long since given up in front of
790
+ * an approver, one message each. Those go out as ONE message instead, and this
791
+ * is what distinguishes it from an ordinary digest.
792
+ *
793
+ * It carries a REJECT-ALL button and deliberately no approve. The payloads are
794
+ * not in this message, and SPEC.md §10.3 requires the canonical rendering of a
795
+ * manual action's payload in front of the approver before a decision is
796
+ * collected: an approve-all here would collect a decision for bytes nobody was
797
+ * shown. A rejection authorizes nothing, so it needs no such showing, and every
798
+ * one of these requests can still be approved on its own card or from a
799
+ * terminal.
800
+ */
801
+ export interface StaleSummary {
802
+ /** Computed lines: how many, how old the oldest is, which classes. */
803
+ lines: string[];
804
+ }
805
+ /** One digest message, as the delivering process remembers it. */
806
+ export interface DigestState {
807
+ /** The digest message's own id: what every member's annotation edits. */
808
+ deliveryId: DeliveryId;
809
+ /** Shared by every member's decision event (SPEC.md §10.3). */
810
+ batchDeliveryId: DeliveryId;
811
+ /** The nonce the "all" buttons were issued under. */
812
+ allNonce: string;
813
+ /** The computed facts every member shares, already rendered as text. */
814
+ facts: {
815
+ label: string;
816
+ text: string;
817
+ origin: string;
818
+ }[];
819
+ /**
820
+ * The collapsed re-delivery this message is, or `null` for an ordinary digest
821
+ * (APRV-287). See {@link StaleSummary}.
822
+ */
823
+ stale?: StaleSummary | null;
824
+ /** Who authored the claimed lines below. */
825
+ author: string;
826
+ members: DigestMemberState[];
827
+ /**
828
+ * When this process delivered the digest, on {@link TelegramConfig.now}'s
829
+ * clock (APRV-135). Read by the sweep and by nothing else: it is never
830
+ * displayed, never compared against a log timestamp, and never a deadline —
831
+ * the request's own `ts` remains the only instant a TTL is measured from.
832
+ */
833
+ deliveredAtMs: number;
834
+ }
835
+ /**
836
+ * The computed facts a digest's members share, as the digest states them.
837
+ *
838
+ * Every one is read off the first member, which is sound precisely because the
839
+ * grouping key made them equal across the set: a digest whose members disagreed
840
+ * about their class or their task is a digest the listener would not have
841
+ * built. The last line is the one an approver needs most — it says how many
842
+ * payloads are above and that each request has its own.
843
+ */
844
+ export declare function digestFacts(members: ChannelRequest[]): {
845
+ label: string;
846
+ text: string;
847
+ origin: string;
848
+ }[];
849
+ /**
850
+ * The digest message: text plus the keyboard for whatever is still open.
851
+ *
852
+ * Pure. The computed/claimed split of an ordinary prompt is kept — the shared
853
+ * facts are computed and sit under a heading that says so, the per-member lines
854
+ * are the agent's own words and sit under one that says they are not verified —
855
+ * because a digest is a prompt, and SPEC.md §9 does not stop applying because
856
+ * there are five of them.
857
+ *
858
+ * A settled member keeps its line, gains its outcome underneath, and loses its
859
+ * buttons. The "all" row appears only while two or more members are open: with
860
+ * one left, "all" is the same tap as its own Approve and a second way to do one
861
+ * thing is a way to do the wrong one.
862
+ */
863
+ export declare function renderDigest(digest: DigestState): {
864
+ text: string;
865
+ keyboard: {
866
+ inline_keyboard: InlineButton[][];
867
+ } | null;
868
+ };
869
+ /**
870
+ * The stable short reference to an action key that a button carries (APRV-196).
871
+ *
872
+ * The first {@link ACTION_REF_HEX} hex characters of the key's sha256. Two
873
+ * properties earn it its place, and they are the two the old scheme lacked:
874
+ *
875
+ * 1. **It always fits.** `<verb>:<nonce>:<ref>` is well inside Telegram's
876
+ * 64-byte cap for any nonce this class issues, so the cross-check that used
877
+ * to be dropped for a long action key is now always present.
878
+ * 2. **It survives a restart.** The nonce is per-process and per-copy; the ref
879
+ * is a function of the action key alone, so two copies of the same request
880
+ * delivered by two different listener processes carry the same ref. That is
881
+ * what lets a tap on a pre-restart copy resolve to the request the current
882
+ * process is holding, instead of dying as an unknown nonce.
883
+ *
884
+ * It is a REFERENCE and never an authorization. The bytes come back from the
885
+ * network, so a ref is only ever matched against deliveries THIS process made
886
+ * (and only from the configured chat); it can select among what the listener
887
+ * has itself put in front of the approver, and it can name nothing else.
888
+ */
889
+ export declare const ACTION_REF_HEX = 16;
890
+ export declare function actionRefOf(actionKey: string): string;
891
+ /**
892
+ * `callback_data` for one button: `<g|r>:<nonce>:<action ref>`.
893
+ *
894
+ * The **nonce is authoritative** where it resolves: it is issued by this process
895
+ * at `notify` and maps to the request that was actually delivered, so an
896
+ * ordinary tap never consults the ref for anything but a cross-check (a
897
+ * mismatch is an anomaly and the callback is dropped). The ref is the fallback
898
+ * for the copy whose nonce this process never issued, and {@link actionRefOf}
899
+ * states the bound on what that fallback may reach.
900
+ */
901
+ export declare function callbackData(verb: "g" | "r", nonce: string, actionKey: string): string;
902
+ /**
903
+ * `callback_data` for a digest's "all" button: `<G|R>:<nonce>` (APRV-115).
904
+ *
905
+ * Upper case, and no action key: an "all" button names a *delivery*, and the
906
+ * set it decides is whichever members of that delivery are still open at the
907
+ * moment of the tap — which the delivering process knows and the network does
908
+ * not. Naming keys in the bytes would let something that can reach the bot
909
+ * choose the set, and there is no length at which that becomes acceptable.
910
+ */
911
+ export declare function digestCallbackData(verb: "G" | "R", nonce: string): string;
912
+ /**
913
+ * `callback_data` for the checkpoint prompt's two buttons (APRV-257).
914
+ *
915
+ * `k:<nonce>` signs, `x:<nonce>` declines. Verbs of their own rather than a
916
+ * reuse of `g`/`r`, and the separation is load-bearing: {@link CALLBACK_VERBS}
917
+ * maps every decision verb onto a grant or a reject, so a checkpoint button
918
+ * spelled `g` would be a button {@link parseCallbackData} hands to the decision
919
+ * path — where an unknown nonce becomes an action-reference lookup, and a
920
+ * signature gesture starts hunting for a request to approve. Two vocabularies,
921
+ * two parsers, and neither can be read as the other.
922
+ *
923
+ * No action key and no reference in the bytes: a checkpoint names no request,
924
+ * and the head it covers is held by the process that issued the nonce, exactly
925
+ * as a digest's member set is. Nothing that can reach the bot chooses what gets
926
+ * signed.
927
+ */
928
+ export declare function checkpointCallbackData(verb: "k" | "x", nonce: string): string;
929
+ /** `k:<nonce>` / `x:<nonce>`, or `null` for anything else. Never throws. */
930
+ export declare function parseCheckpointCallback(data: unknown): {
931
+ sign: boolean;
932
+ nonce: string;
933
+ } | null;
934
+ /**
935
+ * The account the Bot API attributes a callback to (APRV-324, amended SPEC.md
936
+ * §10.3).
937
+ *
938
+ * `callback_query.from.id` and nothing else. Telegram assembles the `from`
939
+ * object itself, from the session the tap arrived on, which is why it is the
940
+ * one field on an update this channel treats as evidence about a person. Three
941
+ * neighbours are deliberately not read:
942
+ *
943
+ * - **`from.username`.** Mutable and reusable, so a mapping keyed on it would
944
+ * hand an identity over with a handle. Never a key here and never recorded
945
+ * (`core/sender-identity.ts` states the argument in full).
946
+ * - **`message.from`.** The author of the message the button sits on — this bot
947
+ * — rather than the person who pressed it.
948
+ * - **`data`, and any text.** What the sender says about themselves, which
949
+ * §11.1 invariant 4 says may raise scrutiny and never lower it. A callback
950
+ * whose payload names a user id names it about itself; the id here comes from
951
+ * the transport, and the two disagreeing changes nothing.
952
+ *
953
+ * `undefined` when the update carries no usable id: a sender this channel could
954
+ * not read is not a sender it may guess at, and the decision then travels with
955
+ * none, which the contract reads as today's configured attribution.
956
+ */
957
+ export declare function senderOf(query: Record<string, unknown>, channel: string): ChannelSender | undefined;
958
+ interface ParsedCallback {
959
+ decision: "grant" | "reject";
960
+ /** `all` for a digest's "all" button; `one` for every per-request button. */
961
+ scope: "one" | "all";
962
+ nonce: string;
963
+ /** {@link actionRefOf} of the action this button was drawn for, when present. */
964
+ actionRef: string | null;
965
+ }
966
+ export declare function parseCallbackData(data: unknown): ParsedCallback | null;
967
+ /**
968
+ * The headline of a retrospective review card.
969
+ *
970
+ * Deliberately not {@link TELEGRAM_PROMPT_HEADING} and deliberately not a
971
+ * question. A sample is an action that ALREADY RAN: nothing is pending, no
972
+ * token is minted by any button on this message, and a card that said
973
+ * "APPROVAL REQUIRED" would be telling the approver they are holding something
974
+ * up. The supervised bargain (SPEC.md §5.2) is "execute now, a fraction is
975
+ * reviewed after", and this is the "after".
976
+ */
977
+ export declare const TELEGRAM_REVIEW_HEADING = "REVIEW \u2014 THIS ALREADY RAN";
978
+ /** The headline a recorded review puts on the card it settles. */
979
+ export declare const TELEGRAM_REVIEW_RECORDED = "\u2713 REVIEWED";
980
+ /** The headline a recorded DENIAL puts on the card it settles. */
981
+ export declare const TELEGRAM_REVIEW_DENIED = "\u2717 REVIEWED \u2014 DENIED";
982
+ /**
983
+ * The headline a card wears while a first Deny tap is armed and nothing has
984
+ * been recorded.
985
+ */
986
+ export declare const TELEGRAM_REVIEW_ARMED = "DENY ARMED \u2014 nothing is recorded yet";
987
+ /**
988
+ * What a review tap's single answer says (APRV-302).
989
+ *
990
+ * {@link TELEGRAM_ACK_HEARD}'s "deciding" is a request card's word: something is
991
+ * pending, and the tap just settled it. A review decides nothing — the action
992
+ * ran, and what the tap does is record what a person thought of it — so a
993
+ * reviewer told they were "deciding" is being told the wrong thing about the
994
+ * card in front of them. The load-bearing half is carried over unchanged: this
995
+ * claims only that the tap ARRIVED, never that anything was appended, because at
996
+ * the moment it is sent nothing has been and `core/audit.ts` may still refuse.
997
+ * What became of it is on the card edit that follows.
998
+ */
999
+ export declare const TELEGRAM_REVIEW_ACK = "Heard \u2014 recording your review. The card will say what the log recorded.";
1000
+ /** The toast a first Deny tap gets: it says plainly that nothing was written. */
1001
+ export declare const TELEGRAM_REVIEW_ARM_TOAST = "Deny armed \u2014 nothing recorded. Tap Deny again to record it, or a reaction to record it with a grade.";
1002
+ /** The toast a reaction that needs the human's own words gets. */
1003
+ export declare const TELEGRAM_REVIEW_NOTE_TOAST = "Heard \u2014 reply to the prompt with why. Nothing is recorded until it arrives.";
1004
+ /**
1005
+ * What the ForceReply prompt asks for.
1006
+ *
1007
+ * A separate message rather than a second keyboard, because Telegram's inline
1008
+ * keyboards have no text input at all — the same limitation the reject path
1009
+ * documents. The prompt is bound to its card by the message id the reply names,
1010
+ * which this process holds and the network does not.
1011
+ */
1012
+ export declare function reviewNotePromptLines(reaction: Reaction, verdict: ReviewVerdict, actionKey: string): string[];
1013
+ /**
1014
+ * What a tap on a review card asks the runtime to record (APRV-299).
1015
+ *
1016
+ * The channel decides none of it. It reports which sample the card was drawn
1017
+ * for, which verdict the taps add up to, the grade if one was given, and the
1018
+ * human's words if a note prompt collected any — and the runtime's handler
1019
+ * calls the human-only `reviewSample`, exactly as {@link ChannelDecision} goes
1020
+ * to the human-only `decide()`. The actor is NOT here, for the reason it is not
1021
+ * on a decision either: it is the identity the LISTENER was configured with,
1022
+ * never a field that arrived from the network.
1023
+ */
1024
+ export interface ReviewTap {
1025
+ /** `seq` of the `audit.sampled` record this card was drawn for. */
1026
+ sampleSeq: number;
1027
+ verdict: ReviewVerdict;
1028
+ /** The grade, when the human gave one. Absent means absent. */
1029
+ reaction?: Reaction;
1030
+ /** The human's words, when a note prompt collected any. */
1031
+ note?: string;
1032
+ /**
1033
+ * The account the transport authenticated (APRV-324 follow-up), when it
1034
+ * authenticated one.
1035
+ *
1036
+ * A review confers no authority, which is why it took the paragraph above so
1037
+ * long to stop being true. It is still recorded as a HUMAN's observation and
1038
+ * `approval feedback` presents it to agents as human-authored guidance, so a
1039
+ * review attributed to the wrong person is guidance in somebody else's name.
1040
+ * The channel resolves nothing; the runtime's handler asks the attested
1041
+ * policy, refuses an account it does not name, and records the one it does.
1042
+ */
1043
+ sender?: ChannelSender;
1044
+ }
1045
+ /** What the runtime did with a review tap, as it reports it back. */
1046
+ export interface ReviewTapResponse {
1047
+ /** Whether an `audit.reviewed` landed. */
1048
+ ok: boolean;
1049
+ /** The headline for the edited card. */
1050
+ headline: string;
1051
+ /** The lines under it: the record, or the refusal code and its message. */
1052
+ detail: string[];
1053
+ /** The toast, which Telegram caps at a short sentence. */
1054
+ toast: string;
1055
+ }
1056
+ export type ReviewTapHandler = (tap: ReviewTap) => ReviewTapResponse | Promise<ReviewTapResponse>;
1057
+ /**
1058
+ * One retrospective review card, as the runtime hands it over (APRV-299).
1059
+ *
1060
+ * Everything here is derived by the runtime from the verified log, the policy
1061
+ * and the payload store; the channel adds the buttons and nothing else. The
1062
+ * computed/claimed split of SPEC.md §9 is carried by the fields themselves, so
1063
+ * a card cannot render a claimed summary with a computed line's authority any
1064
+ * more than a prompt can.
1065
+ */
1066
+ export interface ReviewCard {
1067
+ /** `seq` of the `audit.sampled` record. What a review names. */
1068
+ sampleSeq: number;
1069
+ /** The rows a request prompt renders identically. */
1070
+ fields: ReviewCardFields;
1071
+ /** Computed: when the sampled execution started, and which record says so. */
1072
+ ranAt: TaggedField<string>;
1073
+ /**
1074
+ * The same instant, machine-readable.
1075
+ *
1076
+ * Not displayed and not a {@link TaggedField} for that reason: it exists so
1077
+ * that the runtime's "oldest awaiting review" arithmetic reads an instant off
1078
+ * the log rather than parsing the sentence {@link ReviewCard.ranAt} renders.
1079
+ * A display string is written for a person and is free to be reworded; a
1080
+ * number a summary is computed from is not.
1081
+ */
1082
+ ranAtTs: string;
1083
+ /** Computed: what the runtime did at the time, and why this is being reviewed. */
1084
+ verdict: TaggedField<string>;
1085
+ }
1086
+ /** A review card, as the delivering process remembers it. Never a decision. */
1087
+ export interface ReviewCardState {
1088
+ deliveryId: DeliveryId;
1089
+ card: ReviewCard;
1090
+ /** The nonce every button on this card was issued under. */
1091
+ nonce: string;
1092
+ /**
1093
+ * Whether a first Deny tap has armed the card. **Process memory**, exactly
1094
+ * like the digest bookkeeping: it appends nothing, it is stated on the card
1095
+ * so the human can see it, and losing it to a restart costs a tap and can
1096
+ * never cost a denial nobody meant. A card whose arming is lost is a card
1097
+ * whose next Deny tap arms again.
1098
+ */
1099
+ denyArmed: boolean;
1100
+ /**
1101
+ * The outcome, once the runtime has recorded one. Written only from the
1102
+ * handler's answer, never inferred here.
1103
+ */
1104
+ settled: {
1105
+ headline: string;
1106
+ detail: string[];
1107
+ } | null;
1108
+ /**
1109
+ * What the last tap produced without settling anything: the arming, or a
1110
+ * refusal the runtime returned. Rendered under the rows so the card keeps
1111
+ * saying what it is about.
1112
+ */
1113
+ notice: {
1114
+ headline: string;
1115
+ lines: string[];
1116
+ } | null;
1117
+ /**
1118
+ * The outstanding note prompt, and what its reply will record.
1119
+ *
1120
+ * `sender` is the account that asked for the prompt (APRV-324 follow-up),
1121
+ * when the transport authenticated one. A ForceReply prompt is addressed to
1122
+ * the person who tapped, and the words it collects are recorded as theirs, so
1123
+ * a reply from a different account is not the answer to this question: it is
1124
+ * left unrecorded and the prompt stays live for whoever armed it.
1125
+ */
1126
+ awaitingNote: {
1127
+ promptId: DeliveryId;
1128
+ verdict: ReviewVerdict;
1129
+ reaction: Reaction;
1130
+ sender?: ChannelSender;
1131
+ } | null;
1132
+ /** When this process delivered the card, on {@link TelegramConfig.now}'s clock. */
1133
+ deliveredAtMs: number;
1134
+ }
1135
+ /**
1136
+ * The six things a review card's buttons can say (APRV-299).
1137
+ *
1138
+ * `ok` and `deny` are the verdict, which is enforcement; the four reactions are
1139
+ * the grade, which is not (SPEC.md §11.1 invariant 10). Both travel in the same
1140
+ * closed vocabulary because they arrive through the same six buttons, and a
1141
+ * seventh word would be a button nobody drew.
1142
+ */
1143
+ export declare const REVIEW_CHOICES: readonly ["ok", "deny", "disliked", "indifferent", "liked", "loved"];
1144
+ export type ReviewChoice = (typeof REVIEW_CHOICES)[number];
1145
+ /**
1146
+ * `callback_data` for one review button: `v:<nonce>:<choice>`.
1147
+ *
1148
+ * Its own verb, for exactly the reason the checkpoint prompt's is its own
1149
+ * (APRV-257): {@link CALLBACK_VERBS} maps every DECISION verb onto a grant or a
1150
+ * reject, so a review button spelled `g` would be handed to the decision path,
1151
+ * where an unresolved nonce falls back to an action-reference lookup and a
1152
+ * gesture about something that already happened would start hunting for a
1153
+ * request to approve. Three vocabularies, three parsers, and none can be read
1154
+ * as another.
1155
+ *
1156
+ * No action reference in the bytes, and no sample seq: the card names a
1157
+ * DELIVERY, and which sample that delivery is about is held by the process that
1158
+ * issued the nonce. Nothing that can reach the bot chooses what gets reviewed.
1159
+ * There is also no stale-copy ladder underneath it: a review is never urgent,
1160
+ * a lost card leaves the sample open, and the next cycle offers it again.
1161
+ */
1162
+ export declare function reviewCallbackData(choice: ReviewChoice, nonce: string): string;
1163
+ /** `v:<nonce>:<choice>`, or `null` for anything else. Never throws. */
1164
+ export declare function parseReviewCallback(data: unknown): {
1165
+ nonce: string;
1166
+ choice: ReviewChoice;
1167
+ } | null;
1168
+ /**
1169
+ * The card's message: the rows, whatever notice the last tap produced, and the
1170
+ * keyboard.
1171
+ *
1172
+ * No paragraph explaining the buttons (APRV-302). The heading
1173
+ * ({@link TELEGRAM_REVIEW_HEADING}) is what says a review is not a request, and
1174
+ * the deny latch says itself: the first tap is answered by
1175
+ * {@link TELEGRAM_REVIEW_ARM_TOAST} and the card's own heading becomes
1176
+ * {@link TELEGRAM_REVIEW_ARMED} until it is spent. Four sentences of rules under
1177
+ * every card said the same thing to a reader who had already read them once, and
1178
+ * pushed the rows a review is actually about off the first screen.
1179
+ *
1180
+ * Pure. Two things it deliberately does NOT carry, and both are the same rule
1181
+ * read twice: no payload region, and no approve button. SPEC.md §10.3 requires
1182
+ * the canonical rendering in front of an approver before a DECISION is
1183
+ * collected, and this collects none — the action ran, the review says only what
1184
+ * a person thought of it, and a card that offered an approve would be
1185
+ * presenting a settled fact as a live authorization. A sample is never
1186
+ * delivered as an approval request and never accepts a token.
1187
+ */
1188
+ export declare function renderReviewCard(state: ReviewCardState): {
1189
+ text: string;
1190
+ keyboard: {
1191
+ inline_keyboard: InlineButton[][];
1192
+ } | null;
1193
+ };
1194
+ /** A Bot API call that did not produce a usable result. */
1195
+ export declare class TelegramApiError extends Error {
1196
+ readonly method: string;
1197
+ /**
1198
+ * The HTTP status, when the failure was an HTTP one. `null` for a transport
1199
+ * failure, an unparseable body, or an `ok: false` envelope that arrived
1200
+ * with a 200 (APRV-277).
1201
+ */
1202
+ readonly status: number | null;
1203
+ /**
1204
+ * The Bot API's own `description` for this failure, redacted, when the
1205
+ * error body carried one. `null` when the body was absent, unreadable, not
1206
+ * JSON, or carried no description.
1207
+ */
1208
+ readonly description: string | null;
1209
+ constructor(message: string, method: string,
1210
+ /**
1211
+ * The HTTP status, when the failure was an HTTP one. `null` for a transport
1212
+ * failure, an unparseable body, or an `ok: false` envelope that arrived
1213
+ * with a 200 (APRV-277).
1214
+ */
1215
+ status?: number | null,
1216
+ /**
1217
+ * The Bot API's own `description` for this failure, redacted, when the
1218
+ * error body carried one. `null` when the body was absent, unreadable, not
1219
+ * JSON, or carried no description.
1220
+ */
1221
+ description?: string | null);
1222
+ }
1223
+ /**
1224
+ * Whether a failed call is the Bot API saying an edit changed nothing
1225
+ * (APRV-277).
1226
+ *
1227
+ * `editMessageText` answers 400 "Bad Request: message is not modified" when the
1228
+ * text and the keyboard it was handed are already what the message holds. Every
1229
+ * caller here re-annotates from the verified log rather than from memory, so a
1230
+ * message annotated once and derived again produces exactly that: the phone
1231
+ * already shows the outcome, and the operator has nothing to be told. It is the
1232
+ * one 400 that means the intended state stands, which is why it is the only one
1233
+ * that goes unreported.
1234
+ */
1235
+ export declare function isMessageNotModified(cause: unknown): boolean;
1236
+ /**
1237
+ * Whether a failed poll is the Bot API saying another process holds this bot
1238
+ * (APRV-390).
1239
+ *
1240
+ * It is the one poll failure that is not transient and not the network's
1241
+ * fault: retrying it forever produces an identical line every few seconds and
1242
+ * never recovers, because the other poller is not going to stop. {@link
1243
+ * TelegramChannel.listen} reports it once, with whatever the caller knows
1244
+ * about who the other process is, and then stops repeating itself.
1245
+ */
1246
+ export declare function isPollConflict(cause: unknown): boolean;
1247
+ /** What one `pollOnce()` did, for tests and for programmatic drivers. */
1248
+ export interface TelegramPollResult {
1249
+ /** Updates received in this batch. */
1250
+ updates: number;
1251
+ /** Decisions the runtime recorded from this batch, in order. */
1252
+ outcomes: {
1253
+ action_key: string;
1254
+ outcome: DecisionOutcome;
1255
+ }[];
1256
+ /** Callbacks ignored in this batch, with the reason. */
1257
+ ignored: {
1258
+ kind: TelegramAnomalyKind;
1259
+ detail: string;
1260
+ }[];
1261
+ /** Bot commands handed to the runtime in this batch, in order (APRV-216). */
1262
+ commands: TelegramCommand[];
1263
+ /**
1264
+ * Review taps handed to the runtime in this batch, in order (APRV-299), each
1265
+ * with whether an `audit.reviewed` landed. `ok: false` is a refusal the
1266
+ * runtime returned — the card says which code — and nothing was appended.
1267
+ */
1268
+ reviews: {
1269
+ tap: ReviewTap;
1270
+ ok: boolean;
1271
+ }[];
1272
+ }
1273
+ export interface TelegramListenOptions {
1274
+ /** Process exactly one successful `getUpdates` batch, then return. */
1275
+ once?: boolean;
1276
+ /**
1277
+ * Run before every `getUpdates`, including the first and including the poll
1278
+ * that follows a recovered poll error (APRV-55).
1279
+ *
1280
+ * This is how the runtime gets a dispatch cycle without the channel growing
1281
+ * an opinion about what is pending: the callback belongs to
1282
+ * `cli/channel-telegram.ts`, which re-derives the pending queue from the
1283
+ * verified log and sends what it has not sent yet. The channel neither reads
1284
+ * the log nor remembers a queue, so nothing here makes it stateful.
1285
+ *
1286
+ * It MUST NOT throw. A rejection is treated exactly like a poll failure
1287
+ * (counted, complained about, retried after backoff) rather than being
1288
+ * allowed to end the loop, because a listener that stops listening is the
1289
+ * failure mode this loop exists to rule out.
1290
+ */
1291
+ beforePoll?: () => Promise<void>;
1292
+ /**
1293
+ * What the caller knows about who else might be polling this bot (APRV-390).
1294
+ *
1295
+ * Consulted only when a poll fails with the Bot API's 409, and appended to
1296
+ * the one line that failure produces. The CLI builds it from the per-machine
1297
+ * ownership registry (`core/channel-owner.ts`), so this class keeps no
1298
+ * knowledge of instances, directories or files — it asks the question and
1299
+ * prints the caller's answer, the same arrangement {@link
1300
+ * TelegramConfig.describeAction} uses for the log.
1301
+ */
1302
+ conflictAdvice?: () => string | null;
1303
+ }
1304
+ /**
1305
+ * What {@link TelegramChannel.notifyBatch} did with a set (APRV-115).
1306
+ *
1307
+ * `digestId` is `null` when the set was delivered the old way, one message per
1308
+ * member — the fallback every "cannot render this whole" path takes. `members`
1309
+ * carries the message id each member's annotation must edit, which for a digest
1310
+ * is the one digest message and for the fallback is the member's own.
1311
+ */
1312
+ export interface TelegramBatchDelivery {
1313
+ batchDeliveryId: DeliveryId;
1314
+ digestId: DeliveryId | null;
1315
+ members: {
1316
+ action_key: string;
1317
+ delivery_id: DeliveryId;
1318
+ }[];
1319
+ rendered: RenderedRequest[];
1320
+ }
1321
+ export declare class TelegramChannel implements TestableChannel {
1322
+ readonly name = "telegram";
1323
+ private readonly token;
1324
+ private readonly chatId;
1325
+ private readonly apiBase;
1326
+ private readonly fetchImpl;
1327
+ private readonly pollTimeoutSeconds;
1328
+ private readonly requestTimeoutMs;
1329
+ private readonly backoffMs;
1330
+ private readonly maxBackoffMs;
1331
+ private readonly complain;
1332
+ private readonly makeNonce;
1333
+ /** The policy's approval TTL, or `null` when it declares none (APRV-135). */
1334
+ private readonly approvalTtlMs;
1335
+ private readonly now;
1336
+ /** The listener's verified-log probe for a stale tap (APRV-196), or null. */
1337
+ private readonly describeAction;
1338
+ /** The policy's row layout for this channel (APRV-218). Read-only, and pure input to the renderer. */
1339
+ private readonly layout;
1340
+ /** When {@link sweep} last ran, so the poll loop can call it every cycle. */
1341
+ private lastSweepMs;
1342
+ /**
1343
+ * The callback query being handled, and whether an ack has been attempted for
1344
+ * it (APRV-196). Set and cleared by {@link handleUpdate}, which processes
1345
+ * updates one at a time and awaits each.
1346
+ */
1347
+ private ack;
1348
+ private handler;
1349
+ /**
1350
+ * What to do with a bot command (APRV-216). Absent unless the runtime asked
1351
+ * for commands, and its absence is what keeps `message` out of
1352
+ * `allowed_updates` — see {@link onCommand}.
1353
+ */
1354
+ private commandHandler;
1355
+ /**
1356
+ * What to do with a checkpoint tap (APRV-257). Absent unless the runtime
1357
+ * registered one, and its absence makes {@link offerCheckpoint} refuse: a
1358
+ * button nobody is listening for is a button that spins on a phone.
1359
+ */
1360
+ private checkpointHandler;
1361
+ /**
1362
+ * Checkpoint nonce -> the head that prompt asked about, and the message it is
1363
+ * on. **In memory only**, like every other map in this class and for the same
1364
+ * reason (SPEC.md §10.3: channels hold no state that is a source of truth).
1365
+ *
1366
+ * The head lives HERE and not in the callback bytes, so what is signed is
1367
+ * what this process put on the screen. Losing the map to a restart costs a
1368
+ * tap its meaning — the button answers `unknown-callback` and the listener
1369
+ * offers again on its next lapse — and can never cost a signature over
1370
+ * something nobody was shown.
1371
+ */
1372
+ private readonly checkpointNonces;
1373
+ /**
1374
+ * What to do with a review tap (APRV-299). Absent unless the runtime
1375
+ * registered one, and its absence makes {@link offerReview} refuse, for the
1376
+ * reason {@link offerCheckpoint} refuses: a button nobody is listening for is
1377
+ * a button that spins on a phone.
1378
+ */
1379
+ private reviewHandler;
1380
+ /**
1381
+ * Review card message id -> what is on it. Delivery bookkeeping, never truth
1382
+ * (SPEC.md §10.3). Losing it to a restart costs the card its buttons; the
1383
+ * sample stays open in the log, `approval audit list` still names it, and the
1384
+ * next cycle offers a fresh card.
1385
+ */
1386
+ private readonly reviewCards;
1387
+ /** Review nonce -> the card message it was issued for. */
1388
+ private readonly reviewNonces;
1389
+ /** Note-prompt message id -> the card whose reply it is waiting for. */
1390
+ private readonly reviewNotePrompts;
1391
+ private readonly deliveries;
1392
+ /** Digest message id -> what is on it. Delivery bookkeeping, never truth. */
1393
+ private readonly digests;
1394
+ /** "All" nonce -> the digest message it was issued for. */
1395
+ private readonly allNonces;
1396
+ private rendered;
1397
+ private offset;
1398
+ private counter;
1399
+ private stopped;
1400
+ private inFlight;
1401
+ private readonly counters;
1402
+ constructor(config: TelegramConfig);
1403
+ onDecision(handler: (decision: ChannelDecision) => DecisionOutcome): void;
1404
+ /**
1405
+ * Register what to do with a checkpoint tap (APRV-257).
1406
+ *
1407
+ * The handler is the runtime's, on the runtime's side of the boundary, and it
1408
+ * is where the vault passphrase and the signing live. This channel holds a
1409
+ * nonce, a message id and a `(seq, hash)`, and hands the head back when the
1410
+ * button is pressed — the same shape as {@link onDecision}, for the same
1411
+ * reason: a channel that signed anything would be a channel with authority.
1412
+ */
1413
+ onCheckpoint(handler: CheckpointTapHandler): void;
1414
+ /**
1415
+ * Put one `CHECKPOINT DUE` prompt in the chat, with a Sign and a Not now
1416
+ * button (APRV-257).
1417
+ *
1418
+ * A unit like any other: the paced walkthrough sends it as one thing to read,
1419
+ * and it is never grouped into a digest, because a digest is a set of
1420
+ * SIMILAR REQUESTS decided together and a checkpoint is neither a request nor
1421
+ * similar to one.
1422
+ *
1423
+ * Refuses when no handler is registered, rather than sending a dead button.
1424
+ */
1425
+ offerCheckpoint(prompt: CheckpointPrompt): Promise<DeliveryId>;
1426
+ /**
1427
+ * Register what to do with a review tap (APRV-299).
1428
+ *
1429
+ * The handler is the runtime's, on the runtime's side of the boundary, and it
1430
+ * is where the human-only `reviewSample` lives — same shape as
1431
+ * {@link onDecision} and {@link onCheckpoint}, for the same reason: a channel
1432
+ * that appended an `audit.reviewed` of its own would be a supervision backlog
1433
+ * emptying itself through its own transport.
1434
+ *
1435
+ * Registering it, like registering a command handler, is what makes this
1436
+ * channel read `message` updates at all: the note a `loved` or `disliked`
1437
+ * asks for arrives as a reply, and an inline keyboard has no text input.
1438
+ */
1439
+ onReview(handler: ReviewTapHandler): void;
1440
+ /**
1441
+ * Put one retrospective review card in the chat (APRV-299).
1442
+ *
1443
+ * A unit like a checkpoint prompt: one thing to read, never grouped into a
1444
+ * digest, and never delivered through {@link notify} — a digest is a set of
1445
+ * similar pending REQUESTS decided together, and a sample is neither pending
1446
+ * nor a request. It sends ONE message: no payload region, and a keyboard
1447
+ * whose six buttons collect a verdict and a grade and mint nothing.
1448
+ *
1449
+ * Refuses when no handler is registered, rather than sending a dead button.
1450
+ */
1451
+ offerReview(card: ReviewCard): Promise<DeliveryId>;
1452
+ /**
1453
+ * Register what to do with `/queue`, `/skip` and `/next` (APRV-216).
1454
+ *
1455
+ * **Registering is what makes this channel read messages at all.** Until a
1456
+ * handler is here, `getUpdates` asks for `callback_query` only, exactly as it
1457
+ * did before this task, so a listener in `burst` delivery consumes no message
1458
+ * updates — which matters because `approval setup channel telegram`
1459
+ * discovers the approver chat by reading one (APRV-74), and a listener that
1460
+ * swallowed it would break the bootstrap of the very channel it runs on.
1461
+ *
1462
+ * The handler owns whatever answer the human gets. This class sends nothing
1463
+ * of its own for a command: it holds no queue to summarise (SPEC.md §10.3),
1464
+ * so the sentence a command produces is written where the pending set is
1465
+ * re-derived, in `cli/channel-telegram.ts`.
1466
+ */
1467
+ onCommand(handler: (command: TelegramCommand) => Promise<void> | void): void;
1468
+ health(): ChannelHealth;
1469
+ /** The rendering split of the most recent `notify`, for the conformance suite. */
1470
+ lastRendered(): RenderedRequest[];
1471
+ /** Delivery, decision and anomaly counters. Live; read from anywhere. */
1472
+ stats(): TelegramStats;
1473
+ /** Ignored callbacks so far. Exposed for `health()` and for operators. */
1474
+ anomalyCount(kind?: TelegramAnomalyKind): number;
1475
+ /**
1476
+ * Put a request, or a set of them, in front of the approver.
1477
+ *
1478
+ * One request is one prompt: its header, its payload chunks, and the
1479
+ * Approve/Reject keyboard on the last message, whose `message_id` is the
1480
+ * delivery id. A {@link ChannelBatch} goes through {@link notifyBatch} and
1481
+ * comes back as a digest when it can be one; either way it gets one shared
1482
+ * batch delivery id, which is what this returns and what every resulting
1483
+ * event will carry.
1484
+ */
1485
+ notify(target: ChannelRequest | ChannelBatch): Promise<DeliveryId>;
1486
+ /**
1487
+ * Deliver a set as one digest, or as one message per member when it cannot
1488
+ * be one (APRV-115).
1489
+ *
1490
+ * The fallback is taken for a set of fewer than two, and for one whose digest
1491
+ * text would not fit inside {@link TELEGRAM_MAX_MESSAGE_CHARS}. Both are the
1492
+ * same rule: the approver sees every member before any button that decides
1493
+ * more than one appears, and when that cannot be arranged the channel sends
1494
+ * MORE messages rather than fewer.
1495
+ *
1496
+ * Not atomic, and it cannot be: a `sendMessage` that fails part way leaves
1497
+ * the messages already sent in the chat, and this throws. Nothing is armed —
1498
+ * the member nonces are registered only once the digest message carrying
1499
+ * their buttons exists — so the caller's retry re-sends the set and the
1500
+ * approver gets a duplicate prompt, never a live button on a half-sent one.
1501
+ */
1502
+ notifyBatch(batch: ChannelBatch): Promise<TelegramBatchDelivery>;
1503
+ /**
1504
+ * Deliver a set of stale pending requests as ONE message with a reject-all
1505
+ * button (APRV-287).
1506
+ *
1507
+ * Returns `null` when the message would not fit, and the caller then leaves
1508
+ * the members undelivered so the next cycle shows them the ordinary way:
1509
+ * SPEC.md §10.3's rule for this bookkeeping is that losing it degrades to
1510
+ * showing a request again, never to a pending request nobody is shown.
1511
+ */
1512
+ notifyStale(members: ChannelRequest[], stale: StaleSummary): Promise<TelegramBatchDelivery | null>;
1513
+ /**
1514
+ * The digest itself: every member's prompt and payload, then the one message
1515
+ * that carries the buttons.
1516
+ *
1517
+ * Returns `null` when the digest message would not fit, so the caller falls
1518
+ * back — and it decides that BEFORE sending anything, because a fallback
1519
+ * discovered after four member prompts had gone out would double them.
1520
+ *
1521
+ * `stale` (APRV-287) makes it the collapsed re-delivery instead: no member
1522
+ * prompts, no payload, one reject-all button. See {@link StaleSummary}.
1523
+ */
1524
+ private deliverDigest;
1525
+ /**
1526
+ * Send one request's messages: the computed header, the payload chunks, then
1527
+ * the claimed block, with `keyboard` (when there is one) on the last.
1528
+ *
1529
+ * The claimed block goes last because it is the human-meaningful description
1530
+ * of the act, and the message a reader answers on should be the one that says
1531
+ * what they are answering about; bookkeeping above it is context, not the
1532
+ * question. SPEC §10.3 allows claimed material to sit around the canonical
1533
+ * block while it stays visibly separated and labelled, which the heading on
1534
+ * every claimed message keeps. It is always sent, so a missing summary is a
1535
+ * visible "(none given)" rather than an absent message, and so the keyboard
1536
+ * has one message it can always ride on.
1537
+ *
1538
+ * Shared by the ordinary prompt and by a digest member, which differ in
1539
+ * exactly two things: the heading, and whether anything is armed.
1540
+ */
1541
+ private sendPrompt;
1542
+ private deliverOne;
1543
+ /**
1544
+ * Forget every nonce issued for `deliveryId`, and report the action key it
1545
+ * was issued for.
1546
+ *
1547
+ * Called by {@link annotate} before the edit goes out, so a tap on a button
1548
+ * the edit does not manage to remove resolves to nothing and is answered as
1549
+ * a `stale-copy` rather than carried to the gate as a decision attempt.
1550
+ * Forgetting is never the channel growing state, and forgetting a SETTLED
1551
+ * request is what stops APRV-196's action-reference fallback from finding it:
1552
+ * the ladder rescues a tap on an old copy of a request still open here, and
1553
+ * a decided one is not that.
1554
+ */
1555
+ private disarm;
1556
+ /**
1557
+ * Drop the delivery bookkeeping no callback can still be honoured against
1558
+ * (APRV-135).
1559
+ *
1560
+ * The condition is both halves of the sentence, evaluated per entry:
1561
+ *
1562
+ * 1. **Every member is terminal.** For a digest that means every member
1563
+ * carries a `settled` outcome; for a unit delivery it is automatic in the
1564
+ * other direction, since annotating a decided, expired or withdrawn
1565
+ * request already forgets its nonces ({@link disarm}), so a delivery still
1566
+ * in the map is one this process has not seen settled. A request past its
1567
+ * approval TTL is terminal too — the gate refuses every decision on it —
1568
+ * which is what lets an unannotated delivery be swept at all.
1569
+ * 2. **Older than the retention window**, which is the policy's approval TTL
1570
+ * when it declares one and {@link TELEGRAM_DEFAULT_RETENTION_MS} when it
1571
+ * does not. Measured from the moment THIS process delivered the message,
1572
+ * which is at or after the `approval.requested` the TTL actually runs
1573
+ * from, so the window this sweep waits out is never shorter than the one
1574
+ * the gate enforces.
1575
+ *
1576
+ * Both together are what makes forgetting safe: a live button can never
1577
+ * reference a dropped entry, because the state in which no callback can still
1578
+ * be honoured is exactly the state in which the entry is dropped. A tap that
1579
+ * arrives anyway is answered by the stale-callback path a restarted
1580
+ * listener's buttons already take: `stale-copy` since APRV-196, counted,
1581
+ * toasted with what the log says became of the request, never carried to the
1582
+ * gate.
1583
+ *
1584
+ * Process memory only. No event, no message edit, no log read. `nowMs`
1585
+ * defaults to the configured clock and is a parameter so a test can run a
1586
+ * simulated week without one.
1587
+ */
1588
+ sweep(nowMs?: number): {
1589
+ deliveries: number;
1590
+ digests: number;
1591
+ };
1592
+ /** How many entries the bookkeeping holds. For tests and for operators. */
1593
+ bookkeepingSize(): {
1594
+ deliveries: number;
1595
+ digests: number;
1596
+ allNonces: number;
1597
+ reviewCards: number;
1598
+ };
1599
+ /**
1600
+ * Mark one digest member settled and redraw the digest (APRV-115).
1601
+ *
1602
+ * The member's own nonce is forgotten first, so a tap on a button the redraw
1603
+ * does not manage to remove resolves to nothing rather than reaching the
1604
+ * gate. The other members keep theirs: a partially decided digest is a real
1605
+ * state and the rest of it is still answerable.
1606
+ */
1607
+ private settleMember;
1608
+ /** One `editMessageText` that replaces a digest's text and its keyboard. */
1609
+ private redraw;
1610
+ /**
1611
+ * Edit a delivered message to say what became of its question, and remove the
1612
+ * buttons (APRV-106 for withdrawal, generalized in APRV-113 to every terminal
1613
+ * state).
1614
+ *
1615
+ * ONE `editMessageText` call, not two. Telegram's `editMessageText` replaces
1616
+ * the reply markup along with the text, and omitting `reply_markup` clears
1617
+ * it — so the annotation and the disarming land together, and there is no
1618
+ * window in which the message reads "approved" and still offers a tap.
1619
+ *
1620
+ * The text is REPLACED rather than appended to, because this class does not
1621
+ * remember what it sent (it remembers a nonce and a message id) and refetching
1622
+ * a message to append to it would be the channel reconstructing state it is
1623
+ * not supposed to hold. What the approver keeps is the outcome, the action key
1624
+ * and the detail lines, which is what a chat transcript needs to stay readable.
1625
+ *
1626
+ * `outcome` is a headline word (see {@link TELEGRAM_TERMINAL_HEADLINES}) and
1627
+ * `detail` the lines under it; both are HTML-escaped here, and neither may
1628
+ * carry an execution token — no caller in this repository has one to give,
1629
+ * since {@link DecisionOutcome} deliberately does not carry it.
1630
+ *
1631
+ * Best effort: {@link TelegramApiError} propagates to the caller, which logs
1632
+ * it and carries on. A message that could not be edited is a cosmetic
1633
+ * problem — the log has already settled the request, so a tap on the stale
1634
+ * buttons is refused by the gate and answered with the refusal toast.
1635
+ */
1636
+ annotate(deliveryId: DeliveryId, outcome: string, detail: string[],
1637
+ /**
1638
+ * Which request this settles, when `deliveryId` names a digest (APRV-115).
1639
+ * A digest holds several, so an annotation without one can only mean the
1640
+ * whole delivery is over — which is handled by falling through to the
1641
+ * message-replacing path below, buttons and all.
1642
+ */
1643
+ actionKey?: string): Promise<void>;
1644
+ /**
1645
+ * The withdrawal case of {@link annotate} (APRV-106), and the one the
1646
+ * {@link Channel} interface names. Its wording is unchanged.
1647
+ */
1648
+ retract(deliveryId: DeliveryId, reason: string, actionKey?: string): Promise<void>;
1649
+ /**
1650
+ * Send one plain message that carries no question (APRV-196).
1651
+ *
1652
+ * Used for the re-delivery banner the listener puts in front of a startup
1653
+ * batch. It arms nothing, remembers nothing, and names no action key: a
1654
+ * banner is a sentence about the messages that follow, and a reader who
1655
+ * mistook it for a request would be a reader the banner had made worse off.
1656
+ * `lines` are escaped here, exactly as everything else interpolated into an
1657
+ * HTML-mode message is.
1658
+ */
1659
+ announce(lines: string[]): Promise<DeliveryId>;
1660
+ /**
1661
+ * Which bot this token is, from one `getMe` (APRV-390).
1662
+ *
1663
+ * `getMe` is the only Bot API call that answers it, and it is the call
1664
+ * `approval doctor` and `approval setup channel telegram` already make for
1665
+ * the same reason: it mutates nothing, sends nothing, and acknowledges
1666
+ * nothing. In particular it is NOT `getUpdates`, so asking who this bot is
1667
+ * consumes no update and steals no pending tap from a listener that is
1668
+ * already running — which matters here above all, because the answer is
1669
+ * what decides whether this process is allowed to poll at all.
1670
+ *
1671
+ * Throws {@link TelegramApiError} like every other call; the caller decides
1672
+ * whether an unreachable Bot API is a refusal or a shrug.
1673
+ */
1674
+ identify(): Promise<{
1675
+ id: string;
1676
+ username: string;
1677
+ }>;
1678
+ /**
1679
+ * Long-poll `getUpdates` until {@link stop} is called (or one batch, with
1680
+ * `once`).
1681
+ *
1682
+ * **The loop survives the network.** A poll that times out, is refused, drops
1683
+ * its socket, returns a 5xx, or answers with something that is not JSON is
1684
+ * counted, complained about on stderr, and retried after a doubling backoff.
1685
+ * There is no failure mode in which the listener quietly stops listening: the
1686
+ * whole value of a push channel is that a human's inbox keeps receiving, and
1687
+ * a listener that died at 3am on a transient 502 would fail exactly when the
1688
+ * queue was filling up.
1689
+ *
1690
+ * Each iteration begins with {@link TelegramListenOptions.beforePoll} when
1691
+ * one is supplied, which is where the runtime's dispatch cycle runs: the
1692
+ * loop is therefore "deliver anything newly pending, then wait for a
1693
+ * decision", not "deliver once at startup, then wait forever".
1694
+ */
1695
+ listen(options?: TelegramListenOptions): Promise<void>;
1696
+ /** Stop the loop and abort any in-flight request. */
1697
+ stop(): void;
1698
+ /**
1699
+ * One `getUpdates` batch, processed. Throws on a transport failure — which is
1700
+ * what {@link listen} catches and retries.
1701
+ */
1702
+ pollOnce(): Promise<TelegramPollResult>;
1703
+ /**
1704
+ * Exactly one `answerCallbackQuery` per callback query, on every path
1705
+ * (APRV-196).
1706
+ *
1707
+ * The incident this closes: a tap that reached no branch with a toast on it
1708
+ * spun on the approver's phone until Telegram gave up, and the human — with
1709
+ * no way to tell a swallowed tap from a slow one — tapped again. So the ack
1710
+ * is a property of the WRAPPER rather than of each branch: every route below
1711
+ * still writes its own, better sentence, and anything that fails to (a throw
1712
+ * halfway through, a branch a later change forgets) is caught here and
1713
+ * answered with {@link TELEGRAM_ACK_FALLBACK}.
1714
+ *
1715
+ * A thrown handler is answered and swallowed rather than propagated, and that
1716
+ * is deliberate: `pollOnce` throwing puts `listen` into its backoff, so one
1717
+ * malformed update would cost the whole batch and the poll after it. Nothing
1718
+ * is lost by continuing — the gate has already appended whatever it appended,
1719
+ * and the log is what says so.
1720
+ *
1721
+ * APRV-206 moved WHEN that one answer is sent on the decision path: it now
1722
+ * goes out before the gate runs, so the spinner on the phone is one Bot API
1723
+ * call long instead of one decision long. The guarantee is unchanged and is
1724
+ * now enforced in one place — {@link safeAnswer} answers a query at most once,
1725
+ * so the fallback below cannot follow an early ack with a second call.
1726
+ */
1727
+ private handleUpdate;
1728
+ /**
1729
+ * A `message` update: the bot-command path (APRV-216).
1730
+ *
1731
+ * Three rules, in this order, and each of them is a refusal to act on
1732
+ * something the network said:
1733
+ *
1734
+ * 1. **No handler, no reading.** A channel with no command handler wants no
1735
+ * message updates and did not ask for any; one that arrives anyway (a
1736
+ * webhook backlog, a poll issued before the handler was registered) is
1737
+ * dropped without a counter, because there is nothing wrong with it.
1738
+ * 2. **The configured chat only.** A message from anywhere else is counted
1739
+ * `foreign-chat` and answered with nothing at all. Not even a refusal
1740
+ * reply: a stranger who can reach the bot learns from silence that the
1741
+ * bot is there, and learns from a reply what it is for.
1742
+ * 3. **A closed vocabulary.** `/queue`, `/skip`, `/next`. Anything else
1743
+ * beginning with `/` is counted `unknown-command`; anything not beginning
1744
+ * with `/` is ordinary chat and is ignored silently.
1745
+ *
1746
+ * A command decides nothing and appends nothing — it cannot, because it
1747
+ * never reaches {@link handler}. The handler it does reach reorders what the
1748
+ * runtime shows next, which is process memory on the runtime's side of the
1749
+ * boundary (SPEC.md §10.3).
1750
+ */
1751
+ private handleMessage;
1752
+ private routeCallback;
1753
+ /**
1754
+ * One tap over every still-open member of a digest (APRV-115).
1755
+ *
1756
+ * **N decisions, never one.** Each member is turned into its own
1757
+ * {@link ChannelDecision} — its own action key, its own payload binding — and
1758
+ * handed to the runtime's handler on its own, which records it through the
1759
+ * gate's compare-and-append on its own. There is no code path here that could
1760
+ * produce a single event covering two actions, because there is no call here
1761
+ * that writes anything at all.
1762
+ *
1763
+ * A member that refuses (already decided elsewhere, expired, withdrawn) does
1764
+ * not stop the rest, for the reason `channels/batch.ts` sets out: abandoning
1765
+ * four answers because the fifth had lapsed would discard a human's decision,
1766
+ * and un-appending the ones already written is not a thing the log permits.
1767
+ * The toast says how many landed and how many did not.
1768
+ *
1769
+ * The digest is redrawn ONCE at the end rather than per member: N edits of
1770
+ * the same message would show the approver their own decisions arriving one
1771
+ * at a time, and would spend N Bot API calls to end in the same place.
1772
+ */
1773
+ private handleDigestAll;
1774
+ /**
1775
+ * {@link annotate}, with a failed edit complained about rather than thrown
1776
+ * (APRV-206).
1777
+ *
1778
+ * Every caller on the decision path wants the same thing from a failed edit:
1779
+ * say so on the operator's terminal and carry on, because whatever the gate
1780
+ * did or did not append has already happened and no chat message changes it.
1781
+ *
1782
+ * The exception is {@link isMessageNotModified}, which says the message
1783
+ * already reads the way this call wanted it to read (APRV-277). Nothing is
1784
+ * printed for it: there is no staleness to warn about.
1785
+ */
1786
+ private annotateQuietly;
1787
+ /**
1788
+ * What a refused tap is told, in the message edit (APRV-206; it was the toast
1789
+ * until the single answer moved to the early ack).
1790
+ *
1791
+ * The duplicate case is the one worth naming: a second tap on a request the
1792
+ * gate has already decided produces `already-decided`, no second event, and
1793
+ * this text. Telegram redelivers callbacks on its own, so this path is
1794
+ * ordinary traffic, not an error.
1795
+ *
1796
+ * The sentences themselves moved to `channels/contract.ts` in APRV-235, so
1797
+ * that this message edit and the line the terminal channel prints are the
1798
+ * same words and cannot drift apart: a human who taps on their phone and
1799
+ * then reads the operator's terminal should not have to decide which of two
1800
+ * wordings to believe. The edit puts {@link TELEGRAM_NOT_RECORDED} above it
1801
+ * and clears the buttons, in `annotate`'s single call.
1802
+ */
1803
+ private answerFor;
1804
+ /**
1805
+ * A tap on `Sign` or `Not now` (APRV-257).
1806
+ *
1807
+ * The nonce is authoritative and there is no fallback ladder underneath it:
1808
+ * a checkpoint names no request, so there is no action reference to rescue a
1809
+ * stale copy with, and a tap this process cannot resolve is answered as
1810
+ * `unknown-callback` rather than guessed at. The cost is one dead button
1811
+ * after a restart, and the listener offers again on its next lapse.
1812
+ *
1813
+ * The nonce is consumed BEFORE the handler runs, so a double tap cannot
1814
+ * produce two records: the second tap finds nothing and says so. Even if it
1815
+ * did, `appendCheckpointAt` is a compare-and-append and the log would carry
1816
+ * two honest checkpoints over the same head, which is harmless — but a human
1817
+ * who taps twice should be told what happened rather than shown two
1818
+ * successes.
1819
+ *
1820
+ * The ack goes out FIRST (APRV-206's rule), because signing reads a vault
1821
+ * and appends to a log, and a spinner that lasted a decision long is what
1822
+ * that task removed.
1823
+ */
1824
+ private handleCheckpointTap;
1825
+ /**
1826
+ * A tap on one of a review card's six buttons (APRV-299).
1827
+ *
1828
+ * The nonce is authoritative and there is no fallback ladder underneath it,
1829
+ * for the reason {@link reviewCallbackData} gives: a review is never urgent,
1830
+ * a card this process is not holding leaves its sample open, and the next
1831
+ * cycle offers a fresh one. An unresolvable tap is answered
1832
+ * `unknown-callback` rather than guessed at.
1833
+ *
1834
+ * Which combinations are legal is decided by `core/audit.ts` and by nothing
1835
+ * here. A denied review that says the human loved the work is refused by
1836
+ * `reviewSample` before it reads the log, with the code SPEC.md §11.2 names,
1837
+ * and this method's job is to put that pair in front of it rather than to
1838
+ * re-implement the rule. The one thing this method owns is the ARMING, which
1839
+ * is process memory that appends nothing.
1840
+ */
1841
+ private handleReviewTap;
1842
+ /**
1843
+ * Send the ForceReply prompt a `loved` or `disliked` needs, and remember what
1844
+ * its reply will record (APRV-299).
1845
+ *
1846
+ * The pending grade lives HERE and not in the reply's own text, exactly as a
1847
+ * checkpoint's head lives in this process rather than in the callback bytes:
1848
+ * what gets recorded is what this process put on the screen. Losing the map
1849
+ * to a restart costs the reply its meaning — nothing is appended, the sample
1850
+ * stays open, and a fresh card is offered — and can never cost a record
1851
+ * nobody asked for.
1852
+ *
1853
+ * A second prompt replaces the first: only one grade can be outstanding on
1854
+ * one card, and the older prompt stops resolving so a late reply to it lands
1855
+ * nowhere rather than recording a grade the human moved on from.
1856
+ */
1857
+ private askForNote;
1858
+ /**
1859
+ * A message replying to an outstanding note prompt (APRV-299).
1860
+ *
1861
+ * Returns `true` when this update was a note reply and has been dealt with,
1862
+ * so the command path below never sees it. Three refusals to act on something
1863
+ * the network said, in order: a reply naming no prompt this process issued is
1864
+ * not ours, a reply from another chat is counted `foreign-chat` and answered
1865
+ * with nothing at all, and the words themselves are passed to the runtime
1866
+ * verbatim — a blank one included, because whether blank is a note is
1867
+ * `core/audit.ts`'s rule and not this channel's.
1868
+ */
1869
+ private handleNoteReply;
1870
+ /**
1871
+ * Hand one review tap to the runtime and redraw the card from its answer
1872
+ * (APRV-299).
1873
+ *
1874
+ * A recorded review settles the card and forgets its nonce, so a tap on a
1875
+ * button the edit does not manage to remove resolves to nothing rather than
1876
+ * recording a second human observation of one item. A REFUSAL does neither:
1877
+ * nothing was appended, the sample is still open, and the codes that get here
1878
+ * are ones the reviewer can act on — `reaction-conflicts-verdict` asks them
1879
+ * to say which half they meant, and `note-required` asks for words — so the
1880
+ * buttons stay, with the refusal rendered above them and the arming intact.
1881
+ */
1882
+ private recordReview;
1883
+ /** One `editMessageText` that replaces a review card's text and its keyboard. */
1884
+ private redrawReview;
1885
+ private ignore;
1886
+ private answer;
1887
+ /**
1888
+ * Answer, and never throw (APRV-196).
1889
+ *
1890
+ * A toast is a courtesy on every path, including the successful one: the
1891
+ * decision is already in the log by the time the ack is attempted, and an
1892
+ * `answerCallbackQuery` that fails (Telegram drops a query after its own
1893
+ * window, and a phone on a train produces plenty of late taps) must not
1894
+ * abandon the annotation or push the poll loop into backoff.
1895
+ *
1896
+ * The attempt is recorded either way, so {@link handleUpdate}'s guarantee
1897
+ * does not turn one failed ack into a second doomed call.
1898
+ *
1899
+ * **Idempotent per callback query since APRV-206.** A query that has already
1900
+ * been answered in this handling is not answered again: the early ack the
1901
+ * decision path sends is THE answer, and every later sentence — a branch's
1902
+ * own toast, the wrapper's fallback — becomes a no-op rather than a second
1903
+ * `answerCallbackQuery`. APRV-196's "exactly one per callback" therefore
1904
+ * holds structurally, in this one method, instead of by every branch
1905
+ * remembering to return.
1906
+ */
1907
+ private safeAnswer;
1908
+ /**
1909
+ * The delivery this process is holding open for an action reference, if any
1910
+ * (APRV-196).
1911
+ *
1912
+ * A linear walk of the delivery map rather than a second index: the map is
1913
+ * bounded by the pending queue and swept (APRV-135), this runs only on the
1914
+ * uncommon path where a nonce did not resolve, and a second map would be a
1915
+ * second thing to keep in step with `disarm`, `settleMember` and `sweep` —
1916
+ * three places where forgetting is the safety property.
1917
+ *
1918
+ * Digest members are eligible: a member's nonce is deleted the moment it is
1919
+ * settled, so a member still in the map is one still armed on a live message.
1920
+ */
1921
+ private liveDeliveryFor;
1922
+ /** Replace the token with a placeholder anywhere it appears in `text`. */
1923
+ private redact;
1924
+ private describe;
1925
+ /**
1926
+ * The Bot API's own `description` for a failed response, redacted (APRV-277).
1927
+ *
1928
+ * `null` whenever there is nothing trustworthy to quote: the body could not
1929
+ * be read, it was not JSON, or it carried no description. Every failure mode
1930
+ * here is silent by design, because this runs on a path that is already
1931
+ * reporting a failure and a second one thrown from the diagnostic would
1932
+ * replace the real reason with a worse one.
1933
+ */
1934
+ private describeFailure;
1935
+ /**
1936
+ * One Bot API call.
1937
+ *
1938
+ * The token is in the URL, which is how the Bot API works — there is no
1939
+ * header form. It is therefore never put in a message body, an error string,
1940
+ * or a log line: {@link redact} scrubs everything that leaves this class, and
1941
+ * the test suite scans every request body and every log byte for it.
1942
+ */
1943
+ private call;
1944
+ }