diff --git a/DOCS-changelog.md b/DOCS-changelog.md index 1c32a4c..27d34e9 100644 --- a/DOCS-changelog.md +++ b/DOCS-changelog.md @@ -16,6 +16,23 @@ Add the new section at the top when you bump `RELEASE_ORDINAL`. Anything that is not a `##` heading or a `-` bullet under one is ignored, so prose between releases is free. +**A version bump is a publish, not a checkpoint.** Restarting the service after +one posts these bullets into every watched group, including the real one. So +exercise the feature in a test group — `testbot`, `catbot`, `finaltestbot`, +each just you and the bot — *before* bumping `RELEASE_ORDINAL`, tagging or +restarting. To try something at the current version without announcing, leave +`RELEASE_ORDINAL` alone and restart. Green unit tests and a green +`scripts/e2e.sh` are **not** sufficient evidence: `e2e.sh` runs against the +local fake, so it proves the wiring and nothing about how Signal renders the +result or how the real upstream API behaves. 0.0.3 shipped a `!gekon` that +passed every gate and posted the same photo every time, because the one thing +nobody had done was call the real endpoint twice. + +**Write bullets the group can notice.** The audience is people in a chat, not +this repo: describe what changes for them. 0.0.1 shipped a bullet about +`/healthz`, an endpoint bound to 127.0.0.1 that nobody in a Signal group can +reach, and it was rightly called out. + ## 0.0.4 — Ewaryst - `!gekon` pokazuje teraz za każdym razem innego gekona — wcześniej wrzucał diff --git a/DOCS-latency.md b/DOCS-latency.md new file mode 100644 index 0000000..7f6941e --- /dev/null +++ b/DOCS-latency.md @@ -0,0 +1,46 @@ +# Latency: where the 3.7 seconds actually go + +Measured on the live bot, 2026-08-29, for one real `!cat` in the production +group. Every number here came from `journalctl --user -u signal-bot.service`, +not from a model of the system. + +``` +22:59:37.984 running command cat +22:59:38.370 GET api.thecatapi.com/…/search 200 386 ms +22:59:39.054 GET s3…/MjA2MDgxMA.jpg 200 684 ms +22:59:41.702 POST localhost:9922/v2/send 201 2648 ms +``` + +Fetching the picture — the JSON lookup plus the download — is **1.07 s of the +3.7 s**. `POST /v2/send` on its own is **2.65 s**, and that is the +signal-cli-rest-api container uploading the attachment to Signal. It is 72% of +the wall clock and none of it is this repo's code. + +## The consequence for the latency release + +The plan proposed an image prefetch pool as "the single biggest win": keep N +pre-fetched, pre-base64'd images per command in memory so a command costs one +send instead of *fetch JSON → download → encode*. The measurement says that +buys 3.7 s → about 2.7 s. Worth having, but it is not the biggest win and it +does not get anywhere near a sub-second reply, because the send is untouched. + +**Do not design the latency release on the plan's assumption.** Before +building anything, measure these two separately: + +1. A **text-only** reply, end to end. That isolates `POST /v2/send` without an + attachment and says how much of the 2.65 s is upload versus fixed overhead. +2. Whether signal-cli can **reuse an already-uploaded attachment id**, so a + repeated image costs a reference rather than a fresh upload. If it can, + that is the real win and it makes the prefetch pool far more valuable, + because the pool could hold ids instead of bytes. + +Head-of-line blocking is a separate, confirmed problem and does not need +re-measuring: `app.py` awaits each frame to completion, so one slow upstream +fetch delays every other message behind it. Dispatching each frame as its own +task under a bounded semaphore fixes that regardless of what the send costs. + +## Rule + +An upstream latency claim gets measured before it gets designed around. The +prefetch-pool assumption survived a whole planning pass and was wrong by more +than a factor of two in the direction that mattered. diff --git a/DOCS-signal-envelopes.md b/DOCS-signal-envelopes.md index c19c5ad..3cdc780 100644 --- a/DOCS-signal-envelopes.md +++ b/DOCS-signal-envelopes.md @@ -61,9 +61,18 @@ trusts `source`. ## From the bot's own account: `syncMessage.sentMessage` -Because the bot runs as a *linked device*, the account's own outgoing messages +When the account is deployed as a *linked device*, its own outgoing messages come back as sync messages. Same fields, one level deeper. +> **This deployment never receives them.** The bot registers its own number as +> its own primary device and has no linked devices, so Signal sends it no copy +> of its own traffic. The consequence to know before you try to test anything: +> the bot **cannot self-trigger a command**. Posting `!cat` from the bot's +> number into a group does nothing whatsoever — a human has to type it, and +> `journalctl --user -u signal-bot.service` showing `running command …` is how +> you confirm it landed. The parsing below stays because the linked-device +> deployment is still supported, not because it fires here. + ```json { "envelope": { @@ -99,7 +108,7 @@ were counted as messages. Four consecutive commits (`f1931b8`, `b325126`, ## No `groupInfo` means a direct message A frame without `groupInfo` arrived in a one-to-one conversation. A linked -device receives every one of those on the account, so `counting.watched_group` +device would receive every one of those on the account, so `counting.watched_group` rejects them explicitly rather than relying on a falsy comparison. See the `scoping` scenario in `fake_signal/scenarios/`. diff --git a/README.md b/README.md index fcff713..0fad240 100644 --- a/README.md +++ b/README.md @@ -201,9 +201,8 @@ connection to Signal is dead, a two-minute timer restarts the container, and Aliases live in `signal_bot/aliases.json`, not in source. Each command is rate-limited per user, per command — the original bot shared one 10-second window across everybody. -`signal_bot/decorators.py` also ships `daily_limit` and `vote_gated` (run only -once N distinct people ask, within a window). Both are generic and tested; -attach them to any command. +`signal_bot/decorators.py` also ships `daily_limit` and `vote_gated` (run only once N +distinct people ask, within a window). Both are generic and tested; attach them to any command. ## Watching more than one group @@ -214,15 +213,14 @@ group and the production group at once: GROUP_IDS=group.,group. ``` -Everything downstream is keyed by group. A command is answered **in the group -that asked and nowhere else**; cooldowns, daily limits, votes and the tally -are per group, so a `!cat` in the test group neither throttles nor counts -against the production one. There is no reply-target setting to get wrong — +Everything downstream is keyed by group. A command is answered **in the group that asked and nowhere +else**; cooldowns, daily limits, votes and the tally are per group, so a `!cat` in the test group neither +throttles nor counts against the production one. There is no reply-target setting to get wrong — `GROUP_ID_SEND` was removed, and setting it now fails startup rather than quietly posting the day's report into one group's chat. -The `per_group_isolation` scenario is the gate: one person asks in both -groups inside a single cooldown window and must get two images, one in each. +The `per_group_isolation` scenario is the gate: one person asks in both groups +inside a single cooldown window and must get two images, one in each. ## Daily tally @@ -237,13 +235,15 @@ Counts live in SQLite and survive a restart. ## Develop ```sh -pytest # 368 tests, 100% branch coverage, no network +pytest # 377 tests, 100% branch coverage, no network pre-commit run --all-files # ruff ALL + mypy strict + 250-line cap + gates ./scripts/e2e.sh # the real bot against the fake server ``` -No `# noqa`, no `# type: ignore`, no coverage omits. Wire format: -[DOCS-signal-envelopes.md](DOCS-signal-envelopes.md). Releases are named after the popes, in order: [DOCS-changelog.md](DOCS-changelog.md). +No `# noqa`, no `# type: ignore`, no coverage omits. Never bump `SCHEMA_VERSION` to add a table — +read `signal_bot/schema.py` first; a bump DROPs every table and throws the live tallies away, while +`CREATE TABLE IF NOT EXISTS` is additive and safe. Wire format: [DOCS-signal-envelopes.md](DOCS-signal-envelopes.md). +Releases are named after the popes, in order, and a bump publishes to the group: [DOCS-changelog.md](DOCS-changelog.md). Latency: [DOCS-latency.md](DOCS-latency.md). ## Licence