docs: record the traps this session hit

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Krzysztof Rudnicki 2026-08-30 14:44:28 +02:00
parent b70730fc7a
commit e102e413ff
4 changed files with 86 additions and 14 deletions

View File

@ -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ł

46
DOCS-latency.md Normal file
View File

@ -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.

View File

@ -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/`.

View File

@ -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.<base64-of-test>,group.<base64-of-prod>
```
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