Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions docs/DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -484,7 +484,11 @@ removes the lines the answer covered. A line that arrives while the turn runs is
when it asks (`incoming`, section 6); the answer names the ones it covered, and the rest stay for the
next turn. An answer that covers one message replies to it; one that covers several is sent plainly.
On an exception, a timeout or `kept: false` the app posts `ai_down` with the keyboard, applies no
block, and leaves every line pending, handed-over ones too, so they go with the next turn. A `typing`
block, and leaves every line pending, handed-over ones too, so they go with the next turn. An
exception with a numeric `retry_after` (seconds) means the provider is out of capacity: the app posts
`ai_busy` with the wait and the keyboard, once per busy spell, keeps every line pending, and the
first tick after the wait (at least 60 s) runs the chat's turn again by itself; lines that arrive
meanwhile wait and go with it. A `typing`
chat action is sent once at the start.

**Restarts.** A redeploy can replace the container at any moment, and it loses no message. Telegram's
Expand Down Expand Up @@ -742,6 +746,11 @@ The calendar plan is there only with `calendar.events` in the profile (5.6).
escritos. Puedes anotarlo con los botones y, si me lo vuelves a escribir más tarde, lo leo.» plus the
keyboard. No block is applied. Her message stays pending and goes with her next one. Nothing is
retried in the background.
- The plugin is out of capacity (its error has `retry_after`): «Ahora no puedo contestar: no me queda
capacidad hasta dentro de unos 66 minutos. Entonces te contesto a todo, sin que haga falta repetir
nada. Mientras, puedes anotarlo con los botones.» plus the keyboard, once per busy spell (hours from
90 minutes up). Her messages stay pending, the ones she sends meanwhile wait with them, and the
turn runs again by itself after the wait; a restart meanwhile answers them at once.

### 5.5 Settings from the chat

Expand Down Expand Up @@ -919,6 +928,10 @@ Return value, either a bare string (no conversation kept) or:
- `kept: false` means the provider did not store the turn. The app treats it as a failed turn: it
posts `ai_down`, applies no block from `text`, and sends the same `new` lines again with the next
turn. A plugin that has a usable answer returns `kept: true` with it.
- A provider out of capacity raises an exception with a numeric `retry_after` attribute (seconds
until it has capacity again). The app tells the chat the wait once and runs the turn again by
itself after it (5.2). A `RuntimeError` subclass keeps an older app working: it reads it as any
other failure.
- The plugin must return before `settings.timeout_s`; the app cannot stop its thread. A plugin that
outlives the wait would store a turn the app never saw.
- The record and settings blocks come back inside `text`; the app extracts them. A provider that
Expand Down Expand Up @@ -971,6 +984,9 @@ locale. English source strings and their Spanish translations:
| `line_deleted` | • {date}: deleted | • {date}: borrado |
| `invalid_block` | I could not note that properly, so I saved nothing. Can you tell me with the buttons? | No he sabido anotarlo bien, así que no he guardado nada. ¿Me lo dices con los botones? |
| `ai_down` | Right now I cannot read written messages. You can note it with the buttons, and if you write it again later I will read it. | Ahora mismo no puedo leer mensajes escritos. Puedes anotarlo con los botones y, si me lo vuelves a escribir más tarde, lo leo. |
| `ai_busy` | I can't answer right now: I have no capacity left for {wait}. I'll answer everything then, with no need to repeat anything. Meanwhile you can note it with the buttons. | Ahora no puedo contestar: no me queda capacidad hasta dentro de {wait}. Entonces te contesto a todo, sin que haga falta repetir nada. Mientras, puedes anotarlo con los botones. |
| `busy_minutes` | about {n} minutes | unos {n} minutos |
| `busy_hours` | about {n} hours | unas {n} horas |
| `no_ai` | Without the assistant I cannot read written messages. You can note it with the buttons: | Sin el asistente no entiendo mensajes escritos. Puedes anotarlo con los botones: |
| `voice_echo` | 🎤 {text} | 🎤 {text} |
| `voice_off` | Right now I cannot listen to voice notes. Can you write it to me? | Ahora mismo no puedo escuchar notas de voz. ¿Me lo escribes? |
Expand Down Expand Up @@ -1170,7 +1186,7 @@ joins a group or its abilities change.
| `test_records.py` | derive_starts reproduces the fictional log's flags; the gap rule at exactly 10 vs 11 days; `none` and `spotting` never start a cycle; replace-field semantics; delete; untouched lines byte-identical; validate_block refuses a future date, a date older than 90 days, pain 11, an unknown flow, an unknown symptom, an unknown key, 15 days, each with a reason code that does not contain the value; a Change with before == after is empty |
| `test_cycle.py` | `period_max_day` moves the last daily ask and the catch-up day; lengths outside 20..45 dropped; median and window; fewer than 3 lengths; period length; `bot_day` at 04:59 vs 05:01; DST days 2026-03-29 and 2026-10-25; each phase kind with its exit; day 8 is `period` only while days are missing |
| `test_schedule.py` | quiet before the window; the ask at `ask_at`, not before, not twice, not after `ask_at + 2 h`; `ask_at` 23:00 with ticks at 22:59, 23:00, 00:30 and 01:00 the next day (no, yes, yes, no); skipped when today has a flow; a restart on the same day asks no second time; late asks daily; a 60-day frozen run counts the messages (at most one scheduled per bot day); missing days older than 3 never asked; the day-8 catch-up card once, with the catch-up wording, and nothing on day 8 when nothing is missing |
| `test_bot.py` | the three-tap flow edits one message; a start from the card asks about the day before, a toggle there writes a symptom-only day and leaves the start alone, and the step is skipped when that day already says something or the day is not a start; the eve toggles follow what she logs there most; `/resumen` tells the day-before pattern from three starts on; `period_max_day` from the profile moves the last daily ask; a `settings` block rewrites the profile by line, commits, pushes and takes effect, a refused one (each reason, and broken JSON) changes nothing; `/hora` needs no assistant and the next tick asks at the new hour; a profile pulled from the remote is reloaded and a bad one refused; `set_profile_lines` keeps every other byte; `/estado` shows the ask hour; a stale keyboard works by date; a callback dated 91 days back gets the toast and writes nothing; «Igual que ayer»; the picker; «Ya no sangro» closes and posts the closing line; a close undone and redone posts it again; day 7 line; pain 9 sends `pain_high` once, 9 again does not, 3 then 9 does; «Contarte más» sends selective force_reply with the mention; a reply triggers a turn with the turn shape of section 6; a turn names its chat and the sender of its oldest line, an admin's when that line is the admin's; a reply to someone else's photo, file, video, voice note or text names its kind, sender and UTC date on a text, caption, voice or `[not to you]` line, a reply to the bot or to a topic's first message does not; a message mentioning the bot does too; the record is written and the confirmation is built from the Change; a block whose days are unchanged posts no «Anotado:»; a future-dated block writes nothing; plugin raises, the keyboard comes and the line stays pending; `kept: false` posts `ai_down`, writes nothing, and the resend yields exactly one answer; 20 queued lines are capped to 10 and the next turn succeeds; `/nuevo` clears pending lines; a message arriving mid-turn is not dropped; an answer ending in "?" carries force_reply; plain group text from her ignored with privacy on, answered with privacy off, an admin's plain text ignored in both; a stranger's tap gets the toast; `/anotar ayer mucho 6 calambres`; `/estado` admin-only and every phase kind renders through a locale key; migration; a voice note with no mention is heard, echoed under the note and sent as `[voice note]`; an audio file and an admin's note in private; strangers' and other chats' notes ignored; `voice_off` (no plugin, no `transcribe`, `NotImplementedError`), `voice_failed` (plugin, empty text, download, `getFile`) and `voice_too_long` (on arrival and from `getFile`); a heard note is never heard twice; the echo is escaped and cut; a photo with a mention in its caption reaches the turn as `[photo: inbox/<id>-foto.jpg]` with the largest size's bytes in `attachments`, and no caption word is logged; an admin's PDF in private is named by its folded file name; a spreadsheet or any other kind of document reaches the turn as an attachment with its own extension; a video, a GIF, a video note and each kind of sticker reach it under their own stem and extension; one over 20 MiB `file_too_big`, a failed download `file_failed` with the caption still answered; strangers' and other chats' photos are ignored; without an assistant a photo gets `no_ai`; photos past the turn budget go with the next turn on their own; a bot that is a group admin reads her plain text; `later` posts after the answer, fails with `later_failed`, says nothing when empty, runs one job at a time in order without holding turns, and never runs for a turn not kept |
| `test_bot.py` | the three-tap flow edits one message; a start from the card asks about the day before, a toggle there writes a symptom-only day and leaves the start alone, and the step is skipped when that day already says something or the day is not a start; the eve toggles follow what she logs there most; `/resumen` tells the day-before pattern from three starts on; `period_max_day` from the profile moves the last daily ask; a `settings` block rewrites the profile by line, commits, pushes and takes effect, a refused one (each reason, and broken JSON) changes nothing; `/hora` needs no assistant and the next tick asks at the new hour; a profile pulled from the remote is reloaded and a bad one refused; `set_profile_lines` keeps every other byte; `/estado` shows the ask hour; a stale keyboard works by date; a callback dated 91 days back gets the toast and writes nothing; «Igual que ayer»; the picker; «Ya no sangro» closes and posts the closing line; a close undone and redone posts it again; day 7 line; pain 9 sends `pain_high` once, 9 again does not, 3 then 9 does; «Contarte más» sends selective force_reply with the mention; a reply triggers a turn with the turn shape of section 6; a turn names its chat and the sender of its oldest line, an admin's when that line is the admin's; a reply to someone else's photo, file, video, voice note or text names its kind, sender and UTC date on a text, caption, voice or `[not to you]` line, a reply to the bot or to a topic's first message does not; a message mentioning the bot does too; the record is written and the confirmation is built from the Change; a block whose days are unchanged posts no «Anotado:»; a future-dated block writes nothing; plugin raises, the keyboard comes and the line stays pending; a busy plugin (`retry_after`) is told once in minutes or hours, holds every line through later messages and a restart, and the tick after the wait answers them all with nobody writing again; `kept: false` posts `ai_down`, writes nothing, and the resend yields exactly one answer; 20 queued lines are capped to 10 and the next turn succeeds; `/nuevo` clears pending lines; a message arriving mid-turn is not dropped; an answer ending in "?" carries force_reply; plain group text from her ignored with privacy on, answered with privacy off, an admin's plain text ignored in both; a stranger's tap gets the toast; `/anotar ayer mucho 6 calambres`; `/estado` admin-only and every phase kind renders through a locale key; migration; a voice note with no mention is heard, echoed under the note and sent as `[voice note]`; an audio file and an admin's note in private; strangers' and other chats' notes ignored; `voice_off` (no plugin, no `transcribe`, `NotImplementedError`), `voice_failed` (plugin, empty text, download, `getFile`) and `voice_too_long` (on arrival and from `getFile`); a heard note is never heard twice; the echo is escaped and cut; a photo with a mention in its caption reaches the turn as `[photo: inbox/<id>-foto.jpg]` with the largest size's bytes in `attachments`, and no caption word is logged; an admin's PDF in private is named by its folded file name; a spreadsheet or any other kind of document reaches the turn as an attachment with its own extension; a video, a GIF, a video note and each kind of sticker reach it under their own stem and extension; one over 20 MiB `file_too_big`, a failed download `file_failed` with the caption still answered; strangers' and other chats' photos are ignored; without an assistant a photo gets `no_ai`; photos past the turn budget go with the next turn on their own; a bot that is a group admin reads her plain text; `later` posts after the answer, fails with `later_failed`, says nothing when empty, runs one job at a time in order without holding turns, and never runs for a turn not kept |
| `test_repo.py` | one commit after 60 s quiet for three taps; flush on SIGTERM; the human clone pushes a profile edit between the bot's commit and push, the bot rebases and both land; the human clone changes a line of `log.jsonl` the bot also changed: the pull completes, the bot's line wins, the human's other edits survive, the push lands; the remote refuses the push (a hook that exits 1): the commit stays, `/estado` counts it, the next flush pushes after the hook is removed (positive control); `/deshacer` on a dirty file restores HEAD with no commit, on a clean `log:` HEAD makes an `undo:` commit, and on an `undo:` HEAD says nothing to undo; a pull that changes the profile raises the reload flag and one that changes only the log does not; `write_profile` commits the profile alone (a dirty log stays out), `flush` pushes it, and a refused or failed commit leaves the file and the index as they were (a pre-commit hook that exits 1, then removed as the positive control); commit messages contain no flow or symptom words; the token never appears in `git config` |
| `test_applehealth.py` | a fictional export in a zip with a localised member name and a `_cda.xml` beside it; the three value forms; metadata child counted; `NotPresent` skipped; heaviest of a day; `IntermenstrualBleeding` to `spotting`; a `startDate` at 23:30 +0000 lands on the next day in the profile timezone; chat days never overwritten; a rerun is a no-op; `--dry-run` writes nothing |
| `test_turns.py` | section 5.2: one answer per burst, lines handed to a running turn, edits, choices as buttons, asks; a line queued before a restart and a turn killed mid-flight are answered by the next process |
Expand Down
2 changes: 1 addition & 1 deletion supercycletracker/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""SuperCycleTracker: a menstrual cycle tracker that lives in a Telegram group."""

__version__ = "0.4.10"
__version__ = "0.5.0"
Loading
Loading