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.
- package/README.md +629 -559
- package/SPEC.md +99 -24
- package/dist/src/adapters/agentmail.d.ts +426 -0
- package/dist/src/adapters/agentmail.js +2 -2
- package/dist/src/adapters/conformance.d.ts +149 -0
- package/dist/src/adapters/contract.d.ts +628 -0
- package/dist/src/adapters/contract.js +110 -16
- package/dist/src/adapters/contract.js.map +1 -1
- package/dist/src/adapters/email.d.ts +324 -0
- package/dist/src/adapters/env-passphrase.d.ts +93 -0
- package/dist/src/adapters/public.d.ts +11 -0
- package/dist/src/adapters/public.js +11 -0
- package/dist/src/adapters/public.js.map +1 -0
- package/dist/src/adapters/registry.d.ts +59 -0
- package/dist/src/adapters/registry.js +2 -1
- package/dist/src/adapters/registry.js.map +1 -1
- package/dist/src/adapters/smtp.d.ts +213 -0
- package/dist/src/adapters/vault-provider.d.ts +114 -0
- package/dist/src/adapters/vault-provider.js +3 -3
- package/dist/src/adapters/zzz.d.ts +66 -0
- package/dist/src/adapters/zzz.js +299 -0
- package/dist/src/adapters/zzz.js.map +1 -0
- package/dist/src/channels/batch.d.ts +109 -0
- package/dist/src/channels/cli.d.ts +193 -0
- package/dist/src/channels/conformance.d.ts +92 -0
- package/dist/src/channels/contract.d.ts +656 -0
- package/dist/src/channels/contract.js +200 -7
- package/dist/src/channels/contract.js.map +1 -1
- package/dist/src/channels/payload-view.d.ts +35 -0
- package/dist/src/channels/render-queue.d.ts +149 -0
- package/dist/src/channels/tagging.d.ts +196 -0
- package/dist/src/channels/telegram.d.ts +1944 -0
- package/dist/src/channels/telegram.js +218 -23
- package/dist/src/channels/telegram.js.map +1 -1
- package/dist/src/channels/web.d.ts +350 -0
- package/dist/src/channels/web.js +17 -0
- package/dist/src/channels/web.js.map +1 -1
- package/dist/src/cli/adapter.d.ts +90 -0
- package/dist/src/cli/adapter.js +25 -15
- package/dist/src/cli/adapter.js.map +1 -1
- package/dist/src/cli/amend.d.ts +59 -0
- package/dist/src/cli/amend.js +214 -30
- package/dist/src/cli/amend.js.map +1 -1
- package/dist/src/cli/args.d.ts +43 -0
- package/dist/src/cli/attest.d.ts +50 -0
- package/dist/src/cli/attest.js +134 -7
- package/dist/src/cli/attest.js.map +1 -1
- package/dist/src/cli/audit-card.d.ts +62 -0
- package/dist/src/cli/audit.d.ts +59 -0
- package/dist/src/cli/channel-telegram.d.ts +879 -0
- package/dist/src/cli/channel-telegram.js +311 -13
- package/dist/src/cli/channel-telegram.js.map +1 -1
- package/dist/src/cli/channel-web.d.ts +131 -0
- package/dist/src/cli/channel.d.ts +80 -0
- package/dist/src/cli/channel.js +9 -0
- package/dist/src/cli/channel.js.map +1 -1
- package/dist/src/cli/checkpoint-tap.d.ts +169 -0
- package/dist/src/cli/codex-bridge.d.ts +819 -0
- package/dist/src/cli/codex-bridge.js +1607 -0
- package/dist/src/cli/codex-bridge.js.map +1 -0
- package/dist/src/cli/codex.d.ts +2 -0
- package/dist/src/cli/codex.js +469 -0
- package/dist/src/cli/codex.js.map +1 -0
- package/dist/src/cli/coverage.d.ts +61 -0
- package/dist/src/cli/daemon.d.ts +120 -0
- package/dist/src/cli/daemon.js +4 -1
- package/dist/src/cli/daemon.js.map +1 -1
- package/dist/src/cli/doctor.d.ts +129 -0
- package/dist/src/cli/doctor.js +586 -17
- package/dist/src/cli/doctor.js.map +1 -1
- package/dist/src/cli/env.d.ts +65 -0
- package/dist/src/cli/execute.d.ts +202 -0
- package/dist/src/cli/execute.js +25 -2
- package/dist/src/cli/execute.js.map +1 -1
- package/dist/src/cli/exit-codes.d.ts +73 -0
- package/dist/src/cli/feedback.d.ts +60 -0
- package/dist/src/cli/gate-window.d.ts +40 -0
- package/dist/src/cli/gate.d.ts +68 -0
- package/dist/src/cli/git-scope.d.ts +190 -0
- package/dist/src/cli/gloss-attach.d.ts +85 -0
- package/dist/src/cli/gloss-codex-child.d.ts +9 -0
- package/dist/src/cli/gloss-codex.d.ts +24 -0
- package/dist/src/cli/gloss-options.d.ts +42 -0
- package/dist/src/cli/gloss.d.ts +265 -0
- package/dist/src/cli/help.d.ts +107 -0
- package/dist/src/cli/help.js +320 -93
- package/dist/src/cli/help.js.map +1 -1
- package/dist/src/cli/hook-codex.d.ts +126 -0
- package/dist/src/cli/hook-codex.js +226 -0
- package/dist/src/cli/hook-codex.js.map +1 -0
- package/dist/src/cli/hook.d.ts +787 -0
- package/dist/src/cli/hook.js +1235 -181
- package/dist/src/cli/hook.js.map +1 -1
- package/dist/src/cli/import.d.ts +35 -0
- package/dist/src/cli/import.js +1 -1
- package/dist/src/cli/import.js.map +1 -1
- package/dist/src/cli/init.d.ts +84 -0
- package/dist/src/cli/init.js +2 -2
- package/dist/src/cli/init.js.map +1 -1
- package/dist/src/cli/instructions.d.ts +23 -0
- package/dist/src/cli/journal.d.ts +41 -0
- package/dist/src/cli/log-advance.d.ts +287 -0
- package/dist/src/cli/log-advance.js +102 -11
- package/dist/src/cli/log-advance.js.map +1 -1
- package/dist/src/cli/log-anchor.d.ts +176 -0
- package/dist/src/cli/log-checkpoint.d.ts +22 -0
- package/dist/src/cli/log-sync.d.ts +243 -0
- package/dist/src/cli/log-verbs.d.ts +16 -0
- package/dist/src/cli/log-verbs.js +7 -1
- package/dist/src/cli/log-verbs.js.map +1 -1
- package/dist/src/cli/long-help.d.ts +70 -0
- package/dist/src/cli/main.d.ts +77 -0
- package/dist/src/cli/main.js +159 -7
- package/dist/src/cli/main.js.map +1 -1
- package/dist/src/cli/mcp.d.ts +52 -0
- package/dist/src/cli/paths.d.ts +56 -0
- package/dist/src/cli/payload.d.ts +58 -0
- package/dist/src/cli/policy-apply.d.ts +195 -0
- package/dist/src/cli/policy-apply.js +573 -0
- package/dist/src/cli/policy-apply.js.map +1 -0
- package/dist/src/cli/policy.d.ts +43 -0
- package/dist/src/cli/policy.js +14 -1
- package/dist/src/cli/policy.js.map +1 -1
- package/dist/src/cli/preflight.d.ts +501 -0
- package/dist/src/cli/preflight.js +689 -45
- package/dist/src/cli/preflight.js.map +1 -1
- package/dist/src/cli/progress.d.ts +78 -0
- package/dist/src/cli/prompt.d.ts +209 -0
- package/dist/src/cli/quickstart.d.ts +46 -0
- package/dist/src/cli/quickstart.js +297 -0
- package/dist/src/cli/quickstart.js.map +1 -0
- package/dist/src/cli/records.d.ts +34 -0
- package/dist/src/cli/render.d.ts +22 -0
- package/dist/src/cli/sandbox.d.ts +51 -0
- package/dist/src/cli/sandbox.js +17 -1
- package/dist/src/cli/sandbox.js.map +1 -1
- package/dist/src/cli/scaffold.d.ts +79 -0
- package/dist/src/cli/scaffold.js +1 -1
- package/dist/src/cli/setup-adapter.d.ts +137 -0
- package/dist/src/cli/setup-adapter.js +38 -4
- package/dist/src/cli/setup-adapter.js.map +1 -1
- package/dist/src/cli/setup-channel.d.ts +126 -0
- package/dist/src/cli/setup-channel.js +28 -1
- package/dist/src/cli/setup-channel.js.map +1 -1
- package/dist/src/cli/setup-checkpoint.d.ts +57 -0
- package/dist/src/cli/setup-common.d.ts +277 -0
- package/dist/src/cli/setup-common.js +3 -2
- package/dist/src/cli/setup-common.js.map +1 -1
- package/dist/src/cli/setup-flow.d.ts +287 -0
- package/dist/src/cli/setup-service.d.ts +96 -0
- package/dist/src/cli/setup.d.ts +204 -0
- package/dist/src/cli/setup.js +94 -2
- package/dist/src/cli/setup.js.map +1 -1
- package/dist/src/cli/style.d.ts +320 -0
- package/dist/src/cli/token.d.ts +39 -0
- package/dist/src/cli/up.d.ts +155 -0
- package/dist/src/cli/up.js +119 -53
- package/dist/src/cli/up.js.map +1 -1
- package/dist/src/cli/usage.d.ts +37 -0
- package/dist/src/cli/values.d.ts +40 -0
- package/dist/src/cli/values.js +3 -4
- package/dist/src/cli/values.js.map +1 -1
- package/dist/src/cli/vault.d.ts +59 -0
- package/dist/src/cli/vault.js +2 -2
- package/dist/src/cli/vault.js.map +1 -1
- package/dist/src/cli/verb-registry.d.ts +76 -0
- package/dist/src/cli/verb-registry.js +344 -11
- package/dist/src/cli/verb-registry.js.map +1 -1
- package/dist/src/cli/wordmark.d.ts +31 -0
- package/dist/src/cli/wordmark.js +2 -2
- package/dist/src/codex/broker.d.ts +229 -0
- package/dist/src/codex/broker.js +548 -0
- package/dist/src/codex/broker.js.map +1 -0
- package/dist/src/codex/doctor.d.ts +13 -0
- package/dist/src/codex/doctor.js +41 -0
- package/dist/src/codex/doctor.js.map +1 -0
- package/dist/src/codex/manifest.d.ts +49 -0
- package/dist/src/codex/manifest.js +103 -0
- package/dist/src/codex/manifest.js.map +1 -0
- package/dist/src/codex/runner.d.ts +178 -0
- package/dist/src/codex/runner.js +231 -0
- package/dist/src/codex/runner.js.map +1 -0
- package/dist/src/codex/serve.d.ts +56 -0
- package/dist/src/codex/serve.js +98 -0
- package/dist/src/codex/serve.js.map +1 -0
- package/dist/src/codex/templates.d.ts +41 -0
- package/dist/src/codex/templates.js +319 -0
- package/dist/src/codex/templates.js.map +1 -0
- package/dist/src/codex/trust.d.ts +19 -0
- package/dist/src/codex/trust.js +183 -0
- package/dist/src/codex/trust.js.map +1 -0
- package/dist/src/codex/workspace-commit.d.ts +219 -0
- package/dist/src/codex/workspace-commit.js +549 -0
- package/dist/src/codex/workspace-commit.js.map +1 -0
- package/dist/src/codex/workspace-plan.d.ts +131 -0
- package/dist/src/codex/workspace-plan.js +561 -0
- package/dist/src/codex/workspace-plan.js.map +1 -0
- package/dist/src/core/actor.d.ts +2 -0
- package/dist/src/core/actor.js +5 -0
- package/dist/src/core/actor.js.map +1 -0
- package/dist/src/core/advance-cycle.d.ts +221 -0
- package/dist/src/core/advance-cycle.js +66 -2
- package/dist/src/core/advance-cycle.js.map +1 -1
- package/dist/src/core/agents-md.d.ts +278 -0
- package/dist/src/core/agents-md.js +33 -31
- package/dist/src/core/agents-md.js.map +1 -1
- package/dist/src/core/apply-patch.d.ts +49 -0
- package/dist/src/core/apply-patch.js +266 -0
- package/dist/src/core/apply-patch.js.map +1 -0
- package/dist/src/core/attest.d.ts +635 -0
- package/dist/src/core/attest.js +326 -4
- package/dist/src/core/attest.js.map +1 -1
- package/dist/src/core/audit.d.ts +510 -0
- package/dist/src/core/audit.js +13 -0
- package/dist/src/core/audit.js.map +1 -1
- package/dist/src/core/budgets.d.ts +238 -0
- package/dist/src/core/channel-owner.d.ts +213 -0
- package/dist/src/core/channel-owner.js +358 -0
- package/dist/src/core/channel-owner.js.map +1 -0
- package/dist/src/core/checkpoint.d.ts +500 -0
- package/dist/src/core/child-env.d.ts +88 -0
- package/dist/src/core/clock.d.ts +52 -0
- package/dist/src/core/command-class.d.ts +697 -0
- package/dist/src/core/command-class.js +713 -25
- package/dist/src/core/command-class.js.map +1 -1
- package/dist/src/core/commit-guard.d.ts +272 -0
- package/dist/src/core/commit-guard.js +424 -0
- package/dist/src/core/commit-guard.js.map +1 -0
- package/dist/src/core/coverage-sources/adapter.d.ts +40 -0
- package/dist/src/core/coverage-sources/gh.d.ts +48 -0
- package/dist/src/core/coverage-sources/git.d.ts +101 -0
- package/dist/src/core/coverage.d.ts +217 -0
- package/dist/src/core/credential-spec.d.ts +72 -0
- package/dist/src/core/daemon-actor.d.ts +45 -0
- package/dist/src/core/daemon-actor.js +54 -0
- package/dist/src/core/daemon-actor.js.map +1 -0
- package/dist/src/core/dark-session.d.ts +432 -0
- package/dist/src/core/dark-session.js +266 -82
- package/dist/src/core/dark-session.js.map +1 -1
- package/dist/src/core/decision-refusal.d.ts +206 -0
- package/dist/src/core/decision-refusal.js +24 -2
- package/dist/src/core/decision-refusal.js.map +1 -1
- package/dist/src/core/env-file.d.ts +455 -0
- package/dist/src/core/env-file.js +60 -1
- package/dist/src/core/env-file.js.map +1 -1
- package/dist/src/core/execute.d.ts +871 -0
- package/dist/src/core/execute.js +59 -8
- package/dist/src/core/execute.js.map +1 -1
- package/dist/src/core/frontmatter.d.ts +78 -0
- package/dist/src/core/gate-window.d.ts +312 -0
- package/dist/src/core/gate.d.ts +1449 -0
- package/dist/src/core/gate.js +149 -14
- package/dist/src/core/gate.js.map +1 -1
- package/dist/src/core/gesture-refusal.d.ts +166 -0
- package/dist/src/core/gesture-refusal.js +188 -0
- package/dist/src/core/gesture-refusal.js.map +1 -0
- package/dist/src/core/git-run.d.ts +73 -0
- package/dist/src/core/harness-version.d.ts +157 -0
- package/dist/src/core/harness-version.js +4 -1
- package/dist/src/core/harness-version.js.map +1 -1
- package/dist/src/core/harness-wait.d.ts +55 -0
- package/dist/src/core/head-retry.d.ts +107 -0
- package/dist/src/core/instance.d.ts +310 -0
- package/dist/src/core/instance.js +113 -0
- package/dist/src/core/instance.js.map +1 -1
- package/dist/src/core/intake-limits.d.ts +247 -0
- package/dist/src/core/jcs.d.ts +52 -0
- package/dist/src/core/journal.d.ts +144 -0
- package/dist/src/core/live-draw.d.ts +436 -0
- package/dist/src/core/log-reconcile.d.ts +89 -0
- package/dist/src/core/log-subscribe.d.ts +36 -0
- package/dist/src/core/log-subscribe.js +162 -0
- package/dist/src/core/log-subscribe.js.map +1 -0
- package/dist/src/core/log.d.ts +316 -0
- package/dist/src/core/log.js.map +1 -1
- package/dist/src/core/loop.d.ts +274 -0
- package/dist/src/core/loop.js +11 -0
- package/dist/src/core/loop.js.map +1 -1
- package/dist/src/core/md-fence.d.ts +41 -0
- package/dist/src/core/money.d.ts +147 -0
- package/dist/src/core/payload-census.d.ts +74 -0
- package/dist/src/core/payload-store.d.ts +175 -0
- package/dist/src/core/payload.d.ts +71 -0
- package/dist/src/core/policy-diff.d.ts +292 -0
- package/dist/src/core/policy-diff.js +27 -4
- package/dist/src/core/policy-diff.js.map +1 -1
- package/dist/src/core/policy-expectations.d.ts +199 -0
- package/dist/src/core/policy-explain.d.ts +160 -0
- package/dist/src/core/policy-explain.js +63 -3
- package/dist/src/core/policy-explain.js.map +1 -1
- package/dist/src/core/policy-load.d.ts +567 -0
- package/dist/src/core/policy-load.js +36 -6
- package/dist/src/core/policy-load.js.map +1 -1
- package/dist/src/core/policy-match.d.ts +324 -0
- package/dist/src/core/policy-match.js +72 -9
- package/dist/src/core/policy-match.js.map +1 -1
- package/dist/src/core/policy-proposal.d.ts +317 -0
- package/dist/src/core/policy-proposal.js +102 -2
- package/dist/src/core/policy-proposal.js.map +1 -1
- package/dist/src/core/prompt-layout.d.ts +221 -0
- package/dist/src/core/protected-path-guard.d.ts +566 -0
- package/dist/src/core/protected-path-guard.js +848 -55
- package/dist/src/core/protected-path-guard.js.map +1 -1
- package/dist/src/core/question-preempted.d.ts +141 -0
- package/dist/src/core/question-preempted.js +152 -0
- package/dist/src/core/question-preempted.js.map +1 -0
- package/dist/src/core/read-scope.d.ts +172 -0
- package/dist/src/core/read-scope.js +252 -0
- package/dist/src/core/read-scope.js.map +1 -0
- package/dist/src/core/registration.d.ts +25 -0
- package/dist/src/core/reindex.d.ts +99 -0
- package/dist/src/core/sampler.d.ts +313 -0
- package/dist/src/core/sandbox.d.ts +371 -0
- package/dist/src/core/sandbox.js +190 -1
- package/dist/src/core/sandbox.js.map +1 -1
- package/dist/src/core/seal.d.ts +165 -0
- package/dist/src/core/sender-identity.d.ts +476 -0
- package/dist/src/core/sender-identity.js +572 -0
- package/dist/src/core/sender-identity.js.map +1 -0
- package/dist/src/core/shlex.d.ts +102 -0
- package/dist/src/core/shlex.js +159 -0
- package/dist/src/core/shlex.js.map +1 -0
- package/dist/src/core/state.d.ts +505 -0
- package/dist/src/core/task-file.d.ts +185 -0
- package/dist/src/core/telegram-config.d.ts +93 -0
- package/dist/src/core/token.d.ts +409 -0
- package/dist/src/core/token.js +21 -38
- package/dist/src/core/token.js.map +1 -1
- package/dist/src/core/validate.d.ts +138 -0
- package/dist/src/core/values.d.ts +147 -0
- package/dist/src/core/values.js +36 -1
- package/dist/src/core/values.js.map +1 -1
- package/dist/src/core/vault.d.ts +291 -0
- package/dist/src/core/verified-snapshot.d.ts +204 -0
- package/dist/src/core/verify.d.ts +336 -0
- package/dist/src/core/version.d.ts +8 -0
- package/dist/src/core/wysiwys.d.ts +370 -0
- package/dist/src/daemon/advance-child.d.ts +39 -0
- package/dist/src/daemon/advance.d.ts +476 -0
- package/dist/src/daemon/advance.js +25 -4
- package/dist/src/daemon/advance.js.map +1 -1
- package/dist/src/daemon/audit.d.ts +87 -0
- package/dist/src/daemon/daemon.d.ts +1180 -0
- package/dist/src/daemon/daemon.js +9 -0
- package/dist/src/daemon/daemon.js.map +1 -1
- package/dist/src/daemon/dark-session.d.ts +64 -0
- package/dist/src/daemon/draw-child.d.ts +36 -0
- package/dist/src/daemon/draw.d.ts +154 -0
- package/dist/src/daemon/git-evidence.d.ts +173 -0
- package/dist/src/daemon/git-evidence.js +1 -1
- package/dist/src/daemon/projection.d.ts +180 -0
- package/dist/src/daemon/prune.d.ts +207 -0
- package/dist/src/mcp/http.d.ts +113 -0
- package/dist/src/mcp/server.d.ts +265 -0
- package/dist/src/mcp/server.js +17 -1
- package/dist/src/mcp/server.js.map +1 -1
- package/docs/adapter-api.md +106 -0
- package/docs/cli-reference.md +1316 -63
- package/docs/codex-enforced-session.md +103 -0
- package/docs/codex-workspace-broker.md +118 -0
- package/package.json +14 -2
- package/schema/codex-instance.schema.json +82 -0
- package/schema/event.schema.json +539 -9
- package/schema/fixtures/codex-instance/invalid/unpinned-codex-version.json +40 -0
- package/schema/fixtures/codex-instance/valid/canonical.json +40 -0
- package/schema/fixtures/event/invalid/approval-granted-sender-hashed-false.json +20 -0
- package/schema/fixtures/event/invalid/approval-granted-sender-hashed-raw-id.json +20 -0
- package/schema/fixtures/event/invalid/audit-gesture-refused-human-actor.json +16 -0
- package/schema/fixtures/event/invalid/audit-gesture-refused-no-actor-no-sender.json +15 -0
- package/schema/fixtures/event/invalid/audit-gesture-refused-unknown-gesture.json +16 -0
- package/schema/fixtures/event/invalid/audit-question-preempted-agent-actor.json +16 -0
- package/schema/fixtures/event/invalid/audit-question-preempted-no-question-id.json +16 -0
- package/schema/fixtures/event/invalid/audit-question-preempted-unknown-source.json +15 -0
- package/schema/fixtures/event/invalid/gate-path-signed-off-absolute-path.json +14 -0
- package/schema/fixtures/event/invalid/gate-path-signed-off-agent-actor.json +14 -0
- package/schema/fixtures/event/invalid/gate-path-signed-off-missing-path.json +13 -0
- package/schema/fixtures/event/valid/approval-granted-sender-hashed.json +20 -0
- package/schema/fixtures/event/valid/audit-gesture-refused-review-note.json +21 -0
- package/schema/fixtures/event/valid/audit-gesture-refused-sender-key-unavailable.json +19 -0
- package/schema/fixtures/event/valid/audit-gesture-refused.json +19 -0
- package/schema/fixtures/event/valid/audit-question-preempted-no-verdict.json +16 -0
- package/schema/fixtures/event/valid/audit-question-preempted.json +20 -0
- package/schema/fixtures/event/valid/gate-path-signed-off.json +14 -0
- package/schema/fixtures/event/valid/harness-kind-claude-code.json +23 -0
- package/schema/fixtures/event/valid/harness-kind-codex.json +23 -0
- package/schema/fixtures/event/valid/harness-kind-cursor.json +23 -0
- package/schema/fixtures/event/valid/harness-kind-grok.json +23 -0
- package/schema/fixtures/event/valid/harness-kind-muse.json +23 -0
- package/schema/fixtures/policy/invalid/senders-half-keyed.json +20 -0
- package/schema/fixtures/policy/valid/canonical.json +1 -1
- package/schema/fixtures/policy/valid/senders-keyed.json +24 -0
- package/schema/fixtures/policy-md/valid/canonical.md +1 -1
- package/schema/fixtures/policy-md/valid/with-values.md +5 -7
- package/schema/fixtures/values/invalid/class-shaped.json +1 -1
- package/schema/fixtures/values/invalid/duplicate-entry.json +1 -1
- package/schema/fixtures/values/invalid/non-string-item.json +1 -1
- package/schema/fixtures/values/invalid/over-cap.json +1 -1
- package/schema/fixtures/values/invalid/unknown-key.json +1 -1
- package/schema/fixtures/values/invalid/version-float.json +1 -0
- package/schema/fixtures/values/invalid/version-integer.json +1 -0
- package/schema/fixtures/values/invalid/version-wrong-string.json +1 -0
- package/schema/fixtures/values/valid/empty-lists.json +2 -3
- package/schema/fixtures/values/valid/full.json +5 -7
- package/schema/fixtures/values/valid/minimal.json +1 -1
- package/schema/fixtures/values-md/invalid/schema-invalid.md +5 -3
- package/schema/fixtures/values-md/invalid/two-blocks.md +3 -3
- package/schema/fixtures/values-md/invalid/unterminated.md +2 -2
- package/schema/fixtures/values-md/invalid/version-1.md +69 -0
- package/schema/fixtures/values-md/invalid/version-unquoted.md +64 -0
- package/schema/fixtures/values-md/invalid/yaml-error.md +2 -2
- package/schema/fixtures/values-md/valid/absent.md +1 -1
- package/schema/fixtures/values-md/valid/with-values.md +5 -7
- package/schema/policy.schema.json +75 -3
- package/schema/values.schema.json +7 -11
- package/templates/codex/README.md +9 -0
- 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 `&` 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
|
+
}
|