Browse Source

docs(openwiki): refresh wiki for web panel, function keys and global autoreply

- add integrations/web-control-panel page (routes, X-Wh-Request guard,
  AppEvent::Web bridge, quit flow, browser page, security caveats)
- app-state, terminal-ui, quickstart: F2/F3/F4/F10 bindings, list filter,
  local chat deletion, global "AI tutte" autoreply, web requests
- runtime: optional wh-web thread started from web_port
- history-store: settings table, global autoreply flag, delete_chat
- ai-client: ai_retries, retry policy and linear backoff
- configuration: ai_retries and web_port keys, template file
- whatsapp-client: fatal-only app state sync reporting, refreshed refs
- ai-autoreply: enabled = global OR per-chat in every path, retries
- unit-tests: updated counts (214) including the web suite
- regenerated claims, indexes, manifest and last-update metadata
softword 2 days ago
parent
commit
f311c2a3d7
31 changed files with 1437 additions and 650 deletions
  1. 118 42
      openwiki/.claims/architecture/app-state.json
  2. 22 12
      openwiki/.claims/architecture/runtime-and-event-loop.json
  3. 36 22
      openwiki/.claims/architecture/terminal-ui.json
  4. 49 17
      openwiki/.claims/concepts/history-store.json
  5. 80 28
      openwiki/.claims/integrations/ai-client.json
  6. 132 0
      openwiki/.claims/integrations/web-control-panel.json
  7. 28 10
      openwiki/.claims/integrations/whatsapp-client.json
  8. 58 30
      openwiki/.claims/operations/configuration-and-data.json
  9. 23 9
      openwiki/.claims/quickstart.json
  10. 46 32
      openwiki/.claims/testing/unit-tests.json
  11. 56 28
      openwiki/.claims/workflows/ai-autoreply.json
  12. 2 2
      openwiki/.last-update.json
  13. 47 40
      openwiki/.page-manifest.json
  14. 142 79
      openwiki/architecture/app-state.md
  15. 3 3
      openwiki/architecture/index.md
  16. 36 26
      openwiki/architecture/runtime-and-event-loop.md
  17. 51 38
      openwiki/architecture/terminal-ui.md
  18. 49 28
      openwiki/concepts/history-store.md
  19. 1 1
      openwiki/concepts/index.md
  20. 1 1
      openwiki/index.md
  21. 63 37
      openwiki/integrations/ai-client.md
  22. 2 1
      openwiki/integrations/index.md
  23. 151 0
      openwiki/integrations/web-control-panel.md
  24. 49 39
      openwiki/integrations/whatsapp-client.md
  25. 56 33
      openwiki/operations/configuration-and-data.md
  26. 1 1
      openwiki/operations/index.md
  27. 32 23
      openwiki/quickstart.md
  28. 1 1
      openwiki/testing/index.md
  29. 22 16
      openwiki/testing/unit-tests.md
  30. 79 50
      openwiki/workflows/ai-autoreply.md
  31. 1 1
      openwiki/workflows/index.md

+ 118 - 42
openwiki/.claims/architecture/app-state.json

@@ -1,14 +1,14 @@
 {
   "schemaVersion": 1,
-  "pageVersion": "sha256:0bfeac2d0428869dc0ca2ded7ee01713526fb6f573a921826fe0a1b96b3d4c7b",
+  "pageVersion": "sha256:34c88f57821c236e84d8cbbd5e3868cc3ff6629a5c6da55afacfbd3989da5c5a",
   "claims": [
     {
       "id": "claim_daef343b267c45a987738a3daf7e404d",
-      "statement": "AppEvent has variants Key, Redraw, Wa, AiReply, CooldownElapsed and Sent.",
+      "statement": "AppEvent has variants Key, Redraw, Wa, AiReply, CooldownElapsed, Sent and Web; Web carries a WebRequest from the web control panel.",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L56-L79",
-          "version": "repo-lines-v1:sha256:4f8895b8f084334866be94b1935ed3aed873fe96d785e516f1f0d16b6a2ab219:eyJzZWxlY3RlZExpbmVDb3VudCI6MjQsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImI4OWMyOWE3NjJmOGZhMmEzZmY0NmY5MGZiZDY3N2QyYTI3MjViMjM3NTYxNjIwODdkOTRmMWI2MDg2Yzk5YzEiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjU5YWI2NTg3Y2U3NzQ5Y2FkNWYwNmU4MThjZmZlODBiNGZjZmFlNTEwZjhmMTk3ZjQwNTIyZmNhN2E3YzM4OTciLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjJlYzJiMDUyMWI2NDA2YzM3MGE3MzdmMTZhYTU0NDBkZDcyZDE1YWMxY2E2OGY5MmI1ZDc5NzUxM2I0NDk0NGQifQ"
+          "resource": "repo://src/app.rs#L73-L98",
+          "version": "repo-lines-v1:sha256:5e875e0df93e3b8f591f71b16614b5ad4d266aee0c29ef8e75203b10f4914e9d:eyJzZWxlY3RlZExpbmVDb3VudCI6MjYsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImI4OWMyOWE3NjJmOGZhMmEzZmY0NmY5MGZiZDY3N2QyYTI3MjViMjM3NTYxNjIwODdkOTRmMWI2MDg2Yzk5YzEiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjZmMGExNjA2YWZlMjU3OTU2NWMyMWNkOGU1ZjUyNzljMmY0ZWMwZWMyMjZmZjM5YjljNmY2MjllZTNkYjk0NTEiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjJlYzJiMDUyMWI2NDA2YzM3MGE3MzdmMTZhYTU0NDBkZDcyZDE1YWMxY2E2OGY5MmI1ZDc5NzUxM2I0NDk0NGQifQ"
         }
       ]
     },
@@ -17,18 +17,18 @@
       "statement": "Screen has Connecting, Pairing and Main; Focus has List and Input; ListMode has Chats and Contacts.",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L81-L101",
+          "resource": "repo://src/app.rs#L100-L120",
           "version": "repo-lines-v1:sha256:92aaf2af1678376e51c1ad2567e9dce9ab29a60eb8e773ba16594a42750ff769:eyJzZWxlY3RlZExpbmVDb3VudCI6MjEsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjY2YTEyNzUwZTQ5YzY3NDE5NGM1Yjg4ODEzOGQ5Y2JjNGI0Y2M2OTM2NjdiYmZjMzIxYjgzM2MxMWZlMTdlY2QiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjY3NmE1NDRjYzAyM2VkNDA1ZTNiOThiOGEzNTQwOTBmYWQxNmU5ZTE4ZDVlZTlmN2JhNmMzZTcwMDUzMDA5ZDEiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjFmY2RhYjQwOGQyMDcwM2JmNTU2ZWJmZjk2NGM3M2I4NGQzYWUzYzdiMzUwZTFiODNmNTA3OTE4MDUxYjA0ZjYifQ"
         }
       ]
     },
     {
       "id": "claim_0a2882d788054b5586b16e6e990f77ee",
-      "statement": "App::new builds the AI client, loads chats and contacts and starts on the Connecting screen with a ReplyGate created at the current time with Cooldowns taken from cooldown_seconds and group_cooldown_seconds.",
+      "statement": "App::new builds the AI client, loads chats, contacts and the persisted global autoreply flag, and starts on the Connecting screen with an empty filter and a ReplyGate created at the current time with Cooldowns taken from cooldown_seconds and group_cooldown_seconds.",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L165-L199",
-          "version": "repo-lines-v1:sha256:a25933c1c4dff0de5b2f69ae0e9d8a49c1940e8a4b0643b39cd8981e74a78bbd:eyJzZWxlY3RlZExpbmVDb3VudCI6MzUsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjdmNTYzZjU4ZTBlOWQyYjgxMjgwMTZhOWEyZGZhMTU2YjgzNTM0NWYzOTkzYTVhYjdkNjVhNWU4ODFmMDg0Y2UiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjBhOGJlZTA0NjIzODBmZjdmN2Y0ZDZhYzE2MGViODRmYjk3YjgwZGRjMjM2ZGFmZWY4NTQ4OTYxYjcwZDQ4ZmQiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImU3M2U3ZTFjNDIxZDk5NzQ5ZWVmNzNjNjJiNmE4ODAyODhjNGNhZDM4ZTYwZWQyMTNjNmM1ZDI1ZjAyMDU3OGMifQ"
+          "resource": "repo://src/app.rs#L191-L229",
+          "version": "repo-lines-v1:sha256:b8e7258ce42c14156856b5548408686e166ef6c02cf336560ffc75ccd7ecf145:eyJzZWxlY3RlZExpbmVDb3VudCI6MzksImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjdmNTYzZjU4ZTBlOWQyYjgxMjgwMTZhOWEyZGZhMTU2YjgzNTM0NWYzOTkzYTVhYjdkNjVhNWU4ODFmMDg0Y2UiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjBhOGJlZTA0NjIzODBmZjdmN2Y0ZDZhYzE2MGViODRmYjk3YjgwZGRjMjM2ZGFmZWY4NTQ4OTYxYjcwZDQ4ZmQiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImU3M2U3ZTFjNDIxZDk5NzQ5ZWVmNzNjNjJiNmE4ODAyODhjNGNhZDM4ZTYwZWQyMTNjNmM1ZDI1ZjAyMDU3OGMifQ"
         }
       ]
     },
@@ -37,7 +37,7 @@
       "statement": "handle_wa_event maps Qr to Pairing, Connected to Main with status Connesso, LoggedOut to Connecting, OfflineSyncCompleted to the startup catch-up, and routes Contact and Message events (with addressed_to).",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L226-L251",
+          "resource": "repo://src/app.rs#L257-L282",
           "version": "repo-lines-v1:sha256:9c2eb0130ed8fe6d1f4a7364331089bc43b4a8f4aa42c0a29d8794946dd32f3f:eyJzZWxlY3RlZExpbmVDb3VudCI6MjYsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjZkOGNmMDJkODU2OWY1YTQ3ZjRiNmY0Zjc5MzBmNzkzMTczM2VmNGE4NDMwODNmYmZjMzAzNmNiM2JiMjc5MGQiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImM5NDkyZWM5YTM2ZThkODU0NjE1MmI4MjJkNTdmZDIwMDk4OTE1MDMwYTIwZDliZGExZDVmYWQ0NGY4ZjFiNWYiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImI2ZDczY2I1NTUwODdmZGQwZTc4MzhjZjM4MWZmZmRmMzljMzk2M2VmZWJhMGE0ZjQyNzg4OWE0MGUwODYwZWQifQ"
         }
       ]
@@ -47,18 +47,22 @@
       "statement": "on_message returns early when History::save_message reports the message was not new.",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L273-L282",
+          "resource": "repo://src/app.rs#L304-L313",
           "version": "repo-lines-v1:sha256:eb6c3b3607fadb274e63e9fe5f6be1f2f35bb2e82b3beea4cba64c0b5f362f1e:eyJzZWxlY3RlZExpbmVDb3VudCI6MTAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjYyNjcxZjMwOGZhYjk0MTMwNzgwNDU4MzFhNDMxYjJjZTM3MzhhMTEyMzMyNDg4OGY2ZTgzNTY1ODhjYjNmOTciLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImQ1Zjg3MzRjYWZkNWU0MTNiNTUyZDMzZjFmMThlOTEyNWI2YTkwMTFhODU2OTM2YmJjMzRmMzAxMGM4Yzg1ZjkiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjU2YzA0ODg4Njk3MzQ4ODBlMTJiNTM0MmNmZDI3ZjU4YTU0M2Q1NjE4YWY1M2JlZjRkNmVhZDYyYjhiZmQ1N2YifQ"
         }
       ]
     },
     {
       "id": "claim_a5e4a2575e2a4890b96ffeed57e9f621",
-      "statement": "Ctrl+C quits on any screen, non-main screens ignore other keys, Ctrl+A toggles autoreply, Tab switches focus and F2 switches the list mode.",
+      "statement": "F10 quits on any screen and non-main screens ignore other keys; on the main screen F2 switches the list mode, F3 toggles the open chat's autoreply and F4 toggles the global autoreply regardless of focus, Tab switches focus, and Ctrl command chords are swallowed.",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L528-L550",
-          "version": "repo-lines-v1:sha256:19f7c9cbdcad7bc82262bb7f58fc219966919ee24ae8366f08f897ab42db3764:eyJzZWxlY3RlZExpbmVDb3VudCI6MjMsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjNmODVjMjY1ZjBmMjNkOTA0NDcwMjYxNWZiM2NkMzQ2ODMxYTFiZWNiOTRlYzNkODYyNDYxYzFkNWEwMGQzNDUiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjIyOTViMDhkNjdiODVjNzc0N2YzYzI1NzgzOWQ3OWFiMmFmNTBiNmYzNjkyYmNmYzJlZmIwY2I3OGExZWExZTIiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6Ijk2ZWU5ZDllODg2ODFmN2JmM2M4ZmZiZjEzZDgyODk2ODM4MGY2Y2E0M2Y1ZGE4ODI5MmU1MjY3ZDRjMDhlOWYifQ"
+          "resource": "repo://src/app.rs#L22-L29",
+          "version": "repo-lines-v1:sha256:1b77b4b729c472be09a45c5473cf8594fc6554a18cc8f686ece6245ff2aa26ec:eyJzZWxlY3RlZExpbmVDb3VudCI6OCwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiYmQxOTM5NWQwMDUxZDZhMDgzMGQyZDk5ZmE1OThkNTg3YTI5ZmQ1MDQ5OTA0ZDU5Y2I1MjZiZDdjNzg4MGIwZSIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiYWY2MjM3MDg5YjkxZDEwN2I2NjQ1Nzk3M2E4YmEyYTcxZDg3MDQ0ZWE5MWU5MThjZGUyMDVmODZhY2Q0NmM5MyIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiMWVmZGQwMDhhMTYzNjZhM2YxN2I2NjUzYjI3NzIxOGRjNDY1NGIwYWNhMTkwYjViYWJkNzA2YjIzYjEwNDA3NyIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiZjM1MGFjYmZhYzdlNDEzZmFkNWYxMzk0NTQwYWE3MWM3YWY3ZWZmMDA4MDliYmMxYTlhNzQxZGRhNDFiOGYwZCJ9"
+        },
+        {
+          "resource": "repo://src/app.rs#L573-L603",
+          "version": "repo-lines-v1:sha256:37edcb8b1691dbaa467f7d59b80cdc3781376ce6ce1fc1783f167c52ae5ce169:eyJzZWxlY3RlZExpbmVDb3VudCI6MzEsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjNmODVjMjY1ZjBmMjNkOTA0NDcwMjYxNWZiM2NkMzQ2ODMxYTFiZWNiOTRlYzNkODYyNDYxYzFkNWEwMGQzNDUiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjE5OTRiMGIxZDNhNDA1MzRjYTFiZTVmNDdiNTRlNWY3ZDg3ZTZlMGMxMWE3ZjU5ZjA0NGU2ZGJmNTkyYzNlZDYiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6Ijk2ZWU5ZDllODg2ODFmN2JmM2M4ZmZiZjEzZDgyODk2ODM4MGY2Y2E0M2Y1ZGE4ODI5MmU1MjY3ZDRjMDhlOWYifQ"
         }
       ]
     },
@@ -67,36 +71,36 @@
       "statement": "In input focus characters are appended, Backspace deletes, Esc returns to the list and Enter submits the trimmed text to the open chat.",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L570-L594",
+          "resource": "repo://src/app.rs#L726-L750",
           "version": "repo-lines-v1:sha256:93f1a991412191cf1e47a90542afc809bc43aaab50594660e57b57012282758d:eyJzZWxlY3RlZExpbmVDb3VudCI6MjUsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6Ijc1ZWM1NzZmZGY5ZTZhYjIyYmM3ZTgyYWM2NzNiY2EyM2MxNTZkZjE0MTRmZGFjZjRlNzcwOTEzYmI3Yzg1OTYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImUxNzg4NWVhNGE5MGJjODU2N2NiZDFmYjM0M2FlNTQ0Y2EwMTU5ZDZkNTA0ZGU3ZjcyNWFiMWIyYjg0YzYwZGMiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjBlYTM2NTI1MWJlYzc3N2JhYTBmYTZjMGEzZTZmOGRiYzliNzhiNmQ5YTNmNmI4ODVmODBmY2RkMmQzYWFhMmEifQ"
         }
       ]
     },
     {
       "id": "claim_4b0ec5e5dcd04f838b031e75b64428de",
-      "statement": "Enabling autoreply requires an AI client first and then a chat accepted by supports_autoreply (private chats and groups); disabling is always allowed and the flag is persisted.",
+      "statement": "Enabling a chat's autoreply requires an AI client first and then a chat accepted by supports_autoreply (private chats and groups); disabling is always allowed and the flag is persisted with History::set_autoreply.",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L610-L624",
-          "version": "repo-lines-v1:sha256:16fb5b82a9f45f8cb885a7bf638ad0f4ff3b641766d72a83ad8e63bc0d37e690:eyJzZWxlY3RlZExpbmVDb3VudCI6MTUsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImZkMGU5N2I5YzI5ODYyY2E0MDZkYjkxZDE5ZWU0Yzc4OTU3Yzg1MTNmZmRjMzk3NjM4NWU0NDY1NzhlNDk5YjEiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjFhYTRkM2FkY2MzNTI3NzIxNTI5NjJkMTFiZTY0MzMzMTBiYjg0YjkxYjRlMDE2OGNkY2QzYzY2ZGYyYTU3NWMiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImU2ZWQ1MmZlYjhjZTliMjNiYmNiYTkwMGFkYjk0MzE3MjA4ODNhZTk0ODY4NmQyNjhlNmRkMjFlNjljYTU3YjkifQ"
+          "resource": "repo://src/app.rs#L1132-L1140",
+          "version": "repo-lines-v1:sha256:ba5bea3063970ee3b66b2d54fc0529a98bf3972bf7e9a5cab37c6d0bf4659ff7:eyJzZWxlY3RlZExpbmVDb3VudCI6OSwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiZmFmNzM0NDVmZTU0ZTJjMzIyMDBiMGRiNDhlM2MyOGZlZDgyZDk3NWY0Mzg4MjM0NjA5NjhlZmM4ZmU1ZjJmOSIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDEyY2EzNDVjY2Y3NWJmOWMwODA2YmNlNjk1YmU4ZGU4MDhiNzk5ODQyNTFhN2E1NGQyMDJjZjYxMDFkZDQ1MSIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiOWM5Yjc1NjM5OGI3MTI0NGNmYThkOTk4ZGRlOTQwZWRmODk1MmNkMDEwMTYyMzM1YmEwMzRjYjkxZGZmMjRiZSIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiNDQwM2Q5ZmY0OGI3ZmI2ZDcyM2RlNWM5YjMxODkzMTNlMjQ2NzBhMWE5NjJhYWUxZTYxODRhMjllZmM3MWM1ZCJ9"
         },
         {
-          "resource": "repo://src/app.rs#L890-L898",
-          "version": "repo-lines-v1:sha256:ba5bea3063970ee3b66b2d54fc0529a98bf3972bf7e9a5cab37c6d0bf4659ff7:eyJzZWxlY3RlZExpbmVDb3VudCI6OSwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiZmFmNzM0NDVmZTU0ZTJjMzIyMDBiMGRiNDhlM2MyOGZlZDgyZDk3NWY0Mzg4MjM0NjA5NjhlZmM4ZmU1ZjJmOSIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDEyY2EzNDVjY2Y3NWJmOWMwODA2YmNlNjk1YmU4ZGU4MDhiNzk5ODQyNTFhN2E1NGQyMDJjZjYxMDFkZDQ1MSIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiOWM5Yjc1NjM5OGI3MTI0NGNmYThkOTk4ZGRlOTQwZWRmODk1MmNkMDEwMTYyMzM1YmEwMzRjYjkxZGZmMjRiZSIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiODk0ZTBjYWQ4MWM1MGI5ZjgxYTk4MjZmMDVjNTJmNjQwYTRkYzc3N2JhZDI0YzYwYTAyNDk3OGVlYmYxZGMwYyJ9"
+          "resource": "repo://src/app.rs#L781-L794",
+          "version": "repo-lines-v1:sha256:cbcb0d80e707fedd21b3de7081436677468c63c025abb23dab5e5a6ab0ad90cc:eyJzZWxlY3RlZExpbmVDb3VudCI6MTQsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjA0ZTRlYmRmYjA5YmI0NTM3YzBhZDM0MWUxNGY5NTkzYjEwYWE5ZWJiOTM0ZTNiOGJhMGViY2ZiYjlhYjBkZmYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjI1Zjg5YTg3NjBkYWQ3NjUxYmZiZGQ4ZGFkYTM4OGRmZDUwZmU2NDczOGUwMzM4YjIzZTM5NzM4Y2Y1NTRmOWUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImUzZWNmNGIzNzRlN2M0ODFlZjUzODQ1Y2Y1YjlmNDI5MDk3NmQxYTlmNDlkOWY5NWJjNjIxNWEyY2VhMDYyOTAifQ"
         }
       ]
     },
     {
       "id": "claim_4989af934f564cf789a6028f36e19728",
-      "statement": "Opening a chat loads its display name, autoreply flag and the last 200 messages and moves focus to the input.",
+      "statement": "Opening the selected entry of the filtered list loads its display name, autoreply flag and the last 200 messages and moves focus to the input.",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L19-L19",
-          "version": "repo-lines-v1:sha256:240488cee8382e0e531b9f13fd0aa941c72d0390e7e89c4430811cbc76aa6610:eyJzZWxlY3RlZExpbmVDb3VudCI6MSwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiMjQwNDg4Y2VlODM4MmUwZTUzMWI5ZjEzZmQwYWE5NDFjNzJkMDM5MGU3ZTg5YzQ0MzA4MTFjYmM3NmFhNjYxMCIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiMjQwNDg4Y2VlODM4MmUwZTUzMWI5ZjEzZmQwYWE5NDFjNzJkMDM5MGU3ZTg5YzQ0MzA4MTFjYmM3NmFhNjYxMCIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiODViYTNhNjRkYjcxZTdiNGQwMjIyOTIxYjllMDk3NWFhNGM3MDQ1NzQ0NWYwYTc0OWU1MGJhYTljZTQ5MmI1MiIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiNDcyMjFlYzQ5YThiY2FiYTFkNzUwNTE0MWZhYzY0MDhkOGZmMmUwMjUyMDgwZTZlMGYwNzQ1MDMzMjY3ZjQwZSJ9"
+          "resource": "repo://src/app.rs#L19-L20",
+          "version": "repo-lines-v1:sha256:052992c9f8e9b996b43554cee0a2351140f142d6d9781a2b3217356c40bf6cb1:eyJzZWxlY3RlZExpbmVDb3VudCI6MiwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiODdkOGI3ZDU2NTM2Yjg3MDE0OGY1Y2U5YzQ2YWQxN2FmMzI4ZDQ5NzRjMmNlMTVhODNhOWQ0ODllZTBjNDY5MiIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiMjQwNDg4Y2VlODM4MmUwZTUzMWI5ZjEzZmQwYWE5NDFjNzJkMDM5MGU3ZTg5YzQ0MzA4MTFjYmM3NmFhNjYxMCIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiMGY4MjlhNmQ5ZTNmMmZjNjc2ZTdkYTAzNzA2YjY4ZjQ1OTE5ZjhiMTkyNTIyN2JiYTMwMDkwZGZmYzYwN2Q0MyIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiNDcyMjFlYzQ5YThiY2FiYTFkNzUwNTE0MWZhYzY0MDhkOGZmMmUwMjUyMDgwZTZlMGYwNzQ1MDMzMjY3ZjQwZSJ9"
         },
         {
-          "resource": "repo://src/app.rs#L630-L644",
-          "version": "repo-lines-v1:sha256:aded80cef132a5253ce23fe8d7fc3a9a08ee4998777e70cac1d8568fa2c7a699:eyJzZWxlY3RlZExpbmVDb3VudCI6MTUsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjU0NmM2MGMyMDFmZDM3MThkMzVjZTZhNzRlMTdkMmM2Y2E2Yjg5N2ZmMDc3Y2RkMDcyZGZkZDgzODFhZjVhNWMiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjUzMjlhMjljNjFlZGI3Y2E3NTEzZTQ0NTgxMDJhZTAwNWU1MGM2N2QyYWFhMTdjMjRmMTBhMTRmN2MxOTExNzIiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImNjYzVlMzg4ZThmOTY0ZGY1OTFjZDM1Y2FlZjk5ODM0MmRjNTllNGU5OTMwMmMyYmI0MjMyZmMxMzdhM2Q1ZjkifQ"
+          "resource": "repo://src/app.rs#L862-L878",
+          "version": "repo-lines-v1:sha256:0aceaa3d72a442f277babfd7a811f8fa14bea8944c8dee6f9657dcad479fd9d8:eyJzZWxlY3RlZExpbmVDb3VudCI6MTcsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjU0NmM2MGMyMDFmZDM3MThkMzVjZTZhNzRlMTdkMmM2Y2E2Yjg5N2ZmMDc3Y2RkMDcyZGZkZDgzODFhZjVhNWMiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjUzMjlhMjljNjFlZGI3Y2E3NTEzZTQ0NTgxMDJhZTAwNWU1MGM2N2QyYWFhMTdjMjRmMTBhMTRmN2MxOTExNzIiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImNjYzVlMzg4ZThmOTY0ZGY1OTFjZDM1Y2FlZjk5ODM0MmRjNTllNGU5OTMwMmMyYmI0MjMyZmMxMzdhM2Q1ZjkifQ"
         }
       ]
     },
@@ -105,7 +109,7 @@
       "statement": "spawn_vision_request runs a single task that tries the vision completion and falls back to the courtesy text completion, combining both errors if both fail.",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L781-L822",
+          "resource": "repo://src/app.rs#L1023-L1064",
           "version": "repo-lines-v1:sha256:5f2de4b5568e20c68dce45723dee3fac0fdb727c5c0babf4f85e4baaa14a44e9:eyJzZWxlY3RlZExpbmVDb3VudCI6NDIsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6Ijk0MTM1OGQzZmUwMGY3ZDM4YWM4MjNjMDc4OWNlZmU2NTNhMTE2YzkyZmNjNWMyYjYwNWY4ODA0MjA2MzRjYTAiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImQxZTM5MDcyMDkzN2I1OWI2MzAyZjNjZGUwY2Y5NzgyMTUxZjc2OTg0MDM4Yjc4ZjBiYzJmNjkxYjFmZTcwM2EiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImU0MzNjMDFmNDAzMDNjNmE3M2FiOWRjYzAxY2Y3ZTg2ZWIyMGU3ZmEzZjdjNzVhMjQ1MzMyYTRmYTdmYmYwOWMifQ"
         }
       ]
@@ -115,18 +119,18 @@
       "statement": "spawn_send sends the text via WaHandle::send_text and emits AppEvent::Sent.",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L859-L868",
+          "resource": "repo://src/app.rs#L1101-L1110",
           "version": "repo-lines-v1:sha256:946a66552037dc4ae7eff9cf6c602520fd019e2700d8d6cc4dc93c7db36bb837:eyJzZWxlY3RlZExpbmVDb3VudCI6MTAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImFlOWMwM2FlM2RiZTlkZDA3YjYxNTZkZWFkNzBhNmRhYjc4MzI3M2I2Y2E0Y2JlZGI0M2E0MTI4YmJlNTdiZjgiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjMwZDEyZTUxMmY3N2FlNTEwYWQwZTk5M2I3NGNmNjdmY2ExYmRiOTllYmE3OWY3OTdjZDkxMWYyN2Y2MWQ1ODQiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjUyM2IxZmMxYjIzYjFmMWZhZWVhY2E4MTM2Yzc1YmFkOGEwNGEzMGQ5Nzk0MTc5ZDNjZjUxYjJkNGVhMDI1NTIifQ"
         }
       ]
     },
     {
       "id": "claim_3568f04cad88489a974266f0e241c354",
-      "statement": "An AI reply is sent only if autoreply is still enabled for the chat, otherwise it is discarded.",
+      "statement": "An AI reply is sent only if the autoreply is still enabled for the chat (globally or per chat), otherwise it is discarded with a status message.",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L483-L492",
-          "version": "repo-lines-v1:sha256:873d9c74889e64fc80a3ca96c2ced85669ef71a7f7c6c62bf4fc795cf2d998b7:eyJzZWxlY3RlZExpbmVDb3VudCI6MTAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImVlODZiMzQ5MjhkN2ZlZWQzNTRiNmY1OGM3MjJlNTk5YzI3ODhhNWE0Yzc2MjUxN2YzYzExYzIzMDM0OGVjOTUiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6Ijc1NjVhM2RiYzM1YWQwYzg0NDFjZThkN2YyNWQyNjQzODZhZTE4NDc3MGYzMDRkODdkYjFiMjk0OGZjM2ExNGEiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImVlYjQ1ODA1ZTI5MzcwMzMyZTY2ZDA3NzAwZjlhMDg3MDgyNWE3ZDM2NWFmYmJmNzYxOWNiYjNlMjNiNTViMWQifQ"
+          "resource": "repo://src/app.rs#L525-L534",
+          "version": "repo-lines-v1:sha256:b744280fd770d3942f92d16928b5cdc09f3df4da0b12e5f3de78cc9d7983c222:eyJzZWxlY3RlZExpbmVDb3VudCI6MTAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImVlODZiMzQ5MjhkN2ZlZWQzNTRiNmY1OGM3MjJlNTk5YzI3ODhhNWE0Yzc2MjUxN2YzYzExYzIzMDM0OGVjOTUiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6Ijc1NjVhM2RiYzM1YWQwYzg0NDFjZThkN2YyNWQyNjQzODZhZTE4NDc3MGYzMDRkODdkYjFiMjk0OGZjM2ExNGEiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImVlYjQ1ODA1ZTI5MzcwMzMyZTY2ZDA3NzAwZjlhMDg3MDgyNWE3ZDM2NWFmYmJmNzYxOWNiYjNlMjNiNTViMWQifQ"
         }
       ]
     },
@@ -135,7 +139,7 @@
       "statement": "build_ai returns no client when system_prompt is missing.",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L875-L883",
+          "resource": "repo://src/app.rs#L1117-L1125",
           "version": "repo-lines-v1:sha256:02a6b90c8547969f5229ba908e64ec2f9fd8d0547f3a7b34404429320b337fe4:eyJzZWxlY3RlZExpbmVDb3VudCI6OSwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiNzcyNTRhNjcwYWJhNGU4ZWFiNDlmM2U1OWYzNjc4NTk0OWM2ZDk1OTFlN2ZkMWY1NjA4NGU3NzBlOWM3NDJlNCIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDEyY2EzNDVjY2Y3NWJmOWMwODA2YmNlNjk1YmU4ZGU4MDhiNzk5ODQyNTFhN2E1NGQyMDJjZjYxMDFkZDQ1MSIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiMGQ3OWM4ZDU3OGFjNjM3NDFmOTgwMTlmMzlmOWRmMDA4M2RkN2YzM2U0ZDNlOWU3ODU1YzZmNmQ2MWQzNjg0MCIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiNTA2ZDU0NTM1OGJmMGQyNTFiNDhiNWZhZTE2YzIyZjkzYzgzNDM5ZTQwNzA0YzRkYzg1MzgyN2ZmNWFiNmYyNiJ9"
         }
       ]
@@ -145,7 +149,7 @@
       "statement": "is_command treats Ctrl as a command only when Alt is not held, because Windows reports AltGr as Ctrl+Alt.",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L900-L907",
+          "resource": "repo://src/app.rs#L1158-L1165",
           "version": "repo-lines-v1:sha256:a7e4215d1055bcf132d529dd3c057ef20525fbb22d5a1301c6980d41107b5124:eyJzZWxlY3RlZExpbmVDb3VudCI6OCwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiYzllOWIxZTI5MGI4MzMyZDI5YTMwZjc1N2JjN2E3N2ZlYjZmMjAxYjNjMmM5M2ZiNTQ4MDU1YTFhODVmN2MyZSIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDEyY2EzNDVjY2Y3NWJmOWMwODA2YmNlNjk1YmU4ZGU4MDhiNzk5ODQyNTFhN2E1NGQyMDJjZjYxMDFkZDQ1MSIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiMzVmNmY0NjZlNTljZmMzMDcwM2M0NWY3MmFmNjVkZTU0YWMzNjNkMzU0Y2MxYTMzMzAzNTBhY2Q0NTI0MzRjNCIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiN2JjOThkNmIwZGM0OTJjNTAxNmRmMTA4MDVhM2E1NGViOWMxODRhNWE2MTM3ZTdmNGJlY2ZhMGMyMjA1NTBkMCJ9"
         }
       ]
@@ -155,7 +159,7 @@
       "statement": "start_reply records the reply in the gate and loads the AI context with History::messages_since from context_start(now), limited to CONTEXT_MESSAGES.",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L403-L416",
+          "resource": "repo://src/app.rs#L444-L457",
           "version": "repo-lines-v1:sha256:b986ffa888b04f80920f0e7ee6c1fe15e171ea20ddac89ac45271c5446b2a64e:eyJzZWxlY3RlZExpbmVDb3VudCI6MTQsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjRiYmM5MGM3NmFlZGUwNzkzNzVkMjliNGZiMjRlMWMyMzg3ZTc0ODE2NzkyMzM2ODk5ZTE0NTA2OGExMDYyZjIiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjhiZTI4OTVhNzFkYmI4ODUzNDMxYjIwZDI1ZDI4YWQ3ZDk5ZjUxYWEzOWYyOGIyNWIxMzY4ZjdmMTMyNzMxMGYiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjA2ZWViM2RhYjZhYTAxODdiMjY5ZjlmNGY1NjUyODE3ZTljNDE0ZTQzZjQ1Nzc5OWY4YWJlMjliMjhiM2IzZmQifQ"
         }
       ]
@@ -165,38 +169,110 @@
       "statement": "maybe_autoreply replies when ReplyGate::should_reply allows it, otherwise when deferral_due returns a due time it marks the chat pending and spawns a timer that emits CooldownElapsed.",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L293-L315",
-          "version": "repo-lines-v1:sha256:57adb3d4114db569659ff9158f1f43222176ba6d001e8ae090c43d5168f42f80:eyJzZWxlY3RlZExpbmVDb3VudCI6MjMsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImViZGJkZDZkZGVmMDM5YTViMmI1YTIxZGUxMzliZjllODRmM2JhNDU4YThjZWY0ZTQzOGRiNjQ3M2EzODliODIiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjMwZmQ1Yjc2YTk4ZDRmMjZhODFiMWVjYjkyNjFkMDQzNjJkYTg2NDhhNzMzMGFjNmJjNTVkZmFiOTFmZmY4MmEiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6Ijk0NDE2M2IzN2JlYTdlZjFjNmJiMDZkODAzMTQxOGRmZjZjNTBlNjgzMmUyMWVkZTI2ZjlhN2VjODA5MDlhMTIifQ"
+          "resource": "repo://src/app.rs#L324-L346",
+          "version": "repo-lines-v1:sha256:b7e3059a255a91b3ca5ae0caad411be8cadeb3cd6d0caace25bc17f59db073f9:eyJzZWxlY3RlZExpbmVDb3VudCI6MjMsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImViZGJkZDZkZGVmMDM5YTViMmI1YTIxZGUxMzliZjllODRmM2JhNDU4YThjZWY0ZTQzOGRiNjQ3M2EzODliODIiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjMwZmQ1Yjc2YTk4ZDRmMjZhODFiMWVjYjkyNjFkMDQzNjJkYTg2NDhhNzMzMGFjNmJjNTVkZmFiOTFmZmY4MmEiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6Ijk0NDE2M2IzN2JlYTdlZjFjNmJiMDZkODAzMTQxOGRmZjZjNTBlNjgzMmUyMWVkZTI2ZjlhN2VjODA5MDlhMTIifQ"
         },
         {
-          "resource": "repo://src/app.rs#L749-L755",
+          "resource": "repo://src/app.rs#L991-L997",
           "version": "repo-lines-v1:sha256:184085a61638b88a75f3e4c9a0f6423a3e374b61c7950b7a40df4b0897a7c86b:eyJzZWxlY3RlZExpbmVDb3VudCI6NywiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiN2ZjOGY0YmMzMjEzMzJiNTA5NzlhYTdlYWM3MmNjNTIzM2E5Y2U4ZmY0NzlkMThlYTM5MjFkNzUxNzc1NDE3YSIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDEyY2EzNDVjY2Y3NWJmOWMwODA2YmNlNjk1YmU4ZGU4MDhiNzk5ODQyNTFhN2E1NGQyMDJjZjYxMDFkZDQ1MSIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiYTQxZTYwMjlhYjc2NTVlMjZkNzIzZWNlMGM4ZTdhNjQyYTc0NmI4NDcxYTcwYmNlOWEwOTg2MTM0ODUxZDM4ZSIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiMDVkMmViYWNlOGUxMTQ2MDNlNjJkNzViNWY2ZGM4MWMzMmIzODMyYjhiMGIwZTNkNWFkNjQ5MzIwNDhhNmQzOCJ9"
         }
       ]
     },
     {
       "id": "claim_817155a979284282b5a78c3baad994c8",
-      "statement": "handle_cooldown_elapsed sends one text-only reply only if take_pending reports the chat still pending and autoreply is still enabled.",
+      "statement": "handle_cooldown_elapsed sends one text-only reply only if take_pending reports the chat still pending and the autoreply is still enabled globally or for the chat.",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L324-L332",
-          "version": "repo-lines-v1:sha256:a4475810b1467d78edffc4a5b107f814025267f0c2d097968035ddcaa3012ed5:eyJzZWxlY3RlZExpbmVDb3VudCI6OSwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiYTFjZDQ1NGRkNWMwMzdlZTZiYjUwMWY2ZWFjZDgwZjZmNTE3ODNjY2NiZWE0M2Y5ZGM5Zjc4ZjllNmQ5YTc1NCIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDg5M2Y0MGUzNzgxZWI4OTZjODJkMDQ2YTA4MDY3YTExZTM1Y2RmMmYxZGZhZjI2ZmVhOTBiOGRiNjY3NjQxMyIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiNTkyYTVhYjhhOTUxZTJkNDU0OWI4MzFmYmMxMGFjODE2Mzk1NmJmODUxNjRjNGQyOTYxOTkwZjcyOGExMDE5MSIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiMjIwZTI2OTMzMWFlNTY1MmJhMzE1ZWJhN2ZjNGU1YzE1YWE0OWEzN2IyMTY0Zjc2NGViY2JhMDU4MTlkYzI0MCJ9"
+          "resource": "repo://src/app.rs#L356-L372",
+          "version": "repo-lines-v1:sha256:41cd96a610e59b5cfa51d2283c3c0297bb686cc2990f5a9e13a81349014f265e:eyJzZWxlY3RlZExpbmVDb3VudCI6MTcsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImExY2Q0NTRkZDVjMDM3ZWU2YmI1MDFmNmVhY2Q4MGY2ZjUxNzgzY2NjYmVhNDNmOWRjOWY3OGY5ZTZkOWE3NTQiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjQ4YjNlOGY3MDhlYTQwOGI0OGYyZWU3MTI4NmRiYzMyNjczNTJlZTlkMzhhNzE2ZDg3ZGVhMzAyNmQzMDk2ZWIiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjIyMGUyNjkzMzFhZTU2NTJiYTMxNWViYTdmYzRlNWMxNWFhNDlhMzdiMjE2NGY3NjRlYmNiYTA1ODE5ZGMyNDAifQ"
         }
       ]
     },
     {
       "id": "claim_c8b0ab491aad4ebf91a3b7e090cdbe2b",
-      "statement": "The startup catch-up runs once per process on OfflineSyncCompleted, replies text-only to each autoreply-enabled chat whose last message passes ReplyGate::catch_up_due within catch_up_seconds, and sets the status 'Risposte di recupero: N' when N > 0.",
+      "statement": "The startup catch-up runs once per process on OfflineSyncCompleted, replies text-only to each enabled chat (every chat when the global autoreply is on) whose last message passes ReplyGate::catch_up_due within catch_up_seconds, and sets the status 'Risposte di recupero: N' when N > 0.",
+      "evidence": [
+        {
+          "resource": "repo://src/app.rs#L378-L428",
+          "version": "repo-lines-v1:sha256:2d646f5dfd38412d2d6955e2a274df2d3b193b00b7aa4d0051b442118c63a250:eyJzZWxlY3RlZExpbmVDb3VudCI6NTEsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjJiYWY4NjRiNzJmMjM1YjJhNzAxNzllOWRiZTlkYTg1ZmY0NWM1OTg4YjY1MTUzNmQyYjNmMDM0MzRkMTA3YjgiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjJkOWY0YzAxNDlmZTYwZWNjMDY0NDU2Y2VhNTgwNWIyMmI4YjgzZTljYzY0YzVmZTc3YzhjZDNjMzQ5ODg1OWMiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6Ijc4OTU5NGE5NzIzN2Y2NjVkMmU0Y2RhMTIyZmY1NTIyNzJjMjVmZTFhMTI0NmZjMGNkNjllMDQ4MjE2ZTNlYWMifQ"
+        }
+      ]
+    },
+    {
+      "id": "claim_785e7b1e3d8c4d7ebe9252097c4215de",
+      "statement": "The autoreply applies to a chat when the global autoreply is on or the chat's per-chat flag is on.",
+      "evidence": [
+        {
+          "resource": "repo://src/app.rs#L366-L372",
+          "version": "repo-lines-v1:sha256:9190cc2d950eb45e132eae01c1ae01b5fb4af82470bb42fae8b4e0227a315710:eyJzZWxlY3RlZExpbmVDb3VudCI6NywiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiYmMzZGZhMjZjNzdiNTNkZDcyZWZmZGI1MmEyNWFlNGY5YzMwNTdiZTk2NWI0MzRlMTMyOWIwMTM2MGZmNGViNyIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDg5M2Y0MGUzNzgxZWI4OTZjODJkMDQ2YTA4MDY3YTExZTM1Y2RmMmYxZGZhZjI2ZmVhOTBiOGRiNjY3NjQxMyIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiNjM0ODk4ZWNhYTE1OWQ3NDg1NDNjOTU0Y2NkNTAxZGNiMmRjZGFlMzE4YTk2Y2JhNDE3NWNmNTM0YTkzYjU5MSIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiMjIwZTI2OTMzMWFlNTY1MmJhMzE1ZWJhN2ZjNGU1YzE1YWE0OWEzN2IyMTY0Zjc2NGViY2JhMDU4MTlkYzI0MCJ9"
+        }
+      ]
+    },
+    {
+      "id": "claim_3aacdc2cf4554221967cf93bba7a8d09",
+      "statement": "Typing in the list focus edits a case-insensitive name filter that restricts the visible chats and contacts; selection, opening and deletion operate on the filtered list.",
+      "evidence": [
+        {
+          "resource": "repo://src/app.rs#L610-L638",
+          "version": "repo-lines-v1:sha256:3dc060e5caa83528a61f1e9536a3e160fd7a50ddcdcb132badc610d2590e03da:eyJzZWxlY3RlZExpbmVDb3VudCI6MjksImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjExNTZkYzNmM2MyODVjOTI5NmU1NWNlY2FiOGQzMzczNzg1YTgyOTVkYTZkZjU4Nzk1ZDUyNTZlNDc5MjRiMzMiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjM5YTRmMjg3YzU3OGE2OWZkYmU3YjQ4ZGVmM2UzM2Q2ZTFkMzM3YjE4MjMxNDQ2YzE0NGExODA0MmQ5MDk5N2IiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjdkZWE3NDZmZTI4YTEwOGI2ZDUyNmVkOWJlNzE1NjU3MmI2YjIwYTdmODgxYTI0NGU0OTViN2YzMzg4MWFiMDUifQ"
+        },
+        {
+          "resource": "repo://src/app.rs#L706-L720",
+          "version": "repo-lines-v1:sha256:a74b903231dad2e8c6a1b043953ed2693e6235ed8d22e62536eb65c3acb7c714:eyJzZWxlY3RlZExpbmVDb3VudCI6MTUsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjBjYTdlYjAxNjVkZmQ4YjM0Zjk0YjRlZjFjMWFlYmUxM2EwNjlhMDE2NjcyNTIxMTQ3ZTAwNTk3MTliYmY1YjAiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImEzMTk2M2YzNjVhMWM2NGM3NDk2YzRiZjEwOTZkMzUwMjZiZWJhNTI1OGEwODc0ZWFkNWNiMzZkM2Y3MmViMjgiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImQzMzk2MDA5NzYxYzQ4YWY3MDRkZDRmZDQzNWM1NTNhOTg2ZGQ5ZWIzMmZiNTYyZmY0MGFkODI0NGIwMWU4ZDQifQ"
+        },
+        {
+          "resource": "repo://src/app.rs#L935-L937",
+          "version": "repo-lines-v1:sha256:fa265165a7dbaf620c22011a0c3c22d3351ef4c563f2f54f8fcb163c57bd1734:eyJzZWxlY3RlZExpbmVDb3VudCI6MywiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiOTBhZGVkYjQzODU5N2ZkNWM0ZTVlZTAxMmE5Y2RiYzY1Yjk4Y2VkYTcxMDc2YWU5YjE4MmFjMDNmMmQ5YWRlNyIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDEyY2EzNDVjY2Y3NWJmOWMwODA2YmNlNjk1YmU4ZGU4MDhiNzk5ODQyNTFhN2E1NGQyMDJjZjYxMDFkZDQ1MSIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiOWI5ODAxZjkzZGZiYTU3OWNmZTAzOWRiMGFlYTMwNzc1YWRhYTU2ZDM2NGUxZjE3NWJmNjYxYzhhZjg4MDRkYSIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiMmFiZDA1MjI3NzQyOTJhOTE3MDdjZDg0ZjRmMWExYTExNmQ3MWQzNDA3Yjg1Y2NkNDY4OWU1ZDM1MDBiYjAyNiJ9"
+        }
+      ]
+    },
+    {
+      "id": "claim_3b803c70a66943d38cb5b1741e5a1e08",
+      "statement": "Deleting a chat requires two Delete presses on the same chat in chat mode; confirmation removes the chat from the local history only, drops its deferred reply and closes it if open, and any other key cancels the pending deletion.",
+      "evidence": [
+        {
+          "resource": "repo://src/app.rs#L640-L693",
+          "version": "repo-lines-v1:sha256:4e43012c3fa4132665986e82f140c4b11bd24adebb940cb03888ac8f096a5660:eyJzZWxlY3RlZExpbmVDb3VudCI6NTQsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6Ijc2NjA4ZjMwMzQ4ZWYyZThkYTg5OWZkNTJlY2RhOWI4OTQ0ZWI1MTUyODUyZTM0MzI2OGZkNDY3NGIyMDVhZmEiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjkzNzNhMTgxNzI2YzQ3ZjdjNzlmOTI3ZDI2NzhhYzQ3NjYzY2VhMzI1OGYxOGEwNTM0ZTg2NDVlYmFlOTlkMGUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjgyNTEwNTY0NjA2ZWRmNDUyODU1YTA4OTFlYmRlN2ZhOGZjYjIyMTBlODQyZjU4NDNjNzlhMmRmZDcyZjU5OTAifQ"
+        }
+      ]
+    },
+    {
+      "id": "claim_9de77d642e45495ea644f73ab585004a",
+      "statement": "toggle_global_autoreply refuses to enable without an AI client, persists the global flag in the history and updates the status; disabling is always allowed.",
+      "evidence": [
+        {
+          "resource": "repo://src/app.rs#L839-L856",
+          "version": "repo-lines-v1:sha256:05cde78eab7927a8b5c33ce971f85c0e210ce60936eada88f8c9481585c1519e:eyJzZWxlY3RlZExpbmVDb3VudCI6MTgsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjlhMjY2MjVlMDNiMmE0MDg1NDg0YjQ2NjUzOTllOWRmOWFhZDQzYmVkNGQxNWFkODEwODMzMGIyMDhjNmM5ZTYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6Ijk5OWJkZWZlZWFjYTYzNDBlMDI5MGRjODZlODg5Yzg5ZDVlMTg5YWE1YmE0NGI1ODViM2I3MDczNGY0YTYyZDgiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImU2ZWQ1MmZlYjhjZTliMjNiYmNiYTkwMGFkYjk0MzE3MjA4ODNhZTk0ODY4NmQyNjhlNmRkMjFlNjljYTU3YjkifQ"
+        }
+      ]
+    },
+    {
+      "id": "claim_5447e09be26b40afa6cf039ca7dee031",
+      "statement": "handle_web_request applies State, ToggleGlobal, ToggleChat or Quit and always replies with a WebState snapshot; a ToggleChat for a JID not in the panel's chat list only sets the status 'Chat non trovata'.",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L338-L387",
-          "version": "repo-lines-v1:sha256:ba349479d65e8ca378f4a5e50a3d605adfb0c2b7af38e5afbaf4b5c1d6ebfc55:eyJzZWxlY3RlZExpbmVDb3VudCI6NTAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjJiYWY4NjRiNzJmMjM1YjJhNzAxNzllOWRiZTlkYTg1ZmY0NWM1OTg4YjY1MTUzNmQyYjNmMDM0MzRkMTA3YjgiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjJkOWY0YzAxNDlmZTYwZWNjMDY0NDU2Y2VhNTgwNWIyMmI4YjgzZTljYzY0YzVmZTc3YzhjZDNjMzQ5ODg1OWMiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6Ijc4OTU5NGE5NzIzN2Y2NjVkMmU0Y2RhMTIyZmY1NTIyNzJjMjVmZTFhMTI0NmZjMGNkNjllMDQ4MjE2ZTNlYWMifQ"
+          "resource": "repo://src/app.rs#L796-L837",
+          "version": "repo-lines-v1:sha256:7e50e3ca57a62654a61947e33ba08f34d79beff1c2e3df006a2210c439244f6a:eyJzZWxlY3RlZExpbmVDb3VudCI6NDIsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjA3YTEzOTMxODdkZWNmZmM0NzMzYzYwY2FiYjA2MTkwYjdkYjAxMjJjYWEyYWM3YjRjNGUwNzBiNzIxNTk1NWQiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjM2MGUyOWExOGZkNDE5NDVjMjM3NTgxMzM4ZGI3NWFjOTIzYzYxYjVmYmFiOTNlOTM3ODdlNWJmMGMyMjk4OTciLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImJlODM0YjUzNzA2ODgzYmQzMmMxMmViNmQ0OGEyMmFjZjlkNTIyYzIzOWJlNTE5NDZmNzA5YjIxZDNkNjIwMzIifQ"
+        }
+      ]
+    },
+    {
+      "id": "claim_42f6281320234303b6f272644df4e74f",
+      "statement": "web_chats exposes the TUI chat list in the same order, restricted to chats that support autoreply.",
+      "evidence": [
+        {
+          "resource": "repo://src/app.rs#L1142-L1156",
+          "version": "repo-lines-v1:sha256:fef040ce667500067e70dc91e838a73c35752821bb041d4463b4361b4445b2c0:eyJzZWxlY3RlZExpbmVDb3VudCI6MTUsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImYxMTFhMDk4Y2E0M2I2Zjg1OTYwYTUxMjFiOGVmY2Y5MDVlZDZmNjdmZjE5MjY0NjcxZDhkYTFiYzhjZTEwNWQiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjM1ZjZmNDY2ZTU5Y2ZjMzA3MDNjNDVmNzJhZjY1ZGU1NGFjMzYzZDM1NGNjMWEzMzMwMzUwYWNkNDUyNDM0YzQiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6Ijg5NGUwY2FkODFjNTBiOWY4MWE5ODI2ZjA1YzUyZjY0MGE0ZGM3NzdiYWQyNGM2MGEwMjQ5NzhlZWJmMWRjMGMifQ"
+        },
+        {
+          "resource": "repo://src/app.rs#L1201-L1215",
+          "version": "repo-lines-v1:sha256:8a598e5c5940c056f43610aa06cf7645c95a4e8f8fafac263afe969929ec6e1e:eyJzZWxlY3RlZExpbmVDb3VudCI6MTUsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjkyYjVlZGY1OTQ5Y2ZlYTY3ODIyY2Y2OTExMzIzNjNkNjhkNmYxZTE2OWY1ZTBkNGY3YzE0ZTVmYTFlNmQ0ZTUiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImYyZDgyOGEwMzdhYzFjODE1MzViMWQ1NTAwODMyMzIzYmZiYjg2NmEzYTNmODdlN2E2OGMxM2EyYmFlNTZmZmYiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImI2YjBlZjExZjE0Nzg5Y2Q3ZGY3MTBjYjExOWRhNTZmZjAyYjA3MThiNWQwODQzMTJjOTUwOTFkNzQzZTFhNTUifQ"
         }
       ]
     }
   ],
   "verification": {
     "by": "openwiki/0.7.2",
-    "at": "2026-10-19T19:53:56.480Z"
+    "at": "2026-10-20T20:27:06.515Z"
   }
 }

+ 22 - 12
openwiki/.claims/architecture/runtime-and-event-loop.json

@@ -1,14 +1,14 @@
 {
   "schemaVersion": 1,
-  "pageVersion": "sha256:302d26e89266adfff5cc6a109b6aff5744e957b32642d4337dc08f42da914d70",
+  "pageVersion": "sha256:7cbc08bb6eae59bb32a760bcc976c115e6702ecca67c1710b68b7a232fb16f49",
   "claims": [
     {
       "id": "claim_a384a56f0ebf4add8c0ab7722b040888",
-      "statement": "main loads the config, ensures the data dir, opens the history DB, creates AppEvent and WaEvent unbounded channels, starts WhatsApp, spawns the forwarder, builds App, spawns the input thread and runs the TUI.",
+      "statement": "main loads the config, ensures the data dir, opens the history DB, creates AppEvent and WaEvent unbounded channels, starts WhatsApp, spawns the forwarder, starts the optional web panel, builds App, spawns the input thread and runs the TUI.",
       "evidence": [
         {
-          "resource": "repo://src/main.rs#L28-L51",
-          "version": "repo-lines-v1:sha256:a1f78dd345a599ce923351dc782f73a2a62e23022449fe5ca2ec531b7f066305:eyJzZWxlY3RlZExpbmVDb3VudCI6MjQsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImExZDA2ZTA0OGNkMjcyNzM4ZDgzN2IwYzBlYzZhYWIwYjAzMTQ1N2U0NzVmZDQ5YWJiY2JhZmM1ZjczYzAyMGQiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjczZTczMWU3YjM0NmQ1YzQyMzJhYzA0ODFlYmY5ZWZhNTgxNTljMTY4ZmFhNTQ4YzYwNDZiODI1Yjg2Y2ZmMDQiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjcyOGNjMGI0ZGIzNjFhYzMxMDc4YmMxOTZiZDY1MjkxMzVhNmFmYzQ1ZWNhZTkyMWY2MTcxMjI1ZTJmNGNkOTgifQ"
+          "resource": "repo://src/main.rs#L30-L54",
+          "version": "repo-lines-v1:sha256:78c36720946a6da37fed1551aa53c38d6a246801eca122beca85edd71edb50f1:eyJzZWxlY3RlZExpbmVDb3VudCI6MjUsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImExZDA2ZTA0OGNkMjcyNzM4ZDgzN2IwYzBlYzZhYWIwYjAzMTQ1N2U0NzVmZDQ5YWJiY2JhZmM1ZjczYzAyMGQiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImZhNDUwZGE5MjlmMjA2MmRjNmMxMzBhZTc4MDU2NWE2MGUyYTkyZmI4YjYzNTA5MTZkNTdiM2EwYzA1MDM4MTkiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjcyOGNjMGI0ZGIzNjFhYzMxMDc4YmMxOTZiZDY1MjkxMzVhNmFmYzQ1ZWNhZTkyMWY2MTcxMjI1ZTJmNGNkOTgifQ"
         }
       ]
     },
@@ -17,7 +17,7 @@
       "statement": "The WhatsApp forwarder wraps each WaEvent in AppEvent::Wa and stops when a channel side closes.",
       "evidence": [
         {
-          "resource": "repo://src/main.rs#L57-L65",
+          "resource": "repo://src/main.rs#L60-L68",
           "version": "repo-lines-v1:sha256:6be0244c848e1db9107831f7228801caee72758797c9fa238420d9b550b1aa04:eyJzZWxlY3RlZExpbmVDb3VudCI6OSwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiMDFmYWUxNzcwNWQ2OTQ2NTViZGFlMjdlZTE4ZDA4MDg1ZjdjMzlmZjgyYWEzYjdiZGI0Yjc3ZGUxOTY3MjJjNiIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDEyY2EzNDVjY2Y3NWJmOWMwODA2YmNlNjk1YmU4ZGU4MDhiNzk5ODQyNTFhN2E1NGQyMDJjZjYxMDFkZDQ1MSIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiYzRiMzgyZTY4YzBlZTk1MGMwODdiOTA2YmZmMGJiMTQzNmI1MTFmMGFhZmVkNmFmNmE1YmM0ZTdhMTc5YmJlMCIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiYjU5MDFlMDA0ZGRhODVmZTA2ODQ4ZWRiOGQxYjMzZDU1M2ZhYzhmOTIzNmVlMWUyOWRlM2Y0ZTMxZGY1Mzg1OSJ9"
         }
       ]
@@ -27,11 +27,11 @@
       "statement": "Terminal input is read on a dedicated OS thread named wh-input; a read error is reported as WaEvent::Error and stops the thread.",
       "evidence": [
         {
-          "resource": "repo://src/main.rs#L22-L22",
+          "resource": "repo://src/main.rs#L23-L23",
           "version": "repo-lines-v1:sha256:1d14980ebaa2f31e4144fe6b04613dfc3285892428a59dc526be0808a55c1eff:eyJzZWxlY3RlZExpbmVDb3VudCI6MSwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiMWQxNDk4MGViYWEyZjMxZTQxNDRmZTZiMDQ2MTNkZmMzMjg1ODkyNDI4YTU5ZGM1MjZiZTA4MDhhNTVjMWVmZiIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiMWQxNDk4MGViYWEyZjMxZTQxNDRmZTZiMDQ2MTNkZmMzMjg1ODkyNDI4YTU5ZGM1MjZiZTA4MDhhNTVjMWVmZiIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiNDZmZmE0OWU5ZTQyNmZkYjkwM2I2ZmIyNjYxZDdjZDEwZmM1OTljMjFjZjg2ZDMzMWFlNjY4NDBhMzUwZGNkMyIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiMzRkZmU0NTZlN2M0YTU3MTBiMzQzZDg2MWFjYTE5YWRmM2I1MDA4MzQ3OGYxMjdiM2QxYTZlNWM0MjNkMDMxNyJ9"
         },
         {
-          "resource": "repo://src/main.rs#L71-L102",
+          "resource": "repo://src/main.rs#L85-L116",
           "version": "repo-lines-v1:sha256:cf6579340493800af2e3db3ff443a6e706051052b19e599dfa304084d854b29f:eyJzZWxlY3RlZExpbmVDb3VudCI6MzIsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImRhNzdjMDlkNzU5NTU0NWM4Y2M5NGU5YjhlZDg4YzQzYWQ4ZDAyNDAwM2FmYjE5NmFjOWNlOGZlMTMzMjkzNjYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjhmNWY0NWU2MTAxMTkyODQ5ZGNjNzA5NjBlMDA0M2Q2ZmM2ZTg5ZWIwNDkzYTc0MTMzYjAxMTQ2OWFmYzg0NWIiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImViNTc2ZTc2ZWM5NDRlZjNmY2IzMGZmZDlkYTJiOTU5NzNiNDUyYTdkYTQ4NjM5ODczNjM1MDExOGQ2MDU3ZDAifQ"
         }
       ]
@@ -41,7 +41,7 @@
       "statement": "Only key press events become AppEvent::Key and resize events become AppEvent::Redraw; other terminal events are dropped.",
       "evidence": [
         {
-          "resource": "repo://src/main.rs#L109-L115",
+          "resource": "repo://src/main.rs#L123-L129",
           "version": "repo-lines-v1:sha256:85bac23651d9b95ec5f5593453b077b12706d21eed247ce921d00504110bfaad:eyJzZWxlY3RlZExpbmVDb3VudCI6NywiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiYzcxMDI5NWZiM2ZiYzY4ODZmZmNkZmJiMzEzMGYwMjgzYWM2MDY0MmQ5MGQ2OTllY2QwM2U1ZTI2MmM1NjVjMiIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDEyY2EzNDVjY2Y3NWJmOWMwODA2YmNlNjk1YmU4ZGU4MDhiNzk5ODQyNTFhN2E1NGQyMDJjZjYxMDFkZDQ1MSIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiZTkzZDY1YjVkODczOTc3NmUxMzMzZmZmZGQ0MzlhOGQ3YzQwYTA0MTAyNzUzMjY3NTQ4MDAwNmQyMGVmMGE2NCIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiNjQxY2IxZDYxYmJhN2RkZDlmYmI4YTI5MWY4MDdmMDVjNjY3NjkyMzY2M2RjM2IzYTcyOWQ3MzI4NDVkNDhjZCJ9"
         }
       ]
@@ -51,7 +51,7 @@
       "statement": "run_tui always restores the terminal after the event loop returns.",
       "evidence": [
         {
-          "resource": "repo://src/main.rs#L121-L126",
+          "resource": "repo://src/main.rs#L135-L140",
           "version": "repo-lines-v1:sha256:0f8fe295d420386182d43d18d5735ed806080dd2bfac8adb559c5685bef212f9:eyJzZWxlY3RlZExpbmVDb3VudCI6NiwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiYzI5YzU4MTYzNmNkOTM0NmZjNDQyMjUwOTZkZTU0NDQ3MDAwNGU0MmM1Yzg3Nzg1Y2Q0Yzc0Zjk1Y2I1NzJmNSIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDEyY2EzNDVjY2Y3NWJmOWMwODA2YmNlNjk1YmU4ZGU4MDhiNzk5ODQyNTFhN2E1NGQyMDJjZjYxMDFkZDQ1MSIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiOWQ4OGMwZjlkMTllYjQ0M2JlOGRhN2ZmNzc4MWM5MGEwNTBhZWVlMDJmOWI4ZDFhOTRmYTY3ZmZjMDc0YzY5YSIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiZWQyNGY5Y2EzODEzYjAyZDVkY2E4MmY0ZTc3NTM0ODc5ZGFhM2E0MjhmMmMzMzliMjU4NTgyNTk0MTg1NjI4ZiJ9"
         }
       ]
@@ -61,7 +61,7 @@
       "statement": "The event loop draws a frame, awaits the next event and dispatches it to App until should_quit is set or the channel closes.",
       "evidence": [
         {
-          "resource": "repo://src/main.rs#L133-L152",
+          "resource": "repo://src/main.rs#L147-L166",
           "version": "repo-lines-v1:sha256:644802a473993f7032f4dbd83c1464e2a0c1554ff7d51b3cc5503bc08fcf5548:eyJzZWxlY3RlZExpbmVDb3VudCI6MjAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjkwNTMwZjA2M2RhYzc0YzIwZGZmMGE4YmI5MjA5MTFmYThjYTA2YWRhMTMyMDU4ZGI2NTRiYjBjNDg5MTE0YjAiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjFmYzBjMDAyZTcyMTE3NTg2ODEyN2Y4ZmZiYzNmYTVlMWNhNDBjNmYxYjNlM2JiNTUwMDU3YmU0NDkyYWI2MTIiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjowLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImUzYjBjNDQyOThmYzFjMTQ5YWZiZjRjODk5NmZiOTI0MjdhZTQxZTQ2NDliOTM0Y2E0OTU5OTFiNzg1MmI4NTUifQ"
         }
       ]
@@ -71,14 +71,24 @@
       "statement": "event_loop records list_mode_before = app.list_mode before handle_event and calls terminal.clear() when the list mode changed, forcing a full repaint so wide glyphs from the previous list leave no stale cells.",
       "evidence": [
         {
-          "resource": "repo://src/main.rs#L128-L152",
+          "resource": "repo://src/main.rs#L142-L166",
           "version": "repo-lines-v1:sha256:c813b17a84a3084eefaa34b8e9284507be8cfac301dd0b8abab485d88d66204a:eyJzZWxlY3RlZExpbmVDb3VudCI6MjUsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjNlMTkzYjAzYzI2ZWIzM2EwMjk4MWViY2MyNmM2YzIxNmRlZGQzYTkxMDQ5OTA0ODI5ZjJmNDQ0ZTVmYWYyODUiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjAzYTEwMzdmMDcxOTYxZmIzYjEzYTdkYWZmNjlhY2MyYmFiODEwZmU4YmRhMTZhMmIyMjcxNTU0MzcxNGNjNzUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjowLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImUzYjBjNDQyOThmYzFjMTQ5YWZiZjRjODk5NmZiOTI0MjdhZTQxZTQ2NDliOTM0Y2E0OTU5OTFiNzg1MmI4NTUifQ"
         }
       ]
+    },
+    {
+      "id": "claim_9a63978b2a604355ba70b04ed25dd94f",
+      "statement": "start_web_panel does nothing when web_port is unset; otherwise it starts the web server on that port and a bind or spawn failure aborts startup with 'cannot start web server on port <port>'.",
+      "evidence": [
+        {
+          "resource": "repo://src/main.rs#L70-L79",
+          "version": "repo-lines-v1:sha256:54c6448bd45241fd4b90479b1a91e2fd528e7a38f4ac5d543eda728fbf3756d5:eyJzZWxlY3RlZExpbmVDb3VudCI6MTAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImYwZjA3MmY1NzA4NTRiMGM0ODg3YmRlZDU4YzU2NWRiZGExOTNlYmY3NGIwODZmMjYyNTFmZjZhOTZiMmJiYWMiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImRmYWI3MTBkZGY3ODU5ZTU3NTc2NGE5MTNhMDAyYzc0NTg1ZmZhNzAwMTUxMGNiODgxMWRhODNkMmNmNzRiMzQiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImI1OTAxZTAwNGRkYTg1ZmUwNjg0OGVkYjhkMWIzM2Q1NTNmYWM4ZjkyMzZlZTFlMjlkZTNmNGUzMWRmNTM4NTkifQ"
+        }
+      ]
     }
   ],
   "verification": {
     "by": "openwiki/0.7.2",
-    "at": "2026-10-19T19:53:56.480Z"
+    "at": "2026-10-20T20:27:06.515Z"
   }
 }

+ 36 - 22
openwiki/.claims/architecture/terminal-ui.json

@@ -1,13 +1,13 @@
 {
   "schemaVersion": 1,
-  "pageVersion": "sha256:c631191230dc5883f73a1cabd5eff75e1e0e569f34653b9bf8a81d41e68016d9",
+  "pageVersion": "sha256:52543285487729f07ef002bcc4cb827b749e6ec744b94cd40269ba91da70160f",
   "claims": [
     {
       "id": "claim_7b6015e24275488a85ba88dd065f2ff8",
       "statement": "render dispatches on app.screen to the connecting, pairing or main screen renderer.",
       "evidence": [
         {
-          "resource": "repo://src/ui.rs#L97-L104",
+          "resource": "repo://src/ui.rs#L105-L112",
           "version": "repo-lines-v1:sha256:44f3697b4f82d6e56108d5b16d3fe4449dc6bc389fe30877ba38781793fb8507:eyJzZWxlY3RlZExpbmVDb3VudCI6OCwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiMzg4NTBjODgzNDEyNTY0NDAxZDdlYmI3MWFkMDQ5NzVlMzc2NTA1NjZlNmZlMTU5ODNiZWYzOGVhNGFhMjdhMiIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDEyY2EzNDVjY2Y3NWJmOWMwODA2YmNlNjk1YmU4ZGU4MDhiNzk5ODQyNTFhN2E1NGQyMDJjZjYxMDFkZDQ1MSIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiNjE5YzdkZDRkNTVhMjc2YTc0ZTU0NzA4ODQ0MzQ0MjNlY2JmOGU2OTA5MWNkMjM2ODQ3YTQ5ZTZkNDlmYzAwNyIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiZmI5MWJlYzI2OWYxNGRiOGY2OGJmZjhjMTY2ODA4ODI2MDc0YjdmOGFjYmI0NWMzZDA0NTg4YTYzZTFlY2FhNyJ9"
         }
       ]
@@ -17,7 +17,7 @@
       "statement": "The QR code is rendered with Dense1x2 half blocks, inverted colors and a quiet zone.",
       "evidence": [
         {
-          "resource": "repo://src/ui.rs#L182-L191",
+          "resource": "repo://src/ui.rs#L190-L199",
           "version": "repo-lines-v1:sha256:e126cd7576186dd9f80551b4f1e1c261f754331d72094cfe860fd19a77e2dfc8:eyJzZWxlY3RlZExpbmVDb3VudCI6MTAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImRiM2MzZDFlMjNmYmFlNGQ1ZTA1NzMzMjEyNGEyZTk2MjM1NjNhMzBkMzZhYjA0YjljNzFlY2M0YmY2Mjg1OWYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImMyOWIxMTIwYWEyMmQwM2YwMzBmMjY0NGM3MzZjYWE0ZTZmMzQ2YThiOTBmMDNjNjFhNmE0YzU3YzE5NGMyNDQiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImEyYjI2MDBiMDc3ZWY2NGJmYTgyMGQ3OTc2NTAzMzFlNzI5YWRiMDdhMjhhYTY3OGE3ODQ1YmVlYjExMTNhMmYifQ"
         }
       ]
@@ -27,11 +27,11 @@
       "statement": "qr_block_lines shows a waiting, encoding-error or terminal-too-small message instead of the QR when needed.",
       "evidence": [
         {
-          "resource": "repo://src/ui.rs#L161-L175",
+          "resource": "repo://src/ui.rs#L169-L183",
           "version": "repo-lines-v1:sha256:f70009d551400b4459543b770a01bcb4ca2cfec3c41a6bd11efe47faa57a3e59:eyJzZWxlY3RlZExpbmVDb3VudCI6MTUsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjlkZDQyYTM5MDMyYmQ1ODhiZmNiMTM5NjgyMGY3NmI0ZmIyMmNhMDY3YWU2OGNkZDdjYzhmNjZiZjgzMjMzNGQiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjkzMzJjMDRlNjVmOTMyMGRkNzNjY2VlM2IxOTBiZDZkNThmYmE3ZjhmZTEyNzg4MDA3MmRiNmM5YTMwZjg0ZjMiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImNlYmI2MGRjYjRjZTI4NzBjZWM3ZGE2YTk5YmI4MDk5NGIyYWIzYWQzZWI5NTc3ZTE2MDFmYjU0NzI3NjZlOWIifQ"
         },
         {
-          "resource": "repo://src/ui.rs#L65-L71",
+          "resource": "repo://src/ui.rs#L73-L79",
           "version": "repo-lines-v1:sha256:58a07a9e2caaa4d0fabbb6bb1f5c5dd0d18f3dc926d96b302fa8bdfc5e33644c:eyJzZWxlY3RlZExpbmVDb3VudCI6NywiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiODY5ZTQzNDAyYjczM2JjYTlhOTNhY2EwNjg5ZGQ2ZjkxZjg1M2QwM2E4MDFlMDdlZDMwZGZhYmRkNGM5MDZjYiIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNGE0ZjM4NDMzZDI5ZmNhNjFmNjJiZTc3NmU2ZGEwNDVhMDZlYjQ0MGI5MzQ5ZmQxYTQ5MjY0ZTgxMTY5ZGMwOCIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiM2IzMWVkYmM1ZjI0MWZkZDJjMzA5MThkYmIwODE4YWFjYjRkM2FjZmI2NzgyMjRkYmQxYzBiOGI3ZTU0ZDcxMyIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiODAyNDcyZjBlOGYyYTA1MjI0NGMzYTAzNGU2MGYwOGZkZGNkMjVmYmRmYTg1NmI1NDgxMjQxYTAwYWE2NzU0ZCJ9"
         }
       ]
@@ -41,7 +41,7 @@
       "statement": "The main screen splits into a 30% list pane, a messages pane, a 3-row input pane and a 1-row footer.",
       "evidence": [
         {
-          "resource": "repo://src/ui.rs#L197-L212",
+          "resource": "repo://src/ui.rs#L205-L220",
           "version": "repo-lines-v1:sha256:c7b573159910f69890eba199ba66af477c525be3a8732bcbcd2f9ba7be836466:eyJzZWxlY3RlZExpbmVDb3VudCI6MTYsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImM1NWUwYzhkYjY0N2Q0ZDUwZmM0MWY5Mzk1NjgxMmM5OTk4ODQ3NzZmYzVmMjZiOTM1Zjc1MDA1YjQ3M2U1YmUiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjUxODU3ZjE2ZjQ5MDJjOWNhZWM0MDU1NTgxYWI4ZWJiZjFhYzU0NGU3OTMxMjM2ZWY1NDAxOWVkYTkyMDlhNjMiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6Ijk2YmI0MWI1YThhOTczMmVhYjg5MTA4MWE4MDRmYjU2ZjEwNGRiYzAzNDVhYzAwOWQ4MzdhYzhiZWVhN2FjMzgifQ"
         },
         {
@@ -52,21 +52,25 @@
     },
     {
       "id": "claim_a8294702ebdc481d89ab87061f3c5115",
-      "statement": "The list pane shows chats or contacts, highlights the clamped selection and marks autoreply chats with a green AI marker.",
+      "statement": "The list pane shows the filtered chats or contacts, highlights the clamped selection and marks chats with per-chat autoreply with a green AI marker.",
       "evidence": [
         {
-          "resource": "repo://src/ui.rs#L232-L274",
-          "version": "repo-lines-v1:sha256:37b6420dcaafb9df8e0e6925ed6844ca95b1a2377be0a06f98633dc644706e69:eyJzZWxlY3RlZExpbmVDb3VudCI6NDMsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjVhYWI1NDY2NTFmMTk2NDNiOTg2MGQzNjBkMmYyZGE2YmIxNTMyYzAzZGRkMzIxNGI3YTg5ZWFiOTFmNjgyZDQiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjliZDEwMzhkZWZmODFiYWFlZjU0N2JjMGI2NTk5ODc3YTc5NjhhOWRiZDBkMWM5YzZkODFjOTBhMTJkM2UzM2YiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjU2MzU5NWUzODliYzNjMjYwYThkODRlZmVhNmYyZmM5YWNhMDRhODJkODQ5ZDI1MjU4ODBiY2QwMDliODRiMjYifQ"
+          "resource": "repo://src/ui.rs#L240-L259",
+          "version": "repo-lines-v1:sha256:732f44ba82916dfa1b3ddde98a73ecbac245a78006147401460b3c1aa17b9b15:eyJzZWxlY3RlZExpbmVDb3VudCI6MjAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjVhYWI1NDY2NTFmMTk2NDNiOTg2MGQzNjBkMmYyZGE2YmIxNTMyYzAzZGRkMzIxNGI3YTg5ZWFiOTFmNjgyZDQiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjliZDEwMzhkZWZmODFiYWFlZjU0N2JjMGI2NTk5ODc3YTc5NjhhOWRiZDBkMWM5YzZkODFjOTBhMTJkM2UzM2YiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImJkYjk3MWM3OTZlNTNmZTE5ZDE3OTc4OWIwMDRlZDEzZTdhOWEzN2VhOGNkNGYwNmQ3NzgxNzQ0NmUxNDA5NzYifQ"
+        },
+        {
+          "resource": "repo://src/ui.rs#L281-L296",
+          "version": "repo-lines-v1:sha256:81e36f0dbffed3b264bf79086b294804a0e2423387d63ef243ec387746d2b84c:eyJzZWxlY3RlZExpbmVDb3VudCI6MTYsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjBlN2Q5YjRmZmQ1M2VhYjVmMjdhMmFiNGJlZGI5NWNlYzEyZTRlMTRlNTBjMWRhNTdhZWNlZjdlNTBlZDM1NTkiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjBjMmZiOGQ3YjJjMGMwNTI5MmVjNGM4ZWQ0OTYxMzk2ODgzZjYwZjdkYTQ3Yzk5NzdmZTVmZTU1NTMyMDM1ZDIiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImZkZTk5NjAxMDljZTU4NjlmNjRmNmFmNGMwNDYzMWU2MmU2ZDFlODU5MGYwMGNlMWNhZGYyY2Q0N2JkYmU5ZTEifQ"
         }
       ]
     },
     {
       "id": "claim_95ec27e07ddd44e9aedcef368b9d82ed",
-      "statement": "The messages pane title shows the chat name and AI ON/OFF, and only the most recent wrapped lines that fit are displayed.",
+      "statement": "The messages pane title shows the chat name and AI ON when the open chat's flag or the global autoreply is on (OFF otherwise), and only the most recent wrapped lines that fit are displayed.",
       "evidence": [
         {
-          "resource": "repo://src/ui.rs#L280-L315",
-          "version": "repo-lines-v1:sha256:7fa053aecf55de8e72db5da180af79d143438f299379731f6745ab5a60e7d414:eyJzZWxlY3RlZExpbmVDb3VudCI6MzYsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjMwM2M5MTRlZDU2ZmZiODJmYzlhZDJjZjQ0NDQxMzRmZDRhZTkyZTkzZmYxMTQ2NzZlOWI3NjI2NTUwZDVjMzciLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjBiOWJkNjEyZDA0MDhiOWE2NjFiNGQ4YzJkYThlNzk4OWRmYWIxNmVlOGEyNjBmN2NhMjI2NjBhODRjOWZiNDciLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImJlNWU3NzM0ZjZhNWM1NWJiOTExY2U5ZTNhYjgwYWExZmZmZjljN2FmODgzMTUwMzg1ZDg4ZTVmM2M5ZTJiOWYifQ"
+          "resource": "repo://src/ui.rs#L310-L346",
+          "version": "repo-lines-v1:sha256:c291029f490ad470710b3ac4482737631d1c35ee2eff8d99cde384408b75b573:eyJzZWxlY3RlZExpbmVDb3VudCI6MzcsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjMwM2M5MTRlZDU2ZmZiODJmYzlhZDJjZjQ0NDQxMzRmZDRhZTkyZTkzZmYxMTQ2NzZlOWI3NjI2NTUwZDVjMzciLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjBiOWJkNjEyZDA0MDhiOWE2NjFiNGQ4YzJkYThlNzk4OWRmYWIxNmVlOGEyNjBmN2NhMjI2NjBhODRjOWZiNDciLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImJlNWU3NzM0ZjZhNWM1NWJiOTExY2U5ZTNhYjgwYWExZmZmZjljN2FmODgzMTUwMzg1ZDg4ZTVmM2M5ZTJiOWYifQ"
         }
       ]
     },
@@ -75,22 +79,22 @@
       "statement": "The input pane shows the tail of the typed text that fits and places the cursor when focused.",
       "evidence": [
         {
-          "resource": "repo://src/ui.rs#L461-L491",
+          "resource": "repo://src/ui.rs#L492-L522",
           "version": "repo-lines-v1:sha256:4af9cab79ebcb71cd601b44fe7a4b8fcceb1fe16ecac50a20ec643453b72d21d:eyJzZWxlY3RlZExpbmVDb3VudCI6MzEsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImM5YzFjMzliNjRjZmUzMjlkNGFjZmM2NTNlYTY1M2RmOTBiMGI5MDQ0NDJlMWU4OTZhMjFhMGUxYmY3ZDRkNjAiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjBjOTcwNjYzMjgxOGU3MzliNDJhYWZlZTA2OThlZGQ1N2RkZjc5MmM1ODcxY2I3ZGNiZTliOTBhYWU2YjJhYzEiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImEzYWY3MDc2MmFmY2NkYTcwOTEyZWVkMDUwYzlmZTlmNzVjNzg5YjMwMjVjM2NkOWRhYmMwYTBiYjhkODI1MjQifQ"
         }
       ]
     },
     {
       "id": "claim_6e28bf5fc8004ac8af3412b4f4979d5c",
-      "statement": "The footer shows the bold status followed by dimmed key help.",
+      "statement": "The footer shows the bold status followed by dimmed key help in which the F4 global autoreply shortcut is highlighted green while the global autoreply is on.",
       "evidence": [
         {
-          "resource": "repo://src/ui.rs#L497-L506",
-          "version": "repo-lines-v1:sha256:0af077e2e8363e4f9ba2d8e685b126e0694d64701dbbb9e8cf26f09e3780571f:eyJzZWxlY3RlZExpbmVDb3VudCI6MTAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImQ5NTJkYTBmYWY4MjUyMDVkMmRlNWEyYzIzYjNmOWVkZjgwYmM2OTM1OTE5MzIxNjlkNDg3NTU0ZDYzOTNlNzgiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjdmZGM3N2EzYmE4YjFhZTVmZDIzYzQwOWM5ZDc5MThkNWYxMjBlZDFmNGMxNjY2YjhlYWNkODNjY2M3NTlmYzUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjdiYzk4ZDZiMGRjNDkyYzUwMTZkZjEwODA1YTNhNTRlYjljMTg0YTVhNjEzN2U3ZjRiZWNmYTBjMjIwNTUwZDAifQ"
+          "resource": "repo://src/ui.rs#L524-L556",
+          "version": "repo-lines-v1:sha256:d0e8646c9a73f738bb5de0be1ad81c7138c3994f200493bc090e5e1e3845fff0:eyJzZWxlY3RlZExpbmVDb3VudCI6MzMsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImJiNzk4OWQ5NWQzZGZlM2YyODliYzI4Njg5OWNjYzAxZWI2YzVkNGQyMzZkNDU3Mjg4YjJiYTA4YjZkMWRkNGIiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjNhMzZhNmJkYjRhZWM3MTA3YmVjYTFmM2I1OWZhMDBlOWVkYzdlYWZmNmI2YjUwMDlhMzQ4OGM2MjZhMDdlYTUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjdiYzk4ZDZiMGRjNDkyYzUwMTZkZjEwODA1YTNhNTRlYjljMTg0YTVhNjEzN2U3ZjRiZWNmYTBjMjIwNTUwZDAifQ"
         },
         {
-          "resource": "repo://src/ui.rs#L50-L51",
-          "version": "repo-lines-v1:sha256:c6c4233f9857bdb7940e4018c989a08b6e3be5402ed51d01cd896fb27a1d40c8:eyJzZWxlY3RlZExpbmVDb3VudCI6MiwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiZGY5ZjdiZGQ3NjdkYTg4OGI5OGRlMTRhNTgzNTA5ZDIyODQ1ZDQwYjQwM2UwY2NmODdmOTJkZTczNzY3OTgzNiIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNzUzZDE2Y2JmYzM1NTU2ZWZlMzAwYzIzNjAwZjkwZDUwNzBhZWFmMzA3OGFkOGNlOWU3ZWIyOWVjYTE4ZTlhOCIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiMzcxYzRmMmY2YTFlYzJiN2IzNWRjOWE2MzRlOTQ5NjEyOWNiYTFiYjQwYmM1NTcyZGI0OTIxZTE5MjZhN2ZjNyIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiNzEwYmQ3N2I3NWExZjgxMjI2MjkwNTY1NWU0MTk1YjA0ZjIzZjQ0OGRkNDVkZjE4ZmJiZGY3NGMyMzAxNmIxYSJ9"
+          "resource": "repo://src/ui.rs#L53-L59",
+          "version": "repo-lines-v1:sha256:6375d8760354f1a87914855b5565f443be78757b62647d467bf23d52b5b1b491:eyJzZWxlY3RlZExpbmVDb3VudCI6NywiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiNWJiMDYzYTdlZmM4YTczMDVhM2NhNjliYjFkYjEyYTliMjg3NTNhNWM5MzY0ZmE1NTUzYWYyOGI1NDY0MTI1MiIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiMzljMmIzMjhmMGU0MDc5ZDhiN2ZhZjQxOWUyM2Y3YjkzN2QxNGQ2YzZmZjU2NmUwYmRmMjcyZGNkOTVkZDM0YiIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiMjU3MWY4OTdkYmU1OGFhZDg5YTBkNzc1MWU0YTIyMTAwZGI5Y2UzYmI4MDllMTRmMjBlNWY4ZDJlYWQ3MGJhNiIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiNzEwYmQ3N2I3NWExZjgxMjI2MjkwNTY1NWU0MTk1YjA0ZjIzZjQ0OGRkNDVkZjE4ZmJiZGY3NGMyMzAxNmIxYSJ9"
         }
       ]
     },
@@ -99,7 +103,7 @@
       "statement": "Messages are formatted as HH:MM sender: text in local time, with sender Tu, [AI] Tu, the sender name or the JID user part.",
       "evidence": [
         {
-          "resource": "repo://src/ui.rs#L346-L404",
+          "resource": "repo://src/ui.rs#L377-L435",
           "version": "repo-lines-v1:sha256:c499132db2ac5eead3eb62f5ad7bab3b7f8d914d35adb23bfc5ca621870465f2:eyJzZWxlY3RlZExpbmVDb3VudCI6NTksImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImM3ZWNmMTZhOWUxM2QxZTA4ZmRhYmUzOGM5ZjU3MmMyZjRmNmUxYTkwNGNiODAxYWY3M2RlNzllOTczN2NhZjYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImMyZDgzZjgxYTNiOWQwZjYzMWZmYzlmMTg5ZGQyYTY4NWNhYzJmOGViODA3NTYwYWQxNzlkZTk2Y2NiNjgzMzQiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjQyOGI1ZWVjNDkzNDMxZWNmZGY2ZTIyZGVlMjc5OThhNDcxOTczN2Q1Mjg1NGI3MGEwZmM3MzUwZjViMjg4YTQifQ"
         }
       ]
@@ -109,7 +113,7 @@
       "statement": "AI messages are cyan, own messages green and incoming messages use the default style.",
       "evidence": [
         {
-          "resource": "repo://src/ui.rs#L334-L340",
+          "resource": "repo://src/ui.rs#L365-L371",
           "version": "repo-lines-v1:sha256:9a0c35114d98982d540867094c84291c906d6d832ab746b9c4f2dec88b905a7c:eyJzZWxlY3RlZExpbmVDb3VudCI6NywiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiMDg0NDE5ZmQ0YjVmM2E4YzE4MTM4M2Q4NmQ5ZDc0ZDhiMWUyOWVlZjk5ZGMzNTM3ZGNhNjYzYzI1YjM2Mjk4YSIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDEyY2EzNDVjY2Y3NWJmOWMwODA2YmNlNjk1YmU4ZGU4MDhiNzk5ODQyNTFhN2E1NGQyMDJjZjYxMDFkZDQ1MSIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiZmVmNTliMjU3ZDI4Yjk0NTA2YTg2YTU0MmQyZThhOWNkODlkNDNjNmIwMmY1YTNhYmZjNWIzYmY2ZmJkZGE3YyIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiOWVlODhlMWI2M2U2ZWU3M2I1NDcyNTVjN2EyYzQzMzE1NjJkNWE2OGE1NDc3ZGRkMzY0OTZiMzc5ZmYxNWYxYiJ9"
         }
       ]
@@ -119,14 +123,24 @@
       "statement": "Text wrapping splits on newlines, wraps by display-width characters and drops control characters.",
       "evidence": [
         {
-          "resource": "repo://src/ui.rs#L410-L455",
+          "resource": "repo://src/ui.rs#L441-L486",
           "version": "repo-lines-v1:sha256:e966afd08433fba747794711a95f264804c09ac9f81a7c818a6f80be9a2893ad:eyJzZWxlY3RlZExpbmVDb3VudCI6NDYsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImYwYTJhNzczNmUxMWZkOGUyYjYyYmViMWM5NjNjZjRjNWRmYmNiNTIzNDZkNDdlOTgzNTgwZmE1ZmY1ZjY4OTQiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImNkNWU4MTdmODljYTM3MjE2NTg3NGM0OGRmNzBjMjRiZTc5YTkxZmQxODQyZWE3OTI0YTllZDQzMzg2ZDdlZDgiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImM1MjY1OTg3OTliMDNjYmQ4ODczY2RjNjRhOWJmOGE2NGQyOGMxNjhmNDkwYTMwM2ExZTkwY2U5ODkzMDc4ZmQifQ"
         }
       ]
+    },
+    {
+      "id": "claim_6b210c0792f54e739fbe3b192f51fbd3",
+      "statement": "In chat mode the list title appends a global autoreply marker when the global autoreply is on, and any list title appends the search label and filter text when a filter is typed.",
+      "evidence": [
+        {
+          "resource": "repo://src/ui.rs#L240-L275",
+          "version": "repo-lines-v1:sha256:fc4a9a5029ba905ce7fee869463b44f5f84ba4ff03c0566e28b38b850f9145bb:eyJzZWxlY3RlZExpbmVDb3VudCI6MzYsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjVhYWI1NDY2NTFmMTk2NDNiOTg2MGQzNjBkMmYyZGE2YmIxNTMyYzAzZGRkMzIxNGI3YTg5ZWFiOTFmNjgyZDQiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjliZDEwMzhkZWZmODFiYWFlZjU0N2JjMGI2NTk5ODc3YTc5NjhhOWRiZDBkMWM5YzZkODFjOTBhMTJkM2UzM2YiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjQyZjY4ZGVhNTk5MTczNjlkNmMwZGJhNTg5NTZhNDI3OWYzMWJhNDMwM2YxNjY0Y2QxY2M1ZGNhNzVmODIxNDQifQ"
+        }
+      ]
     }
   ],
   "verification": {
     "by": "openwiki/0.7.2",
-    "at": "2026-10-19T19:53:56.480Z"
+    "at": "2026-10-20T20:27:06.515Z"
   }
 }

+ 49 - 17
openwiki/.claims/concepts/history-store.json

@@ -1,24 +1,24 @@
 {
   "schemaVersion": 1,
-  "pageVersion": "sha256:85e34ab27e1a8c9fd95caffe685dc5671c2f174151fe7b0a2b4a400ba9c7d847",
+  "pageVersion": "sha256:af82818f0b14ff0161574d8625a43bed0807de3f4b8dc152a3283e1a40bccf24",
   "claims": [
     {
       "id": "claim_3afb8d7c0dbf446090433a98b74ad2e5",
       "statement": "History::open creates missing parent directories, opens the SQLite file and applies the schema.",
       "evidence": [
         {
-          "resource": "repo://src/history.rs#L60-L93",
+          "resource": "repo://src/history.rs#L65-L98",
           "version": "repo-lines-v1:sha256:43f65e608bb68aead3f44bea548e6e7b8169cd5274874c39a748cec236b2373e:eyJzZWxlY3RlZExpbmVDb3VudCI6MzQsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImVmODI1MzY0MWU3OWEzYzQyOTk5MDgzODFhZGQxMDhhYTE4OWJkMTI1YTVhYWE0MGMyMTk3NDgxZjNlOTRhNTciLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjBmNzY3M2M5ZTc2ODA2MmJjOTM3MmNlZGQwM2JiYjJiYmQ5ODcxMjEzZWVjNTc2MDA0MjcyM2U1M2E1NTMzZTkiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjlmZTMxZjY0OWZmMjg3ZDhhMzU3ODI3MzJmNDQ3MjgyOWVjZTdkZmUyZGIyMDc1MmY3M2M0Y2JkMDBjNjczZDIifQ"
         }
       ]
     },
     {
       "id": "claim_f1b5347b6c074646bf967856b976ca8c",
-      "statement": "The schema defines messages, chats and contacts tables plus an index on messages(chat_jid, timestamp) using CREATE IF NOT EXISTS.",
+      "statement": "The schema defines messages, chats, contacts and settings tables plus an index on messages(chat_jid, timestamp) using CREATE IF NOT EXISTS, with no migrations.",
       "evidence": [
         {
-          "resource": "repo://src/history.rs#L285-L311",
-          "version": "repo-lines-v1:sha256:a17d7cf43e16b15423ac11e5d04759073ac2b8670812b56a760124f7f47bbe36:eyJzZWxlY3RlZExpbmVDb3VudCI6MjcsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjVmNWI1MzQ2N2IzY2RmMTYxNjU0Y2U2YWEwNmNiYzkzNjlhOTNhM2ZhZDg2OTBkMWUxZWY3OWI5NWY0Mzk1NDgiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjdlMTEzNDZmMmE0ZDdkYmU3ZjE4ODhkZGJjMzViZjQyYWIxN2M0ZmFlZTkyOWUwZDY2Mjc0NWEyMWI4Mjc2NmUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImEzNWM0OWE4ZjYxOTQzMjJkOTYwYTAwZjQwNzA2ZDJkODAzMTE2NDRkNmZmZDhmZWJlNmZjZWIyN2EyZDhkZjYifQ"
+          "resource": "repo://src/history.rs#L340-L374",
+          "version": "repo-lines-v1:sha256:4cff43e18fae30b0a29423e81000ae1701552bd4bcf0333d49a79580fee2a62b:eyJzZWxlY3RlZExpbmVDb3VudCI6MzUsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6Ijg2MTQ5Yjg1MjZhMTc4ZjhmMzMzNTViZjU1MmMxNjJlYzdiZDRhNTQyNjk2MzhmNzlhOTkwODIzNzg1YjM5NWYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjE5ZTc2MzU1Y2YxMTk5NDgwYzNmOTk4MzZiMTQ4NmM4NDFkODkxZmFhZDkwNGM2MDhlMDkzMDIyZDM3NTdhZWUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImEzNWM0OWE4ZjYxOTQzMjJkOTYwYTAwZjQwNzA2ZDJkODAzMTE2NDRkNmZmZDhmZWJlNmZjZWIyN2EyZDhkZjYifQ"
         }
       ]
     },
@@ -27,11 +27,11 @@
       "statement": "save_message inserts with INSERT OR IGNORE keyed by message_id and upserts the chat only for new messages, in one transaction, returning whether the message was new.",
       "evidence": [
         {
-          "resource": "repo://src/history.rs#L100-L111",
+          "resource": "repo://src/history.rs#L105-L116",
           "version": "repo-lines-v1:sha256:598d40867f709e3d9ec09cccd1e752c4ba2ae078a93b814058400d12451c022f:eyJzZWxlY3RlZExpbmVDb3VudCI6MTIsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjJjY2YyOTI3M2ZhMDRlMTFmMTY4ODliZTBiYWY2ZDg1MjdjN2Y1ZGNmY2VjMGVhMmZmNmU3NmViNDA1MGVhZTkiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImYzMWE1ZDk2MDM0YzIyMWYyMWVkZTQyYTlkOWM3ODgwMjQxZTI2YzJjMzQxOWQ4Y2M4MmNiM2JiMjdjNWJhY2MiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImQxZmRlNjg0N2FkMzhlOWUyYjA1ZjdmYTRiNDUzOTY2YzE2NjRkNmJmYTExMDJmY2YyYzJiOWNjNDM5MGQ1OTUifQ"
         },
         {
-          "resource": "repo://src/history.rs#L317-L338",
+          "resource": "repo://src/history.rs#L380-L401",
           "version": "repo-lines-v1:sha256:c8657638062a2f1c8faa48bb2091b77a95206c5c18fb7f83ad66ee472be3b0cc:eyJzZWxlY3RlZExpbmVDb3VudCI6MjIsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjgwYzA2MDFlNTlmNWFmYmUyODNlZjFmYzQxNjFlYjJmOGE1MGMwNGEzZjEzODY1N2NkMTIzOWMxYmQ4MWJjZGIiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImFhODg5OGI0M2QxZDJjNzcwOTgxMzgwNTc4Yjg1N2E3OTY3MzAxMmY4ZGFlYmYyYmIzMTkzNTk5MDg5OWNkZTgiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjViNmM5OWZlNDhhNTllM2Q4NzBhODg1YjNlYzIzZDIyNGNlYmE0Njg5MTZiMzhiOWQ5OTEwZjAwOWQzZDRjNTEifQ"
         }
       ]
@@ -41,7 +41,7 @@
       "statement": "The chat upsert keeps the maximum last_timestamp and only sets the name when the chat has no non-empty name.",
       "evidence": [
         {
-          "resource": "repo://src/history.rs#L345-L358",
+          "resource": "repo://src/history.rs#L408-L421",
           "version": "repo-lines-v1:sha256:6acfe5aab3d7f5a481c785e16736ae25b7d72275d885003104132702619fa2c1:eyJzZWxlY3RlZExpbmVDb3VudCI6MTQsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImFlMjhiYjUwNmYwZTUwYzIxZDU0Y2ZhNWJhNTBhZDA2MzBkNDYxODJlNzUwN2U4ZDcxNzMwM2YzM2UzODBlNTkiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImM5MzAwMTI1YjUzZjIyNWI5MTFmNjc3YmFjZWM4MWM2MmIwMzAyZjg2NWJkN2NhNTAyYmNmNTZhN2UzYjdkOGYiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjgxZWMzYmUyNjA1OTc3Njk1OWJlMTExMjVhN2JkNDUzNmMxZTk2MTRiZDlkMjhkZmU5NThmOTZlM2Y4YTI1NGMifQ"
         }
       ]
@@ -51,7 +51,7 @@
       "statement": "Only the trimmed non-blank push name of incoming messages can name a chat.",
       "evidence": [
         {
-          "resource": "repo://src/history.rs#L364-L374",
+          "resource": "repo://src/history.rs#L427-L437",
           "version": "repo-lines-v1:sha256:0b03e9dc798065c6f1704fcb1a70ba4927bcb0ef47900c115b45a6cca449bfaf:eyJzZWxlY3RlZExpbmVDb3VudCI6MTEsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImMwOTZhZGVlYmNmNjA0ZWMzMTNlMTJkOTI4ODk0NGE4OTZkYmJhZWRlZDJkMzdmZmFjYzFmOTRjMzc4MjVjYWUiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjhhN2NlZDc2NTcxNDVkZjIyMjc1ZDI4Zjc0NGI5Mjg1YWVmYzU2NmE5NjZjNjhjYzVhYjQ4YjgwZGQwZDgxYjAiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjlhZDRhMTk0MTFmMjVhNWNjOWFmOTAzNWNhMmQ2MmY5ZjdmMzViOWIzYTJmZWJlNDczY2JiZDRmOTNlZTEwYWEifQ"
         }
       ]
@@ -61,7 +61,7 @@
       "statement": "recent_messages returns the latest messages up to the limit in ascending timestamp order.",
       "evidence": [
         {
-          "resource": "repo://src/history.rs#L117-L135",
+          "resource": "repo://src/history.rs#L122-L140",
           "version": "repo-lines-v1:sha256:ae0fb8914ec3b6efd265ce9615d6c692fb12c57e2d724423247cb5d9f7780159:eyJzZWxlY3RlZExpbmVDb3VudCI6MTksImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjA2NTI5NjE3OGQwMTk0ZTc4MmI1ZWM1ZTY5NTZkMTY5YzBkNjA3ZTBmN2UxOGE2ZjE1MWUxNmQ1Y2U1NGFjZmIiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImVjMzAxNGZjZDcxYmQ4ZDRhNzE5ZWFlNTgyZDM3ZDQ0Nzk0ZTBmNzFkNmE5YjM3Yjc1MTFlMWMyZmUzYTU4MTIiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjhlZWMyZTE2YzJiYmJhOWFmMmQyMGY4MTcyNTJhZmQ3ZDdiYWRhZjgwYTM0YTcwYWE3N2Y0YzM5NzQxNzFhNzYifQ"
         }
       ]
@@ -71,11 +71,11 @@
       "statement": "list_chats orders chats by last_timestamp descending and names them by contact name, chat name or JID user part.",
       "evidence": [
         {
-          "resource": "repo://src/history.rs#L168-L184",
+          "resource": "repo://src/history.rs#L173-L189",
           "version": "repo-lines-v1:sha256:21c241cfd2393adfe0d73994422db78368373279b5a142e4604f76c30b8092c6:eyJzZWxlY3RlZExpbmVDb3VudCI6MTcsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImViODI2ZTYxYWIwODc5YTZkZGEzYTVhZWYyNTc4NTEyZGMzNzYyMjA0ZjcyNDc5NzI3MWVhMDI1YzFlMWI2NjEiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImFlNjMxYmYzODIyNWIyNzIzNWJjOTE2NTg0YjEzNWMwZjZkNWY3NjQzYzYzZDVkMTU3NzYyODM1NjdjMTRiNTIiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjdhYWZlOThkYmNlMTM2NmMxYmU4OTdkZjk0MmE3NzFmZDI0YzQzODE1Nzc3ZDUzZTAzNjgzYmEyYTk4Zjk3N2UifQ"
         },
         {
-          "resource": "repo://src/history.rs#L405-L416",
+          "resource": "repo://src/history.rs#L468-L479",
           "version": "repo-lines-v1:sha256:65dc24e0ca6d8e29340e1116eab12f58e8fd73c4ed13f85023a071ca22aacfac:eyJzZWxlY3RlZExpbmVDb3VudCI6MTIsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImZhMDdjZTgwZGE1NThjZWQxZDcxMjdiMjIzN2U0OWVmOTQ5OGNmYTA1OGNjNDZmMzY0NzdhNDcxM2ExNDljZGYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6Ijg2NTQ5MzhhNmY0OTljMDQ1ZjcyY2ZkNzNhNDNkMmExZWQ4MjhiZTk3YjI2ZTc1OWY1ZTQ3YjQ2MzllNzcwY2YiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjdiYzk4ZDZiMGRjNDkyYzUwMTZkZjEwODA1YTNhNTRlYjljMTg0YTVhNjEzN2U3ZjRiZWNmYTBjMjIwNTUwZDAifQ"
         }
       ]
@@ -85,7 +85,7 @@
       "statement": "list_contacts orders by name case-insensitively and upsert_contact ignores blank names.",
       "evidence": [
         {
-          "resource": "repo://src/history.rs#L190-L229",
+          "resource": "repo://src/history.rs#L195-L234",
           "version": "repo-lines-v1:sha256:1ff51b6f0919932929d0afd41b6744292e7b841e616f004b76cfa3d7250bacf2:eyJzZWxlY3RlZExpbmVDb3VudCI6NDAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImNmMjc0NmNmOTY0YmJjYjA0YjdjM2M3ZmY4NTY5NmQ2YjNmZmUxNzMzZTkxOTQ5YjVkOGZmNDE1NmUwY2JhMTAiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjkwZDU5NDJlMDYzYTE4ZDFiOTNjOTU5YWU1YzMwZDJiYTMzNmE4NzM3NjNhZjUyMmM3NmFiNzJhMWVmNTkwNTEiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjQxNTZlNzZjODk5MjcxYzEyYzljMTY1MTg5ZmNhMGJiODI2MzJhZjBlMmM5YzA5Y2MwYjkwMDQ2NDEwNjEwYmYifQ"
         }
       ]
@@ -95,7 +95,7 @@
       "statement": "display_name resolves contact name, then chat name, then the JID user part.",
       "evidence": [
         {
-          "resource": "repo://src/history.rs#L235-L246",
+          "resource": "repo://src/history.rs#L240-L251",
           "version": "repo-lines-v1:sha256:e0059167165c59201523d30ec94c3c3e391b151fa8043b42c1010cc0f2e31c99:eyJzZWxlY3RlZExpbmVDb3VudCI6MTIsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImYyMjExZTkxNDI1Y2FlNjdlNjhiODUyN2FkMjg1OTJjNjE5NzQ3NWVmNjY0MzllYmQzYTUwMjAzNzk4NzMzOTQiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjEzOWRiMDQ5N2ZjZjUyNTVhYTJkYThhZTg0MzYyNzY3MmNhYWUwMzYyMDc3ODIzZjNiMDA5ODQ2MzMyYjRjMTUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjM1NGY2MzVmNzkyYWYxZDQ3ZTlkODM5OWY4OGFmYjkxMjgwZGMwZmFjYjVhNDc2OGYxYTc2OWEwOWVlYTg0MDUifQ"
         }
       ]
@@ -105,7 +105,7 @@
       "statement": "set_autoreply upserts the chat row and is_autoreply defaults to false for unknown chats.",
       "evidence": [
         {
-          "resource": "repo://src/history.rs#L252-L278",
+          "resource": "repo://src/history.rs#L257-L283",
           "version": "repo-lines-v1:sha256:956f230dd6020d91e8b2d7d137d9fa4fa04d0a316c598d90f38729025889cd02:eyJzZWxlY3RlZExpbmVDb3VudCI6MjcsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjNkMDNkMmIyZWM0M2RjMGIxYjg3NDg1MjQyNDRmZDc3ODVlNzcwZWJkNGJmZjBhZTUxODc5MjM1ZTFjZjM4OGEiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjQ3NzVhZDM0NjMyYTRjYjNmZjQ4NjA5Zjk2NWI4OTY3NDFmNTIzYjNkODY1Y2FiODQyZTgzZTI0YTZhOGQ3M2IiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImYyZWNiYWUyM2ZlZTE0ZTViOWIzNWQ0NDcwNmFmOGQ1OWY1NWQyOWIwNzYzNjU4ZjI4YjIzNjZkNWUzNWM5Y2UifQ"
         }
       ]
@@ -115,14 +115,46 @@
       "statement": "History::messages_since returns up to limit most recent messages of a chat with timestamp >= since (inclusive), in ascending timestamp order.",
       "evidence": [
         {
-          "resource": "repo://src/history.rs#L137-L161",
+          "resource": "repo://src/history.rs#L142-L166",
           "version": "repo-lines-v1:sha256:6daa209756bdfb87ba10954b88d7b52b35ee2212dc1328db974b019c47aab510:eyJzZWxlY3RlZExpbmVDb3VudCI6MjUsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImRiNWUzOWMyMzYyNTg2NWE5NGNiY2ZlMTVhMDNkNThiYTZkZDY3YTkxNWE4OGJjNzdiMjBkOTc1NTlmMjU2NGUiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjAzODg4YWU3MTJlMTVmZTE3NTYzNDgzMWQ4ZTcwYWZkYWEwMTMxZGI2ZDAxZWZkNzExZjQ1NmFkNThjZmExNjUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjhlZWMyZTE2YzJiYmJhOWFmMmQyMGY4MTcyNTJhZmQ3ZDdiYWRhZjgwYTM0YTcwYWE3N2Y0YzM5NzQxNzFhNzYifQ"
         }
       ]
+    },
+    {
+      "id": "claim_89c1cb5b384e4710bb7b86feb0901cfb",
+      "statement": "The global autoreply flag is stored as the global_autoreply row of the settings table, defaults to false when never set, and is independent of the per-chat autoreply flags.",
+      "evidence": [
+        {
+          "resource": "repo://src/history.rs#L18-L21",
+          "version": "repo-lines-v1:sha256:fb1e0c7f933e63c16b33cec6c6d4ababced3866d36d400b459bc168ff203aab4:eyJzZWxlY3RlZExpbmVDb3VudCI6NCwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiYzgyOGY3ZjM1MjgwY2ZjMDUzOGY5NmMwNmMxNzczZGIyODVhNTdjOWNhMWM2MDYyZjljYmU5MjEzMzA1Yzg3MCIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiODliZTkwZWUzOWU5M2U0N2MzOWRkMTg5NTY0NzNlYTkzZjkxY2MyZjVlNjdlM2Y1MTZlYTZlZTc2NjBjYWE3ZCIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiYjhjNjIyOGY4NDg1ZWFkNTEwZjNjYTEwZGM1YzRhOGE5Zjg5ZTU5MzY4YTJlNjEwZTUyMDY0ODlmMjMzY2MxYiIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiZWRhNzYxMjkwMjY1MmNkNzBlZmVjZWE4MWJhN2NhMjM5Y2NmZjViMjAzN2M2NjQyZDUzZjM5YjVlZDVmMTViNSJ9"
+        },
+        {
+          "resource": "repo://src/history.rs#L285-L315",
+          "version": "repo-lines-v1:sha256:7446cd3d43ecedda818c27075602eb8b9f1b3ef669ea39edf3a6c5520cc4bec3:eyJzZWxlY3RlZExpbmVDb3VudCI6MzEsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImE2OTlhMTQ5OTg2YzgxZjQ4OGYwMjQxMmU4Zjk0YjE1NjdhNWQ2OTEzZTNiYTg0NjJlMmFmYzY5NDJkYWYxMTMiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjRjZTNjZDVlMGE4OTI3NjIwMWQ2ZWU2OWE2ZGM4ZTEyMzgxNDVmMzEyYmE2ODQxZjUzMGYzNDQ0MmU0Njc2NjIiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6Ijk4NTAwNmMxYjY0YjM5MzI5ZWZhMmUxZmU2YTEwMWRiYzg0MTIzYjkxNjkyYjM1MjhjOTg4YzhhOGVmNGRjY2QifQ"
+        },
+        {
+          "resource": "repo://src/history.rs#L670-L681",
+          "version": "repo-lines-v1:sha256:a3499b20fe534b6969c5dd2ce949d44f4ccf9e144ef0dc6beff4386a09b24b59:eyJzZWxlY3RlZExpbmVDb3VudCI6MTIsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjdmNTE1NGVmMDNiMmZjYTVjNzk0NTNlZDVjNGIxYjI2MzllMDVhNDM0Y2U1Y2YzM2RiYjM0NTIwZDA0OGJiMmYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjAxYmE0NzE5YzgwYjZmZTkxMWIwOTFhN2MwNTEyNGI2NGVlZWNlOTY0ZTA5YzA1OGVmOGY5ODA1ZGFjYTU0NmIiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImEwMGRiMDQwZjI5MDliZjk3OGZmMGE5NTZhNTVlODNjNjUzNDE4ZGU2YmI2OGY4MDQxZGE5NzZmOGQzZDIyNzAiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImE4ZTcyMmMzMzU5NWJkZDhiNWE3YTI0YTdlM2ZlYmQxMzFjNzA5NTYyMGQ0NTllMWFjZDg4MWYzOWIzNWEzZGQifQ"
+        }
+      ]
+    },
+    {
+      "id": "claim_90e7318d49a04a75a346fa0021f65ba4",
+      "statement": "delete_chat atomically removes a chat's messages and its chats row (including its autoreply flag) while keeping the contact and other chats.",
+      "evidence": [
+        {
+          "resource": "repo://src/history.rs#L317-L337",
+          "version": "repo-lines-v1:sha256:ec26488ad71019d7a49c9f323eba53ad1d0c270a4114d45cdd0b86d09ecb04f6:eyJzZWxlY3RlZExpbmVDb3VudCI6MjEsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImNkMTFhNTg0ZDdhNWU5N2U1ZmY4NmU3MzY5ZDBkMzdhNGYxZjlmZjBlYzRlMzk5OWEyZWRkYTc2MDMyNTQwMGYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImY5OTM0OWRjNmNiNTFlOWIwMzQxMTdiZjQxMzkzZDE5YzQzMGM3NTNmZDcxZWZmZmZjZmQzYmIzOGFhNjU0ZmYiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImYyZWNiYWUyM2ZlZTE0ZTViOWIzNWQ0NDcwNmFmOGQ1OWY1NWQyOWIwNzYzNjU4ZjI4YjIzNjZkNWUzNWM5Y2UifQ"
+        },
+        {
+          "resource": "repo://src/history.rs#L695-L717",
+          "version": "repo-lines-v1:sha256:167f97ca14887b3abaed94df2233c76fafe73a7a65f58396500a10a95260ff41:eyJzZWxlY3RlZExpbmVDb3VudCI6MjMsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjhkZWE5MGY2NjhiMTNkZjZiMjNmMmYzNjhiYzYyYzY5ZWU5ZTI0YmU2MzJjMGI4MTg4MTljOGJiYTJiNTRiMWYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjAxYmE0NzE5YzgwYjZmZTkxMWIwOTFhN2MwNTEyNGI2NGVlZWNlOTY0ZTA5YzA1OGVmOGY5ODA1ZGFjYTU0NmIiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImEwMGRiMDQwZjI5MDliZjk3OGZmMGE5NTZhNTVlODNjNjUzNDE4ZGU2YmI2OGY4MDQxZGE5NzZmOGQzZDIyNzAiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6Ijk1ODAwYjU1YWE2ZWY1OGE3ZjFhZDVhZmEwNWE2MDNhZWE3YmE2ZTBmZjEwY2M5Yjc0NDMwY2I1Yzk4ZTMyZDkifQ"
+        }
+      ]
     }
   ],
   "verification": {
     "by": "openwiki/0.7.2",
-    "at": "2026-10-19T19:53:56.480Z"
+    "at": "2026-10-20T20:27:06.515Z"
   }
 }

+ 80 - 28
openwiki/.claims/integrations/ai-client.json

@@ -1,13 +1,13 @@
 {
   "schemaVersion": 1,
-  "pageVersion": "sha256:21cc0fb63966a544bddde383e86cf876395c9fc874b47d7f29a65bec3fdf80e7",
+  "pageVersion": "sha256:e30d17eaaf9e0d8e88f0bc51ae0a0c586c23d7bea67344aa86f5188912c80441",
   "claims": [
     {
       "id": "claim_c5dd9ee6003245adaa1701eb40862582",
       "statement": "Role serialises to system, user and assistant, and ChatTurn holds a role and content.",
       "evidence": [
         {
-          "resource": "repo://src/ai.rs#L40-L65",
+          "resource": "repo://src/ai.rs#L42-L67",
           "version": "repo-lines-v1:sha256:2985d21aab6a595c3f5ede27b794f4b4793b24bfb2676f79788e10d0f5c1638c:eyJzZWxlY3RlZExpbmVDb3VudCI6MjYsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjA5YzYwYmU2NjI3ZGFmNGM2MzEzZThlMTdmNzExNzFjY2RlYWU5MjZiNTBlZmU1ZWIyNTljN2FhN2Q0MGUzZDUiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjJmNzFiMjQ4YTgyZjdiZjQ0YzUyZmY4ODk2NDNiZWJmNGI1YWNjZmQ4N2YzOWIzNWIxNGZiNTU2MmRkMTRlODgiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6Ijk0ODgzMTY4NjYzMDEyM2RlNmQ0ZmJlNmJiYjBiZmM5Njg1MzgzOTI3MmQ1YTNmYzFiZjViMjdiZWY3NjlkNTUifQ"
         }
       ]
@@ -17,18 +17,18 @@
       "statement": "AiClient and ImageInput Debug implementations omit the API key and image bytes.",
       "evidence": [
         {
-          "resource": "repo://src/ai.rs#L79-L114",
-          "version": "repo-lines-v1:sha256:b5153594d9ad9919e3ec2998b8a919a82896f11b12e49d60f9ba2a0e9d9e99e8:eyJzZWxlY3RlZExpbmVDb3VudCI6MzYsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjE2NTYxZDE4YzQxMTY4MjBjZGFiOTJmMjQwNDVhNzlmMTVlNzZmZTdiY2FiODY4Y2JkYTMyZWI1YzM0NWQ1YmMiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjlmNmJiOTYyNTQ3MDUwOWQxZGNiM2Y4NmFhZTRhYmY2MzY1NTgxZTYxOTQ4Y2IxNGE4MDUxOWIyNzA0NGVlN2YiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjIyZmQyY2VkZGZlMDVkZDM4ZTYwZmRjYzkwNGU1OWZkZTM3Y2IxODY4Yjk3MzhlODJkNWI2ZTIxZmE3Nzg0OWYifQ"
+          "resource": "repo://src/ai.rs#L102-L138",
+          "version": "repo-lines-v1:sha256:252913842faf254329b50d5fd74240a84fdd0456ec9e8e68bcc77e1d8883daa4:eyJzZWxlY3RlZExpbmVDb3VudCI6MzcsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjE2NTYxZDE4YzQxMTY4MjBjZGFiOTJmMjQwNDVhNzlmMTVlNzZmZTdiY2FiODY4Y2JkYTMyZWI1YzM0NWQ1YmMiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjM3ZDYwZjQxMWJhNTUxYTEzOGUwOWJkYjdiMDg5YjRjMGE3NDUyNTU2MzUzYmE3YTAxNGVhODA5ODEyY2M1Y2QiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjIyZmQyY2VkZGZlMDVkZDM4ZTYwZmRjYzkwNGU1OWZkZTM3Y2IxODY4Yjk3MzhlODJkNWI2ZTIxZmE3Nzg0OWYifQ"
         }
       ]
     },
     {
       "id": "claim_5dfcd52a48d349059a04d949e6637509",
-      "statement": "AiClient::new requires openai_api_key, uses a 60 second request timeout and builds the endpoint from the base URL without trailing slash plus /chat/completions.",
+      "statement": "AiClient::new requires openai_api_key, uses a 60 second request timeout, builds the endpoint from the base URL without trailing slash plus /chat/completions and copies the retry count from ai_retries.",
       "evidence": [
         {
-          "resource": "repo://src/ai.rs#L123-L146",
-          "version": "repo-lines-v1:sha256:db0bee879f2e4478a109162fbc51bdecbf1b9713d605a6c093c17f94bcc77662:eyJzZWxlY3RlZExpbmVDb3VudCI6MjQsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQyMDRmMzBiYmM1OTNhOGQxOGFiYTY3OWYyMWQ0ZGY1M2JiN2YxYjdlYWIwYTNkNDRmOWJhMjgyZmVhYjFhYjUiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjA2YWQyYTE2MzQ0NTcwYjlkY2QwODA0NjQ4Y2NhNzgxYTA0YmRiNWVmMTkwYTQxZGRlOWQ1N2NjYzE0ZjBhY2QiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImY0OGYzMmRjZTI0MWMwNmFjYzBlZjdjZTc4NWM4MGZjM2E5NTk3MTZlODYwNTcwYjdkZmM0MWNiYjg0MWZjNDkifQ"
+          "resource": "repo://src/ai.rs#L147-L171",
+          "version": "repo-lines-v1:sha256:44a0131bcff163d3b23847cb3c2a056e5eadd8bc591340fa6c19aa263ff24757:eyJzZWxlY3RlZExpbmVDb3VudCI6MjUsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQyMDRmMzBiYmM1OTNhOGQxOGFiYTY3OWYyMWQ0ZGY1M2JiN2YxYjdlYWIwYTNkNDRmOWJhMjgyZmVhYjFhYjUiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjA2YWQyYTE2MzQ0NTcwYjlkY2QwODA0NjQ4Y2NhNzgxYTA0YmRiNWVmMTkwYTQxZGRlOWQ1N2NjYzE0ZjBhY2QiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImY0OGYzMmRjZTI0MWMwNmFjYzBlZjdjZTc4NWM4MGZjM2E5NTk3MTZlODYwNTcwYjdkZmM0MWNiYjg0MWZjNDkifQ"
         },
         {
           "resource": "repo://src/ai.rs#L23-L26",
@@ -41,7 +41,7 @@
       "statement": "The default base URL is https://ollama.com/v1 and the default model is deepseek-v4.1-flash.",
       "evidence": [
         {
-          "resource": "repo://src/config.rs#L30-L32",
+          "resource": "repo://src/config.rs#L35-L37",
           "version": "repo-lines-v1:sha256:ace88a2257793291dac3b3a9be115c8460366e7dce83953bc6d52977e07763a4:eyJzZWxlY3RlZExpbmVDb3VudCI6MywiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiZTc2MTBlZDMwOGNjNWU5MmE3MzAxODA5ZDE1OWZiODlkOTg5ZjIxYTNmZjMwZTc5MGQ2NjU4ZDAwNTM0MThjNCIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiYWM0ZGZhMzdlOWNjNjNjNTM4MmEzNjg2M2Q2Nzg3ZWU0OWViYTU2N2EyNGFiYmYyZGI3OTlhN2YwOWFjMmI1NCIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiY2JiNmQ0YzQ2ZTZkZWY5ZGZmYWRiYzZiNTIwN2U4N2EwZTI4MzQzMjliZmI0M2I1YjI2NmU2MTA0Y2RlZWJiMiIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiN2U4MDVmNGIzNWUzODc2YTEyODk5MTUyODJhZGE1Y2QzM2E3N2Y1MzJiZjI5YzE4Yzg5ZGEyMmIxNGNmNTBiZCJ9"
         }
       ]
@@ -51,12 +51,12 @@
       "statement": "complete sends a body with the model and role/content messages.",
       "evidence": [
         {
-          "resource": "repo://src/ai.rs#L155-L157",
-          "version": "repo-lines-v1:sha256:a3ec9a09465e569f49aa094724a4248349e01af6882b33eff3ef98c7a36479b5:eyJzZWxlY3RlZExpbmVDb3VudCI6MywiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiMGE5MGZlNDNkOTJhMDA1YTM3YjEyYmM5Nzk3MTIxNGVjNDc1NDk2MTc0MmUyNzQ0OTgwODQ0ZTVkYWFlMTNmNSIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDg5M2Y0MGUzNzgxZWI4OTZjODJkMDQ2YTA4MDY3YTExZTM1Y2RmMmYxZGZhZjI2ZmVhOTBiOGRiNjY3NjQxMyIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiMGM3M2U1ZDEwODQwYmM2YjFhODk4YTJlNmM0YjY4MTg3NWJlOTYzYmZlMzU4NDU1MWRmNjZiNTUyZjRkNmQyZCIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiM2JlODA2YWQyMDg1ZmYxMDNjZGJjODM1ODRkYjM4MGRmNDU5N2YwNmQ0YmY5NDk0Nzc0ZDVmZGVlODliZjBiYyJ9"
+          "resource": "repo://src/ai.rs#L180-L182",
+          "version": "repo-lines-v1:sha256:6c710c12e7719cdb505b12f9af500f78a0569c21c8098a12c1b53b5c19d43207:eyJzZWxlY3RlZExpbmVDb3VudCI6MywiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiMGE5MGZlNDNkOTJhMDA1YTM3YjEyYmM5Nzk3MTIxNGVjNDc1NDk2MTc0MmUyNzQ0OTgwODQ0ZTVkYWFlMTNmNSIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDg5M2Y0MGUzNzgxZWI4OTZjODJkMDQ2YTA4MDY3YTExZTM1Y2RmMmYxZGZhZjI2ZmVhOTBiOGRiNjY3NjQxMyIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiMTNiOWZjMjM4M2MwMzAwYjUwMzI2NzhkMjUxNDZjYWE3ZDJmODZlOTQwMzZmZTg3ZGI2NTU2ZjZlZTk2MzVjZSIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiM2JlODA2YWQyMDg1ZmYxMDNjZGJjODM1ODRkYjM4MGRmNDU5N2YwNmQ0YmY5NDk0Nzc0ZDVmZGVlODliZjBiYyJ9"
         },
         {
-          "resource": "repo://src/ai.rs#L251-L265",
-          "version": "repo-lines-v1:sha256:8da73c5db165f5fa75abc3690914fecd0fa4f4cf69f9b6e00754f6aa06259d9b:eyJzZWxlY3RlZExpbmVDb3VudCI6MTUsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjdiZjlhNjk0Mjc3MDY1MDI5ZThmZDIwMjYwYzBmMzZjZDNjMDYzYzQ5Yjg3NTU2YzI1NzdjZDZiZmZjODM3NDEiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImMwNDdhZDE2MzZiMWQ2OTc3ODM5NDBjNDMxZmIzN2IwZjFhMWRjNTNlMzc2YzI1YWZkZGUxMGJlYjI3ZGE1MjAiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImFhYTYwMmMyOGUzMzA2N2Y1NjhmNDcxYmYzMDQwNGM0ODllMjZkZGJiNmU3MWZiZDVhNDlhZTY4NTIwMWQzZDEifQ"
+          "resource": "repo://src/ai.rs#L325-L346",
+          "version": "repo-lines-v1:sha256:fc6890863f2d103b4db6f7460506060b7e629e95f79e42e6bca5d92ecf262f51:eyJzZWxlY3RlZExpbmVDb3VudCI6MjIsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjdiZjlhNjk0Mjc3MDY1MDI5ZThmZDIwMjYwYzBmMzZjZDNjMDYzYzQ5Yjg3NTU2YzI1NzdjZDZiZmZjODM3NDEiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6ImQ4MGYwYWFiOGM0NDViYTQ5ZGYxNmVjMDY4ZDk0ODgwYjJjZGJhYzc1MDE3MWU3YzgyMzg4MzMzNTExM2M3YTYiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImMwNDdhZDE2MzZiMWQ2OTc3ODM5NDBjNDMxZmIzN2IwZjFhMWRjNTNlMzc2YzI1YWZkZGUxMGJlYjI3ZGE1MjAiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjczNTE1ZjNiMTM5NmQwNmQxMDZlZmM0ZWM5ZDdjYjU0MjVlMWJjMzU3Y2I2OGJhM2U4NTBkMDVmOWY2OTRjNzEifQ"
         }
       ]
     },
@@ -69,12 +69,12 @@
           "version": "repo-lines-v1:sha256:cddeca6fba6fde7769f6cdec95f08d62b34b93c4f1a6822927f4b106a7db0a07:eyJzZWxlY3RlZExpbmVDb3VudCI6MywiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiNTkyZDVjNzYwZTNiYmRlNWQ3MDRlNjE4YWY1ZmVmZTY5YWMzODEyMjg0MGU3NTUwYjk1N2RlYTZkMjFiY2YxZiIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiMGJmZTUyOGM4OWM4NTc2YTQxMjYyZDU4NTdkMjE4ZDFjYWIzYTFhYzY4MzVlMGVjMDI4ODI2ZDk5YWVkZWVhNiIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiNDdiNTMxMDc0OTNkYTIyZTIzZmQyYTlmZTVjY2EwYjFhOGQzYjM2MmI2YzQ1YjkyMjZiZDM1OWE5MzgxODFiMyIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiMDM4NDJhNGNmOTA1YmJmOWY2NmFkNTY5ODljYjZiYmFhNjNhMzQ0YmIwMjUwNjY4YmU2ODVhNjZlMGRjYjgwZiJ9"
         },
         {
-          "resource": "repo://src/ai.rs#L163-L188",
-          "version": "repo-lines-v1:sha256:09e6dc97be82afa541e08754b7972d99101012d23043f13a33a5b715fae942de:eyJzZWxlY3RlZExpbmVDb3VudCI6MjYsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjkxYzVkNjRhYTJhNTc0N2NmZjZkNGI0NTZhMDU5MjdiYjE0NTYxNDVmN2Y3NTMxMzRkNDJjZjk4ZDE0OGE5ZWUiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjgxMWVjNjQyYTI2NDEzZmE1YjBmYWI4YzA5YzhlODhjZTU1MWM5YzgyNDM5NTlhOTlmNDI4NGVmZDg4ZTdiMzYiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjhjYjUwZmYxYTFiNjc3OTk5YTRkODM5ZDhlMjEwYTg3ZTFjMTE4OTYzOWE4ODYzYzJmYjlhZTAxMjI2ODFlNmIifQ"
+          "resource": "repo://src/ai.rs#L200-L213",
+          "version": "repo-lines-v1:sha256:09ee9e9276c0c6cfe14563db8840a913e8c6c327a0394f7b9be32282dfd32da6:eyJzZWxlY3RlZExpbmVDb3VudCI6MTQsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjFmNmNhMjIzNTg1NmI2MmM3NmZjMDk5ZDk5ZGFiOWFhMzRiY2I2MjM3ZTY0NjkxOTE2OTM4ODVhYTA2MzMxZjciLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjAwNTU5ZGE4YzBkNGQ0Y2ZkMjFkMzZhZGFlMWFmMjNlYWM2Mjk5OGU1Mjg5NTJiMDViNTcxY2JkNTZiOTVhODQiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjA1ZWY0YzRmMGNjMDcxMzBjOTc1ZWVhM2E3MzdkMGJmOGNjZjY3MThiODUwMDY5NWY0NTU2Y2E2OTk5NmQ0YTcifQ"
         },
         {
-          "resource": "repo://src/ai.rs#L311-L323",
-          "version": "repo-lines-v1:sha256:243c30ce27815b3f4e71973311ebe2902652ba4d0e469d4320b987046b4aea2e:eyJzZWxlY3RlZExpbmVDb3VudCI6MTMsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImY5YzNhMGViNjE4M2YxNzFmMmE4ZTQxY2JlNWFmMjc0YjdhNjk1ZmM5Zjk3YTAwZDUzZGQ4MmVmYmJiMDE2ZWEiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImY1NjQyY2ZlYzYwMmNlYzJhOWUyZDk4ODc3OWUzNjIwZjkzZDU2NWRjZDA1MjM1NWFjZmFlOTkyMjAwODRmYjEiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjJjZjMyZDY2ZGY3ZDk0Y2VmMmU2OTgwYjBmMDAwYzUyMTA4MmQ1ZmFhYTNjMTk4ZmM5OTVhMmYxNGU5MjE3Y2UifQ"
+          "resource": "repo://src/ai.rs#L385-L399",
+          "version": "repo-lines-v1:sha256:68a089645a7a69cf4f7f652087e4d1f3a7c37fbcdaa506a831122eef48a12189:eyJzZWxlY3RlZExpbmVDb3VudCI6MTUsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImY5YzNhMGViNjE4M2YxNzFmMmE4ZTQxY2JlNWFmMjc0YjdhNjk1ZmM5Zjk3YTAwZDUzZGQ4MmVmYmJiMDE2ZWEiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjRiNGU1YzcxMTNkZTFlMWIwOTIyYjEyYTlhM2ZhMGJjYTVmNzI5YzQ3OGM5NzA5ZDczZWYyMmUyYWM0YTEyZDQiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImY1NjQyY2ZlYzYwMmNlYzJhOWUyZDk4ODc3OWUzNjIwZjkzZDU2NWRjZDA1MjM1NWFjZmFlOTkyMjAwODRmYjEiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjhjYzlkYTNhZDRjZjZmY2RiMDkwNTE4MmY3ODgyMzIxZTE0MGU5ZWZiYTJmNjBhMjJjODdiY2YwYzk0ZWVkNTAifQ"
         }
       ]
     },
@@ -83,22 +83,26 @@
       "statement": "The vision body turns the last user turn into text and image_url parts, or appends an image-only user message, embedding the image as a base64 data URL.",
       "evidence": [
         {
-          "resource": "repo://src/ai.rs#L274-L304",
+          "resource": "repo://src/ai.rs#L348-L378",
           "version": "repo-lines-v1:sha256:0421b4eafacfa0da02301fb95fc4b94d4cd4b0a9d61791112860348f252e7675:eyJzZWxlY3RlZExpbmVDb3VudCI6MzEsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjdlZTMyOWRjZTI2MzIyY2FkMmI0N2E0NGU0MjY1YmEwNzM3YjM1NDJjMjM1MzUwZmQyMWZlOGI0MTNjM2M4ODUiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImRjNWUzZjQ5YTQ0MjE1MjlmZmI4NTMyNzkzY2ZlZTllNjlkMzFhNWZhMWY4YzA1ZGJlMTcwZGI0MTRlMGQxYjMiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjJjMWU2Yzg2MTE1NTc0YWZlM2FhMjAwNDYxYzRkMmUzZmRiNzI5ZjFmYjMyMjQzN2VmNGJhNWZmYzJmMDkxZGIifQ"
         }
       ]
     },
     {
       "id": "claim_8b2297ed4cce446aaf66b88450532f54",
-      "statement": "request_reply posts with bearer auth and reports transport, read, non-2xx and invalid JSON errors with response bodies truncated to 300 characters.",
+      "statement": "request_reply posts with bearer auth and reports transport, read, non-2xx (as a typed StatusError) and invalid JSON errors with response bodies truncated to 300 characters.",
       "evidence": [
         {
-          "resource": "repo://src/ai.rs#L217-L243",
-          "version": "repo-lines-v1:sha256:377803e3746f5be90128e5ac22c90103dc416685b89058e065fadf2ae66495b2:eyJzZWxlY3RlZExpbmVDb3VudCI6MjcsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjFmYWQyYmNjYzU1ZWJiNGE5OTJlNjQ1YmE4ZDBiNDc3MWFjNDk0YTcwMDhlMDUxOTk5MmMxOWQ4ZjQwYTFmYjEiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjUzNGRhMTA2NDE1N2QyNzIyZWNjYzIzNTUzNTlhZDIyN2IwYjQwNDU5YWRkMGM0YWRhODZkMTNhZDZlODJjZTUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjlmNzAxY2VmNDIxNmQ1ZmRhYmUwMzRiMWRiMDQ3YjRlNmZhNWExMTM2YTM1MzEzNDg1OGYzNmQ4NjlhNTZiZDQifQ"
+          "resource": "repo://src/ai.rs#L265-L291",
+          "version": "repo-lines-v1:sha256:b80ccebb98e52d79a2155c3e75a8de42dc9ab06286c6639216c4094f37a90562:eyJzZWxlY3RlZExpbmVDb3VudCI6MjcsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjFmYWQyYmNjYzU1ZWJiNGE5OTJlNjQ1YmE4ZDBiNDc3MWFjNDk0YTcwMDhlMDUxOTk5MmMxOWQ4ZjQwYTFmYjEiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjUzNGRhMTA2NDE1N2QyNzIyZWNjYzIzNTUzNTlhZDIyN2IwYjQwNDU5YWRkMGM0YWRhODZkMTNhZDZlODJjZTUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImI2YzllMjYwYjQyZThmYmJiMDRiOWZiM2UwOWEzNmMyZjI1OWI1MDBhOTM1OWE0OTYwMTA2NDYxNWIxY2Q5YjMifQ"
         },
         {
-          "resource": "repo://src/ai.rs#L448-L450",
+          "resource": "repo://src/ai.rs#L522-L524",
           "version": "repo-lines-v1:sha256:81326ce3f8703f9c90b0068b202df13efb9689c0fe8ee236b5f77cbaa6edc4d4:eyJzZWxlY3RlZExpbmVDb3VudCI6MywiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiZGYzN2E0ZTlhMDhhNjc2ZjRjMTg3MTA1ZTk0OTQ2YWM4MjhiZWM4ZTZhMjlmYWMxYTk5NDk2ZDBiNjYwYWIyZCIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDEyY2EzNDVjY2Y3NWJmOWMwODA2YmNlNjk1YmU4ZGU4MDhiNzk5ODQyNTFhN2E1NGQyMDJjZjYxMDFkZDQ1MSIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiN2UyNTA4ZjI2MjVhMWRhY2EwMjIyZWMzYTdmYzNkMWQ4MzZjODdlZjQyODVhYTlmYzZiZmQ0ZTAwN2MzOGY4NCIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiN2JjOThkNmIwZGM0OTJjNTAxNmRmMTA4MDVhM2E1NGViOWMxODRhNWE2MTM3ZTdmNGJlY2ZhMGMyMjA1NTBkMCJ9"
+        },
+        {
+          "resource": "repo://src/ai.rs#L83-L98",
+          "version": "repo-lines-v1:sha256:7772287888f472e0806adf8646ac05d7d32a9518c29e32a7359cf3194b2230c7:eyJzZWxlY3RlZExpbmVDb3VudCI6MTYsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImVmOTlmZDhkOTIwYjk2MTFiZmI3MDkxZTJjZTU3ODM3ZTY5MzY2NmVkNjRjMzFmNmIzMDY5MjJhYjVjOTBkMjUiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImJjMDk2M2MxZWY1YzRiMjZkZDNmNGY2MDhlZGRhZmNhMDRkMDVhNjI1ODk1YTcxZTYzMmY2N2Y4OTQxYjc2M2IiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjM3ZDYwZjQxMWJhNTUxYTEzOGUwOWJkYjdiMDg5YjRjMGE3NDUyNTU2MzUzYmE3YTAxNGVhODA5ODEyY2M1Y2QifQ"
         }
       ]
     },
@@ -107,7 +111,7 @@
       "statement": "parse_reply returns the trimmed first choice content and fails on missing choices or empty content.",
       "evidence": [
         {
-          "resource": "repo://src/ai.rs#L331-L348",
+          "resource": "repo://src/ai.rs#L405-L422",
           "version": "repo-lines-v1:sha256:b4141c4af9d4b72b638ca3901c24c019a87de5cc0149465761d63c1c2a7b30c0:eyJzZWxlY3RlZExpbmVDb3VudCI6MTgsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjlhNGJhYzcxZjEwYTkwZWNmMzZkMzM5ZjJjZGZkZmY3OThmNTk4NDE4ODc0NWM3M2IzYTkzMDVmMTE0YjI4ZDAiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImIxNDJlMWMwZDkyM2E5YWY0YzczOGRhZThhMjkwMjg1YzRmYmNkMmY5YmQ1OGY1NGMzMTk1M2NhMTliZDE4ZTQiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjJjZTgwZDQ1MzU3ZWY2NWZiNjUzODgzZWEzOTU1NDMyMDMzN2Q0YjA3MzcxNzQ1ZWE0MGFkYzRmMGFjNTU0YTcifQ"
         }
       ]
@@ -117,15 +121,15 @@
       "statement": "When ai_log is enabled, send_body appends one record per request (header with timestamp, endpoint and model, pretty request body, RISPOSTA or ERRORE outcome) to ai_requests.log in the data directory, ignoring write failures.",
       "evidence": [
         {
-          "resource": "repo://src/ai.rs#L190-L209",
+          "resource": "repo://src/ai.rs#L238-L257",
           "version": "repo-lines-v1:sha256:ba98f0d5c85a04762a0b5b0f52b0312a6fe7010f6894234828e6040933fd49bd:eyJzZWxlY3RlZExpbmVDb3VudCI6MjAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjUxNzY0ZmUzOGM2MTVjODM3MzM2M2ZhM2U2OTVhNTQ3Y2M0MGM5MjhmZjIwYjE0ZTI3NGRjZjQxNDI5NjI5MjQiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjZiMjBlNzhiOGRlMmI1ZDBlYzRlNDgwN2I0ZTNhODFmYzIyYjczMTQ4Mzg2ZWVmNzM1ZjZjYzFlMTIyYjRkZWMiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjJmNjljNjZiNTk4NDRmMjg4YzY3Yzg4NWY2NzQwNmEwNmExNjg4OWI4ZTc4NDMzNDZiMWUzNmRkNjRlYWIzNTUifQ"
         },
         {
-          "resource": "repo://src/ai.rs#L399-L442",
+          "resource": "repo://src/ai.rs#L473-L516",
           "version": "repo-lines-v1:sha256:708d606edee28a507ed556f78b94a9c7da74e9c069e02f390412464b7b2ea30e:eyJzZWxlY3RlZExpbmVDb3VudCI6NDQsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjkxYjRlNWM3NzBmZmRlY2QyZDNlNDczNWUxNjFlOGZiMTVmYmM5NTFkZWMyNmI4YmY2ZWQ3MzdjMmVhZmE4MDciLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImZkMjAxZWU4NzY1ZDNhMzAxMjY5NDkzNmVkZjFmNWQ2NzlhMzAyN2ExYTgwMzg0MjJmYTkzZTZmYzllN2RkZWMiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjJjZTgwZDQ1MzU3ZWY2NWZiNjUzODgzZWEzOTU1NDMyMDMzN2Q0YjA3MzcxNzQ1ZWE0MGFkYzRmMGFjNTU0YTcifQ"
         },
         {
-          "resource": "repo://src/config.rs#L279-L285",
+          "resource": "repo://src/config.rs#L314-L320",
           "version": "repo-lines-v1:sha256:77a53fa11fa04d405caba6426205b2c57c25edd1b59ce057d5429c3cb0af854d:eyJzZWxlY3RlZExpbmVDb3VudCI6NywiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiNzUyM2JkYmRjYjY1ZGIyYThmNWU0OGI0ZmUyMDdjOTUxYjdjNzE3NDYxYzgwNjUzODMxNjExZGI0YjMxZjdhZCIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDg5M2Y0MGUzNzgxZWI4OTZjODJkMDQ2YTA4MDY3YTExZTM1Y2RmMmYxZGZhZjI2ZmVhOTBiOGRiNjY3NjQxMyIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiNDRhMWY2MWFmZTk3ZWM0YWZjODI1OWM4ZGI4MDljZjlmMjkxNWQ3ZWUwYWRlZWJhNzdkZDMxZDBjZmJkM2JhZSIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiYTUyMTYwNGIzNWZjNDBhZjAwY2JhNGVlYTJmZDVlMmMzYWYxODYzYjM5MTE4NWQ3YmY0YmIwOWU3MmQwYzhiMyJ9"
         }
       ]
@@ -135,18 +139,66 @@
       "statement": "Log records replace image base64 data URLs with a placeholder carrying only the payload length and never include the API key.",
       "evidence": [
         {
-          "resource": "repo://src/ai.rs#L350-L397",
+          "resource": "repo://src/ai.rs#L424-L471",
           "version": "repo-lines-v1:sha256:e830f325d8c43a24ff3d984cdf42ad06554b5a92cf56bc4bf6c77a580822e60c:eyJzZWxlY3RlZExpbmVDb3VudCI6NDgsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjNhZmRkOGE1NjBlNzQ4ZmFmNGRlZDhhYjJmOWU4NmUyMTU5NDA1YTAxMGUwYzE4YjI2M2NhNDE4MzAxMzFjZTciLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjcwZmRhNDBjNDBhZTM2Yzk5ODVhZWRjMDE5MDM3NDJmMTViYmY5ZDY4N2IxZjk1MTVmZDkwOWU2ZGUzY2E2ZTkiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjFmYjc4NDZkODI2ZmYyZmU0ZGExODEzZjRmODQxM2ViOWEyMWQwZjZlNWUwOTFkMjFhMDI1OWI3NzhiOGIyNWMifQ"
         },
         {
-          "resource": "repo://src/ai.rs#L399-L421",
+          "resource": "repo://src/ai.rs#L473-L495",
           "version": "repo-lines-v1:sha256:e7220f3e055e3aba45c898158efa96ce9e802fc18cbcac7911028d984018c8ad:eyJzZWxlY3RlZExpbmVDb3VudCI6MjMsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjkxYjRlNWM3NzBmZmRlY2QyZDNlNDczNWUxNjFlOGZiMTVmYmM5NTFkZWMyNmI4YmY2ZWQ3MzdjMmVhZmE4MDciLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImZkMjAxZWU4NzY1ZDNhMzAxMjY5NDkzNmVkZjFmNWQ2NzlhMzAyN2ExYTgwMzg0MjJmYTkzZTZmYzllN2RkZWMiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjNhNTdiMWEyNDNlMjM2MjAyZjY2OGUyYmMwY2EyMTMwNDJmOTgwYzM4NGY4ZmMwZWRlNWZlN2ZiYzNkOTEyY2UifQ"
         }
       ]
+    },
+    {
+      "id": "claim_9851fadc06844be99a0f3d957816a806",
+      "statement": "Completions are sent through send_with_retry, which makes up to 1 + ai_retries attempts, retrying only errors accepted by is_retryable and returning the last attempt's error.",
+      "evidence": [
+        {
+          "resource": "repo://src/ai.rs#L215-L236",
+          "version": "repo-lines-v1:sha256:d5a01e2627147cde8f8cbee315d1c0b7ec0b988745e4b6de771e10c7aecdb27e:eyJzZWxlY3RlZExpbmVDb3VudCI6MjIsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjYwNzBmYTg1ZjNiN2RjOGYwM2QwYjBmMjA2NWNhYzQ5NWYxNWQyNzQwMWExN2I4YzE2Y2Q4ZmQ5YWIwMWQ0MWQiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjZiMjBlNzhiOGRlMmI1ZDBlYzRlNDgwN2I0ZTNhODFmYzIyYjczMTQ4Mzg2ZWVmNzM1ZjZjYzFlMTIyYjRkZWMiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjExYWIwMTYzYTk2YjkzZWI0ODA1NmUxM2Y4ODUzMTJmOGVhNDQ5NjcwYmY3MWU2ZTRiZTAxOTU0ODg1OTk2YjEifQ"
+        }
+      ]
+    },
+    {
+      "id": "claim_3db025b2e15e4fafb172f9c85b626365",
+      "statement": "is_retryable rejects HTTP 4xx responses other than 429 and accepts 429, 5xx and every non-status failure.",
+      "evidence": [
+        {
+          "resource": "repo://src/ai.rs#L294-L309",
+          "version": "repo-lines-v1:sha256:29bbd06d8acee1a3e8788f3a5d669981da5c3ff1da3d4ad7b5a66e0cfcd3a69a:eyJzZWxlY3RlZExpbmVDb3VudCI6MTYsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImQxOGVkNTcxN2U3MjlmODcxM2RkZjJiYWE4ZGI1ZGZlZDgzNWYyY2JhZDI3ZGEwNTUwMzgwMWNiNmZlNTAwNGYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjE5ZTc2MzU1Y2YxMTk5NDgwYzNmOTk4MzZiMTQ4NmM4NDFkODkxZmFhZDkwNGM2MDhlMDkzMDIyZDM3NTdhZWUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjVjZGFjZmMzNDYzZjY1NDljYjAzNTQ0MzdiNjg3N2YzYmNlZjVhNWQ4NDk2YmMwYjFhYzc0YTE5OWUyY2Q0YTEifQ"
+        },
+        {
+          "resource": "repo://src/ai.rs#L815-L827",
+          "version": "repo-lines-v1:sha256:ca506079bde5869a2030b03b404d24fe4f1aff152bd99d9f352fb55bbbd97ef9:eyJzZWxlY3RlZExpbmVDb3VudCI6MTMsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjBiYjUyNzU4NmNlYTg4OTNjNTY4ZjQzNDcxMTYwZjc1ZmY1YjAwMmJlYmFmYzExYmYyYzAxNDYwZDIzZDM4NTYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjAxYmE0NzE5YzgwYjZmZTkxMWIwOTFhN2MwNTEyNGI2NGVlZWNlOTY0ZTA5YzA1OGVmOGY5ODA1ZGFjYTU0NmIiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImEwMGRiMDQwZjI5MDliZjk3OGZmMGE5NTZhNTVlODNjNjUzNDE4ZGU2YmI2OGY4MDQxZGE5NzZmOGQzZDIyNzAiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImFhNTVkYzhiMzVlYzYwZGNmZDI4Mjc4NzJlMmM4MjAyNmFhN2I3NGZmMjMyMzUxZmQ4MGVmZTgyZjhkNjUzZjUifQ"
+        }
+      ]
+    },
+    {
+      "id": "claim_7034e6fb066940d290795dfacb0eec1f",
+      "statement": "The wait before retry number n is 2 seconds times n (linear backoff).",
+      "evidence": [
+        {
+          "resource": "repo://src/ai.rs#L311-L318",
+          "version": "repo-lines-v1:sha256:97d05ac13a6dbe53449bb68ab41111ae5cd01fd47dfe333a12720fe3807b20ca:eyJzZWxlY3RlZExpbmVDb3VudCI6OCwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiOTIwYTE1YWFhMWYxOTliZDY5OWE4ZDYwMmUxZjBmZTI4Y2EzZjY5MDBiYzdkMzRlNTA5ZjA3MDQ5ZjRkN2FiNCIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDEyY2EzNDVjY2Y3NWJmOWMwODA2YmNlNjk1YmU4ZGU4MDhiNzk5ODQyNTFhN2E1NGQyMDJjZjYxMDFkZDQ1MSIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiMTllNzYzNTVjZjExOTk0ODBjM2Y5OTgzNmIxNDg2Yzg0MWQ4OTFmYWFkOTA0YzYwOGUwOTMwMjJkMzc1N2FlZSIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiMmIxNGY0OGEzYjA0NzBiZWJhYWYzOTkyZGUzMWVjOTIzNzQ2OGI2NjhiZGYyOTE3NzgzODdiZGZmNmVmZWM3ZiJ9"
+        },
+        {
+          "resource": "repo://src/ai.rs#L38-L38",
+          "version": "repo-lines-v1:sha256:c0abec049dcec979dadad10a8d96775d266ee7888872117d2eee460f37308a08:eyJzZWxlY3RlZExpbmVDb3VudCI6MSwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiYzBhYmVjMDQ5ZGNlYzk3OWRhZGFkMTBhOGQ5Njc3NWQyNjZlZTc4ODg4NzIxMTdkMmVlZTQ2MGYzNzMwOGEwOCIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiYzBhYmVjMDQ5ZGNlYzk3OWRhZGFkMTBhOGQ5Njc3NWQyNjZlZTc4ODg4NzIxMTdkMmVlZTQ2MGYzNzMwOGEwOCIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiYjhhZjk3MTA0ZmI2ZjRhYjViOTFiZmQ5YWZhMzNkOTYxOTkwMGU1NWFmYmUwZmRkY2U5ZTFhYjNhYmQwOWNhYyIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiMmY3MWIyNDhhODJmN2JmNDRjNTJmZjg4OTY0M2JlYmY0YjVhY2NmZDg3ZjM5YjM1YjE0ZmI1NTYyZGQxNGU4OCJ9"
+        }
+      ]
+    },
+    {
+      "id": "claim_6439ccd0b4f54d9cad75a0e3501d84fb",
+      "statement": "When ai_log is enabled, one log record is appended per attempt, so each retry is logged separately.",
+      "evidence": [
+        {
+          "resource": "repo://src/ai.rs#L222-L257",
+          "version": "repo-lines-v1:sha256:62a72cdb2ab2e3786211b08faa8c92071d19a7d061bf7588f394b3a5f0f2e602:eyJzZWxlY3RlZExpbmVDb3VudCI6MzYsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImQ5MDNlZTRhMmIxYTAzYWQ1OTllYzZmYzk3MTljMDRlNWE4YzBiNjc2OTI5ZTcxYmM1ZjA5ZTNlM2M4ZWEyOGIiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjIxNTYzYjg3YWUxODczMjczNmEwZDU1MTcwYjRkYTE1NGY5MGE3MmI2YTE2YWNjMDRjOTMwYTgxZDFhNmFjNWQiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjJmNjljNjZiNTk4NDRmMjg4YzY3Yzg4NWY2NzQwNmEwNmExNjg4OWI4ZTc4NDMzNDZiMWUzNmRkNjRlYWIzNTUifQ"
+        }
+      ]
     }
   ],
   "verification": {
     "by": "openwiki/0.7.2",
-    "at": "2026-10-19T19:53:56.480Z"
+    "at": "2026-10-20T20:27:06.515Z"
   }
 }

+ 132 - 0
openwiki/.claims/integrations/web-control-panel.json

@@ -0,0 +1,132 @@
+{
+  "schemaVersion": 1,
+  "pageVersion": "sha256:659021f213da32179df3e732784be9d211b7b699c07342e5abae11b3d55b0d63",
+  "claims": [
+    {
+      "id": "claim_3b56098917ee4e02b2f8ede066a9921a",
+      "statement": "The web control panel is started only when web_port is configured, binds on 0.0.0.0 with no authentication, and serves requests on a dedicated wh-web OS thread one at a time.",
+      "evidence": [
+        {
+          "resource": "repo://src/main.rs#L70-L79",
+          "version": "repo-lines-v1:sha256:54c6448bd45241fd4b90479b1a91e2fd528e7a38f4ac5d543eda728fbf3756d5:eyJzZWxlY3RlZExpbmVDb3VudCI6MTAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImYwZjA3MmY1NzA4NTRiMGM0ODg3YmRlZDU4YzU2NWRiZGExOTNlYmY3NGIwODZmMjYyNTFmZjZhOTZiMmJiYWMiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImRmYWI3MTBkZGY3ODU5ZTU3NTc2NGE5MTNhMDAyYzc0NTg1ZmZhNzAwMTUxMGNiODgxMWRhODNkMmNmNzRiMzQiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImI1OTAxZTAwNGRkYTg1ZmUwNjg0OGVkYjhkMWIzM2Q1NTNmYWM4ZjkyMzZlZTFlMjlkZTNmNGUzMWRmNTM4NTkifQ"
+        },
+        {
+          "resource": "repo://src/web.rs#L1-L17",
+          "version": "repo-lines-v1:sha256:525008f2abbc8625d414d67af1579c526c618328ba3286794ced9a8b4448a47a:eyJzZWxlY3RlZExpbmVDb3VudCI6MTcsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjhiM2Y3MzZlODVhNTljMGQxNWY5NTM0YmZmM2ZhZjEwYWQ5NDBjZWQwODY3ODBkYWU1YjNlZDk2NjEwMjMyZDYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjhjNDU3YTVkZDNlNWJkNmQyZjgyZTU5MmRhN2IzOWQ3OTAxN2U0NzZkYjc0YjNhZjA0MDMwNWIwYmIyOGUxZTQiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjowLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImUzYjBjNDQyOThmYzFjMTQ5YWZiZjRjODk5NmZiOTI0MjdhZTQxZTQ2NDliOTM0Y2E0OTU5OTFiNzg1MmI4NTUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImU4ZTEwNzM3YjM4ZjIwMmZmZTRjOTFhYTU0N2IzNmEyNzA4NDQ2ODQ4MTVhYzJiZWZlMzc4MmNkMjBhMmNkZjgifQ"
+        },
+        {
+          "resource": "repo://src/web.rs#L160-L186",
+          "version": "repo-lines-v1:sha256:f8b34287072ae980835326b6b58d084cb50595fbb20e0a5a69a07e2a5107e4a1:eyJzZWxlY3RlZExpbmVDb3VudCI6MjcsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjhhZmRhOTQ2N2Y1YzJkNWJhYzg1MjVjYmQxYzgwMzlmZDYyYjk1NDJjNDg0ODlmODE2NTIxZWJlM2U1N2YwM2QiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjljYTdmZjM2YjFmNjM2MDdlZjI2OTZiMWMwODk5Y2ZiMjlhOTc5NzU4ZGZkMTYwMGYxYzAzMGE3ZDk4NzkyNmQiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjA3ODdlYjhkMTU5MmY2NGVhMmE3M2NhNjA4ZmUyOWRkNjhlZDJiNmYyMDY2MmRlN2MxMmYxNmFjMTEyZWI0ZTIifQ"
+        }
+      ]
+    },
+    {
+      "id": "claim_0efd2e13edc54ed3a8fd6f3d576511ad",
+      "statement": "The server exposes GET /, GET /api/state, POST /api/global, POST /api/chat and POST /api/quit; any other method/path combination returns 404, and the query string is ignored.",
+      "evidence": [
+        {
+          "resource": "repo://src/web.rs#L19-L28",
+          "version": "repo-lines-v1:sha256:549434436d6d39319733236098427e43f3ff8b065cef72fe6bcbe3f13a77b8f9:eyJzZWxlY3RlZExpbmVDb3VudCI6MTAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjM5YzE3NTNiNjYzYWNhNDFjNDA1MzRiYzljMGQwZGE0OGQ1NTc0ZGIxMTU2MjdiZmVlYTViY2RmYTJhMjQ5MmMiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4ZTIzMjgxODM0YjFlNjMyZTQ4NzljY2JmZGIxMjFlMDdhMmNiNGQ5ZGFjMzIxYTdmMGJlYzg1NmRhMmQ4YTciLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImNkNDMyMjkxNTVmYjM5ODIzZjhkMmY1ZDI1MDFkNzM1NzNiNjI0ODI0ODhlNWRlODg2ZGFiN2EzYTc1YjBmYmUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjgzYmIyNTJjMjU1NmVmMTM5MWJjNDU1YmEyOTAxZWQ5MTczNmQ2Njc2MmEyZThmYzExMjJlYTFhMWYzYjVmZmQifQ"
+        },
+        {
+          "resource": "repo://src/web.rs#L313-L334",
+          "version": "repo-lines-v1:sha256:1141f32c78ab436faa724518545a117d98eccf70ff4dc6295b1d0ad5be7cef11:eyJzZWxlY3RlZExpbmVDb3VudCI6MjIsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjkxYTU1YzdlMmE3ODBiZDViNzE1ZDY2MTYyYjI5M2M2ZTI2MWY2MmI2Mjk5MzY2ZWJiNTZmZjhhZTNhMDlhNDEiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjAzYjZjY2UzZDJjZWYzY2RhMWVkNWEwMzM5Y2QwMmM1ZGViNDRhNzBkMGYxNDA4MmJjZjNlYWYwMDJkN2U3NDkiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjQ0NGIyYWRlMGYzNzRmOGZjZGYxNGM5MTQxYzNkNTMxYjcxMzgwN2ZkZTc0NzI1NTZiOWJlNDI1MjE5NzM4OWYifQ"
+        }
+      ]
+    },
+    {
+      "id": "claim_696ce9a26b74435f8d5439a5f1e4ab6b",
+      "statement": "Every POST route requires the header X-Wh-Request with value 1 (name case-insensitive); without it the server returns 403 and forwards nothing to the application.",
+      "evidence": [
+        {
+          "resource": "repo://src/web.rs#L140-L148",
+          "version": "repo-lines-v1:sha256:988e5ef7a69e7a10a978cd986e4879843f82b485ee9ffcc5ffc85f1844b0348e:eyJzZWxlY3RlZExpbmVDb3VudCI6OSwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiODU5OWJkMWM5YjdhMTQ5ZjczYzQzMjQ2MjljMzczMzFmOGNlZTM1NDVhN2IzYWUwNjk1ZWM0NmFlZjBiZmJjNCIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDEyY2EzNDVjY2Y3NWJmOWMwODA2YmNlNjk1YmU4ZGU4MDhiNzk5ODQyNTFhN2E1NGQyMDJjZjYxMDFkZDQ1MSIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiNGRhMDBiODU3YTc3N2IyNzQwZmQxZjg0MWIxZWQ0MzcwNmYwOTc5YWEyNzE5Zjk1YzJjOTJjMGNkYWEzOWJkYiIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiNGNmODQ4NzQ5MjczZDdkMWJlZDQ1ZTg2ZTNmMTU3ZmY3NTE0MjIyMTFkMWE3YzFlM2RhZDIzYWNkODNiZWRmNCJ9"
+        },
+        {
+          "resource": "repo://src/web.rs#L193-L205",
+          "version": "repo-lines-v1:sha256:6f08617663cf012d21e1dd6a16a4f2b6c0c1aeb0e6816a98e6f158ac3e8b7e6a:eyJzZWxlY3RlZExpbmVDb3VudCI6MTMsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjY1MDQ0NWY4YjI4ZjA5MGJhYWIwOGMyZjcwZGM5NGU5NzE4OGVmNzU4NjYwOTYyNDlkYThkMTcxZTA4MmU3OWEiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjA4ZjhkYWY0NGE3ZDkyZmQzZWFkMGFlODI3NjVhOWQxMGEyN2FmYmEzODhiN2NhYjU2Y2ZlNjk5OGUxODU3ZmUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImUyNmM2NzRiYmY2YzlkMzNlZjM0NTEwOGJhYmYzMmFmY2UxNzc1OTMwYjU0Y2JkOWU5NzkyNWQ3NDI2NWEyNzAifQ"
+        },
+        {
+          "resource": "repo://src/web.rs#L336-L345",
+          "version": "repo-lines-v1:sha256:f6fc6a9f07a89bf80251845896e2bb83deb08f3b693960e20ec04eda2ee7cab9:eyJzZWxlY3RlZExpbmVDb3VudCI6MTAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjE1NzEzZjExZDY3OGYyYmQ4ZDhjMWUyYThjNmRlMDg4ODg4NDQzZWFkYmQ0NTdhZmQyMmJhNzlmMjY2N2RjYzkiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjE5ZTc2MzU1Y2YxMTk5NDgwYzNmOTk4MzZiMTQ4NmM4NDFkODkxZmFhZDkwNGM2MDhlMDkzMDIyZDM3NTdhZWUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjhiZGE3OWRiOTQ2MjJmMThhNzVjMWU4YmFmNzVlMTFhMWJjOGVmNzg4Yjg2M2VkMmQ2M2RhNTUyZTc0MGQyNDAifQ"
+        },
+        {
+          "resource": "repo://src/web.rs#L46-L49",
+          "version": "repo-lines-v1:sha256:c5e290866be837c820ed79dff719f958fe09c524ae81a8c5816c86113418fbf4:eyJzZWxlY3RlZExpbmVDb3VudCI6NCwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiMWM3MTlmN2I5YmZmYWVhN2I5MjhmMmE5MTg1MTA1NzQ3ZGJmYzg0YTI1NzNhZDFiNmI5NzQ2NzFlZmY0OWNlZiIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiZTJlMmMwNmY2ZDg3NDRmYzBkODEyZDc0YTAwNWJmZDhhM2U2MDc1MTFlZmM0ZWMwNDQyY2E3YjQzNGQ5MDYxNyIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiODU5M2I4MmRmMTExZjMwMmY3N2U4ZDI4N2EyNGU0NGZhNjVjYTMxYmZmNGYxNjFkZGIyNzgzNDc5MjI1NmRlYyIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiNzc4YzAzMWRlMTc3YTZlMjdhMWQxMzY5OTEzNzBlYzJhOTQ3YTBjZTRmZjQ0MmI3NjQzZmYwM2Q2NTZjZmI5ZSJ9"
+        }
+      ]
+    },
+    {
+      "id": "claim_1687a5d10c5c40f4bb1f7afee48bce96",
+      "statement": "The /api/chat body is the chat JID as text, limited to 1024 bytes; oversized, empty, unreadable or non-UTF-8 bodies return 400.",
+      "evidence": [
+        {
+          "resource": "repo://src/web.rs#L221-L224",
+          "version": "repo-lines-v1:sha256:4ec9fcc7650183cfb21c285788334f7540990a3b6ecc6c0102a5483b1042f772:eyJzZWxlY3RlZExpbmVDb3VudCI6NCwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiOTllNmY5YjI2ZmYxNmJlMThiYjFjOTA0Y2MwNWJhZjFiNTEzMzFhNjkwYTNhZjc0ZGYxNzJmOTdiNzQ3NGI1MSIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiMWU2NzNhYmQ0NTdhOTdlZWExN2E2MDA2MmViMGZhZWE2YmRmMDk0ODY4Y2M2YjhmOGMwZWJkYTJmNjFlZDQyZiIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiYmQ3ZTRhZGRhNDk4ZTkwMmJjZDA5ZGI5YzQ5Yjc4ZmE2NGEzNDk0NjgxMGUyZDhiOTBiYThjOTVkNmRkYTNjZiIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiMTcxMDE3YWI5NThlNTM3MjhlNmNhMTY0YzEzOGY1NzNmMjRhOWZjZDVhNTQzOWQwNTY2MzA1ZTE0ODQ2ODJhZSJ9"
+        },
+        {
+          "resource": "repo://src/web.rs#L30-L31",
+          "version": "repo-lines-v1:sha256:0a12154e8c7fb736954719a1bc6bbb6919359fd03e35f9e1fc18873bf3953b71:eyJzZWxlY3RlZExpbmVDb3VudCI6MiwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiM2Q1NzUxODkwZTcxOGQzNTJkMDE1MTI4ZTdhYTNmOGZlYjgwOTg0MGFlMWI4OWFjNDlhNDFlMTViMjZiZjg0NCIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiODQ2NTQ3N2VkNjEyOTdmNGI4NTAyYmEzNmU4YzM3Nzg1NGFlM2Q4YzgzMDA1YmExOTI1ZDA2Y2Y3NTBkNWU4ZiIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiNzliYmEwYTMwZjdkZGRiODY5Njk1NTQ4NTc1OWE1ODg3M2YwMjM1NmVlNzQ2YjdhMjFjZmY1YmZjODhlODIyOSIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiNzdlNGFiODViNjRhOWM4YTlkYThkNWI5Yzk0YjViZjgzZTRlZjhlNWNkNWMxNjY1MDhkODYzYTJlYTI1ZDVkMiJ9"
+        },
+        {
+          "resource": "repo://src/web.rs#L347-L364",
+          "version": "repo-lines-v1:sha256:ae5215de4d7affbc92b010adc8ddd1dfad664b5e5f3aeb27dba89330c0f5036d:eyJzZWxlY3RlZExpbmVDb3VudCI6MTgsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjEwODk2MWUyZDgwZWE0ZWQxNThjMDExZjkyYzQ4YjEyYWQ2ZjVhM2FmMjdlMjkwNDA5MDkyMTIxYTVhOGVlOWIiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImEyMzhhMjNmMGFkM2ZkODI4NjY4NzhlZTcwZmI0ZmQwYWVlODlmODFiMDJmZTFmYmQ0YjVmNDAyNzI2MzBiMTkiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjdiYzk4ZDZiMGRjNDkyYzUwMTZkZjEwODA1YTNhNTRlYjljMTg0YTVhNjEzN2U3ZjRiZWNmYTBjMjIwNTUwZDAifQ"
+        }
+      ]
+    },
+    {
+      "id": "claim_6fa3690efd134dd3822d716fed56c8bb",
+      "statement": "API requests are forwarded to App as AppEvent::Web with a oneshot reply channel, and the web thread blocks until App returns the WebState snapshot; a closed channel or dropped reply yields 503.",
+      "evidence": [
+        {
+          "resource": "repo://src/app.rs#L796-L812",
+          "version": "repo-lines-v1:sha256:6905da13ba37beaf073ac6f3f6eb07a12c4b6a34bcf57e4beca54f40fe6b64b0:eyJzZWxlY3RlZExpbmVDb3VudCI6MTcsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjA3YTEzOTMxODdkZWNmZmM0NzMzYzYwY2FiYjA2MTkwYjdkYjAxMjJjYWEyYWM3YjRjNGUwNzBiNzIxNTk1NWQiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjM2MGUyOWExOGZkNDE5NDVjMjM3NTgxMzM4ZGI3NWFjOTIzYzYxYjVmYmFiOTNlOTM3ODdlNWJmMGMyMjk4OTciLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjQ0NmM2OGRiNzFjZDkwMmMyZDMxY2QyYjQ5Y2MyM2ZjYzFmNjVlYWRlY2JlZGRmNDFhOWEwZjVhNGIxMzE1MmQifQ"
+        },
+        {
+          "resource": "repo://src/web.rs#L114-L121",
+          "version": "repo-lines-v1:sha256:9dff2fd116ff7266dafbe9bcec01fa24bb6ab864bc1c054f5a6fcb6449d60e7d:eyJzZWxlY3RlZExpbmVDb3VudCI6OCwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiMDgxZDE0MGYzZjljNjA2NWEyZmE5ZjY1ZTQwMmNjNjA0ZThkYmJhYmZlODc0YjAwODI5ZmNkNDhjZjNhY2ExNSIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDEyY2EzNDVjY2Y3NWJmOWMwODA2YmNlNjk1YmU4ZGU4MDhiNzk5ODQyNTFhN2E1NGQyMDJjZjYxMDFkZDQ1MSIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiODZlMGVhYzgyMjFhNzg0OGFhZTUyYTNjNWMwMjM3YzFjMzAyMmU1MzU0NTU2NzgzOGJkMDMyYzhhNWJiYTQzZCIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiZTY5YTZmM2ExYjQ0MmYyNDllOTIzNWEzMTUxOWNkM2VkZTFlMDAxZWMzYzgxZmJhMWJiM2FlMTVhMjIwYjhmZSJ9"
+        },
+        {
+          "resource": "repo://src/web.rs#L248-L261",
+          "version": "repo-lines-v1:sha256:e50fde998c7b07b6e3110582bc1a3d6bad6d7a63d11a8ce8afdbe7deee340a4d:eyJzZWxlY3RlZExpbmVDb3VudCI6MTQsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjA3NTc5MmI3MWNhYTk3ZDhhNGNiMzI2NGVkYTgxMGE2ZTkxMmY1NTFlY2JkNzAzMTQxMTg3ZmQ0MTZjMjM4NzQiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImYyYjNmMGVkYWNhNjE2MDgyMThmODc3ZmEzM2NiNzJjZTc5NzMyZWJhMDc0ZjBkNTUxNTc3NWY4OWUwZTAwY2QiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjgxY2RjMWUzZDg3ZmIzMTllNzNjMDBiNTc4YzJiMDNjY2EyNGUyOTM3NTcxYzNhYTczMTI4NDhjNGZmNTFiMjIifQ"
+        }
+      ]
+    },
+    {
+      "id": "claim_92efe37fdfa340458d352cfd0c9d653f",
+      "statement": "The quit route writes its 200 response before sending WebCommand::Quit without waiting for the reply.",
+      "evidence": [
+        {
+          "resource": "repo://src/web.rs#L229-L246",
+          "version": "repo-lines-v1:sha256:155b300223ddf57fcf0446c63d0fe37474bd4a7d5c77f97cb07f527646eac78d:eyJzZWxlY3RlZExpbmVDb3VudCI6MTgsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImE0YWQxNDUzZDYzOTBhMjY0YWY3NTcxNTJlZmI2ZDdhMDM1YWQ4YmQ4MTE0ZWZkMWRhZDI4MzAzZDM4NGViNmIiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjE5ZTc2MzU1Y2YxMTk5NDgwYzNmOTk4MzZiMTQ4NmM4NDFkODkxZmFhZDkwNGM2MDhlMDkzMDIyZDM3NTdhZWUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6Ijk2YTMwMjE3MjllMmY0MzA2ZTIwOTY5YTU0YWM4YjFmMTA1MzIwMmM4Mzk4NWM5NzA4OTU3NmM2NzQ4YzE1MjAifQ"
+        }
+      ]
+    },
+    {
+      "id": "claim_4ce4cb65f51445c9b7ad852328ccfa6f",
+      "statement": "The browser page polls /api/state every 5 seconds while visible, sends the X-Wh-Request header on every POST and orders responses with a ticket counter so stale polls do not overwrite newer toggle results.",
+      "evidence": [
+        {
+          "resource": "repo://src/web_page.html#L330-L339",
+          "version": "repo-lines-v1:sha256:c9aeb2e41ffe90a632e19e8ce8ae720ca511a700fb78ede63aef855669e7593e:eyJzZWxlY3RlZExpbmVDb3VudCI6MTAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjlhNjA4YjllNWZhNTllMjI0MmFmODdlODAzOTJmMDI1ZmJkNDgxZGFiZGE2YjVjMmZiOTI4N2YyMjk5NzFhNTgiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjFmYTJjMTFkNTdhZDg3YjUxODk3NWM0MjEyNmFkZWNjZjI2ZDgwY2NlNjhmM2E1YzEwNDkxNzViNDIxODQzZTIiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjliODljNTU5ZmFlMWEzMmMwNjAxNTg0M2VlNmFhMzFlN2Q2NTNiY2VkNmMyNTZjOGRhZTE5ZWFmMDY5ODBiZmMiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImIyNjg3MzIzNmVkNThhZGQ4N2E4NWJiMzQ0M2NiNGZiOTY0NmIzOTcyMjcwMzAzZTIyOTM5ZDE0MTI1YWYyMTAifQ"
+        },
+        {
+          "resource": "repo://src/web_page.html#L373-L382",
+          "version": "repo-lines-v1:sha256:7b6d2b04110398692241256da2fc7b0be08d5ec5f56a928bacf625c9b8bfcb56:eyJzZWxlY3RlZExpbmVDb3VudCI6MTAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImVhZjIyY2YwNzMyNTkyYzI0NzE0MWNiZTYxZWZhMTEwMTI2MTJmNWEzMTIyYjlhZjliYmFmNTJmNTcyNGQxOTQiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImZiNTgwZTk2NGY4M2E0NzNmNDNkNDQzODQ2Mjk0OWQ2OGI1NjAwZmRhNzg3ZjE2Mzc0NWQwMGRlODljYmYzNmEiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImNlNDhiMTQzZTUxMjVjNzBhMjUyNTdmMTY2MzI2MDE1ZTBhYzdiYTBmMzlkOTY1ZTZjMmY1YTczZjlkZDVmYTkifQ"
+        },
+        {
+          "resource": "repo://src/web_page.html#L534-L577",
+          "version": "repo-lines-v1:sha256:f288c654dec0232c30b73b2211301190731dcaa22175fa8f43cdc6415fa9fd0a:eyJzZWxlY3RlZExpbmVDb3VudCI6NDQsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImVhZjIyY2YwNzMyNTkyYzI0NzE0MWNiZTYxZWZhMTEwMTI2MTJmNWEzMTIyYjlhZjliYmFmNTJmNTcyNGQxOTQiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImUwNTY3M2NkNWYxZWYwNGEyMTJhOTRmZjljNWU4ZThmZjg2OTI1MzRiMjU5ZGVkYTYyMTNjYjhlMGE5OWRlZGYiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjhjMjA2ZGQ2YzMwMmIxNGFiOTBhYzI0NjM5YzgzNGExNDhlMGMxMDZmMDAyMWRjZjYxZGMyODVhNjk1OThhZGQifQ"
+        },
+        {
+          "resource": "repo://src/web_page.html#L612-L635",
+          "version": "repo-lines-v1:sha256:54837e3c49fb90fcadbee23744b947ae4f8ab37b1f75712831b41a54ad5bc7b0:eyJzZWxlY3RlZExpbmVDb3VudCI6MjQsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImVhZjIyY2YwNzMyNTkyYzI0NzE0MWNiZTYxZWZhMTEwMTI2MTJmNWEzMTIyYjlhZjliYmFmNTJmNTcyNGQxOTQiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImZiNTgwZTk2NGY4M2E0NzNmNDNkNDQzODQ2Mjk0OWQ2OGI1NjAwZmRhNzg3ZjE2Mzc0NWQwMGRlODljYmYzNmEiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6Ijc0YzRlNDU1MWQ1NTYyYjdkNzM1OThiMTYzOTIyNmEyOTE2MDJmYmRjZDE4OWM0NTliYThiMmQ3OTkwYjIzOGIifQ"
+        }
+      ]
+    }
+  ],
+  "verification": {
+    "by": "openwiki/0.7.2",
+    "at": "2026-10-20T20:27:06.515Z"
+  }
+}

+ 28 - 10
openwiki/.claims/integrations/whatsapp-client.json

@@ -1,6 +1,6 @@
 {
   "schemaVersion": 1,
-  "pageVersion": "sha256:eb9c685422f5830657b3b8f06d76614ab7cbf6d4115f92f597ebaf608fa5b7f6",
+  "pageVersion": "sha256:2d56656fd165a43e6f66b741865ee0cc8e711720783c722f71a531e3b8ec0c37",
   "claims": [
     {
       "id": "claim_5cd95bce2100497cbe7ac9e4c2cb1856",
@@ -51,8 +51,8 @@
       "statement": "Translator::handle_event maps raw events to WaEvent, including OfflineSyncCompleted, and turns failures into descriptive Error texts.",
       "evidence": [
         {
-          "resource": "repo://src/whatsapp.rs#L416-L456",
-          "version": "repo-lines-v1:sha256:a5861cc82f084f74ab17cf35a44ef1128b74811e84bca18be49a2addbf244084:eyJzZWxlY3RlZExpbmVDb3VudCI6NDEsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjdmNWQyMzJiN2E2YTQ3NzhhZWE4NGU5YTIzNDhjMWU3NDdlZTdjOWY2NzUxNThjNWEwMGMxNDIzMTBlYTAxNTciLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6Ijg2ZDM0ZTcxMTQyZjZhMTJjYmM1NzA1N2QxMTc0ZGE2OGM3MjBkMmRlNGY1YmRjZDMxN2FhMjVjZDljYTE0NDEiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImM2ZTYxOTdhNjY3YmI1ZjgyYzI4ZDdmYWM2OGIzYWE1YzU1YTk5NmQ2NGRkYmYyOTI3ZWQ3YjAyNzk5MDI0NWYifQ"
+          "resource": "repo://src/whatsapp.rs#L416-L462",
+          "version": "repo-lines-v1:sha256:2e6abdf7a1ad4472d0ad4892b83fcfcab8abaa1e22f9809e3ff9cc172d6d6669:eyJzZWxlY3RlZExpbmVDb3VudCI6NDcsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjdmNWQyMzJiN2E2YTQ3NzhhZWE4NGU5YTIzNDhjMWU3NDdlZTdjOWY2NzUxNThjNWEwMGMxNDIzMTBlYTAxNTciLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6Ijg2ZDM0ZTcxMTQyZjZhMTJjYmM1NzA1N2QxMTc0ZGE2OGM3MjBkMmRlNGY1YmRjZDMxN2FhMjVjZDljYTE0NDEiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImM2ZTYxOTdhNjY3YmI1ZjgyYzI4ZDdmYWM2OGIzYWE1YzU1YTk5NmQ2NGRkYmYyOTI3ZWQ3YjAyNzk5MDI0NWYifQ"
         }
       ]
     },
@@ -71,7 +71,7 @@
       "statement": "handle_live_message skips status broadcasts and silent messages, canonicalizes 1:1 chat JIDs, notes push names, attaches an ImageRef only for image texts and fills addressed_to from addressed_jids.",
       "evidence": [
         {
-          "resource": "repo://src/whatsapp.rs#L472-L519",
+          "resource": "repo://src/whatsapp.rs#L478-L525",
           "version": "repo-lines-v1:sha256:facf13f54250ccf6043e3f41547bfa0285bbb01882144bb37904d51a76ef7d82:eyJzZWxlY3RlZExpbmVDb3VudCI6NDgsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImFkMmYxYjJjNzYwNGI3NDk3NmI5YmMyNjRkMDBlYjJlODJlZWM0MTk5NzE4NzJjMWFjOTE2OTJhNDQ1ZjMxYmYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjdkZTRkYTlhNTUwNmQ1MmI3NWUwOTY1YjY0ZWI2Zjk5ZDhjMWE4MmZjMWU1ZjE1YTZhMjY0MDhlYjc2MmJlOTUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjY2NDFiNWZlM2M5YmRjNTg2MGQzNmM1MzMzZjMyYTY2OGU4OWUzNWJjZTBiNjU2ZTViYjY2MjVhNjFhNTE3ZWQifQ"
         }
       ]
@@ -95,11 +95,11 @@
       "statement": "History sync chunks are decoded in spawn_blocking and emit push names, inline contacts, conversation names and messages without image references and with empty addressed_to.",
       "evidence": [
         {
-          "resource": "repo://src/whatsapp.rs#L553-L646",
+          "resource": "repo://src/whatsapp.rs#L559-L652",
           "version": "repo-lines-v1:sha256:70e7c7d582124297e079899f1a4cba88e33cab1397f009bfda85e367709c2754:eyJzZWxlY3RlZExpbmVDb3VudCI6OTQsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjAzMDUzNzcyNjU4MGYxZDA2OWQxMjUwNTI3MmRhODg5OWM0M2RiYmZiZmM4NmZiY2IzOGJhMmNhZWE0OWU4NDIiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjgwMmNlYTE4N2JjY2NlOWZhNDkyM2M4NzAzZDliYjk3ZWZhZTZkNTk2YTc4OTQ2YTlhZWMzMzc4MDM3YjU2MWMiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6Ijk5NzM0ZmViMmI1ZGU2NjEwOTU2ZjI3N2RjMzAyMWJjMTZhNDVhYzBhMjc2MWM1Y2YwOGM3M2NiZjA0NTU0MWYifQ"
         },
         {
-          "resource": "repo://src/whatsapp.rs#L703-L744",
+          "resource": "repo://src/whatsapp.rs#L709-L750",
           "version": "repo-lines-v1:sha256:8c2114ae474695a3523c19bf0ceac407e82c82d1882bd2d0511a38cab8eeaaa0:eyJzZWxlY3RlZExpbmVDb3VudCI6NDIsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImY1N2FhZjc1ZDg1ZGRhNjUwNDRiYzA5YmE2YjgxN2ExODFlMDNkZmIxNWY5MzIwYWIzNGZmYjkzOTc2MzljZGEiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjliYjA3YjBjZTBkNjhiMjljNDMxZWQyZjUyODcxN2Y0MDY3ZTA2NzZlNWViZGIzNzQ2N2Y5ZWZjNTM1YmI4YTMiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImE4ZDQ3NzIyZjlhZjNiZmQ2OGYwMzRmYTIzMTgwNGJjMTViOGUxNGIwMzY3Y2FmYmIzZDc2YjI5YjU3ZDQ2YTQifQ"
         }
       ]
@@ -109,7 +109,7 @@
       "statement": "describe_message renders text, image and audio placeholders, captions, and silent or generic placeholders for other message kinds.",
       "evidence": [
         {
-          "resource": "repo://src/whatsapp.rs#L826-L884",
+          "resource": "repo://src/whatsapp.rs#L841-L899",
           "version": "repo-lines-v1:sha256:2b1c51b48fce477ab09efb54b0e727a20b91bb212e43c005ffb0ba48ba3f3c2b:eyJzZWxlY3RlZExpbmVDb3VudCI6NTksImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjNjNjE2NzkwYTVhOWYyMTUyNGQyMTkzMGYwODhiYWNiYjkxZmYwNGQ1YTcxZTQzOTljNjEyMjdiNTVlNmRiODAiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjkwNzA2MWVjY2M3ZmRhZTJiN2I0MjlhYTAzYTZkMTNmMjNjNTY5NzZmMmRmOWU5Y2UyY2ZhNjM4ZjdlMTQ1ZDMiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjcwNzFhZGQ3YWQ1Mzg3NDYwZWJmZWY1YTkyOGQyMTAxZGJkMGQ2ZTU2YzRhMzliNzA0NDViMWM3OTU3NmZlNmEifQ"
         }
       ]
@@ -133,7 +133,7 @@
           "version": "repo-lines-v1:sha256:7ea1e000732e0183f2e0d6671934cf8532875e711139eacf688d202ddfc274f2:eyJzZWxlY3RlZExpbmVDb3VudCI6NywiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiY2ZkNDJkOTZmZjZjNWQ3ZDg2YmZjMzI1MDQ4MzYwZWEwMDE4N2VkNzhlNTM1M2Q0NWY0ZGI2NmZiNDBiYjk2NyIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDg5M2Y0MGUzNzgxZWI4OTZjODJkMDQ2YTA4MDY3YTExZTM1Y2RmMmYxZGZhZjI2ZmVhOTBiOGRiNjY3NjQxMyIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiOWY1NTZkYTZjNjZkNTE3ZmZkMmYzMzkyOTY0M2Q4Y2UwMTg5YzNmNTJmNDJhYjc3ZTExNDQ3NGYxNzQ0YzZiMyIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiMWJjZmRiYmE1MDBmYjJlYTIxOWFlN2I0NDQ2ZTNlNTU1MDhhZWIxZDdmN2NiYzgwZjE1NTI2MzMzOTlkZjUxYyJ9"
         },
         {
-          "resource": "repo://src/whatsapp.rs#L815-L817",
+          "resource": "repo://src/whatsapp.rs#L830-L832",
           "version": "repo-lines-v1:sha256:2dff1120b156dde5cd502a03211b000f533177e7214711f8a8a081955c3663ac:eyJzZWxlY3RlZExpbmVDb3VudCI6MywiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiOWY1MGVlZDdhYWNmZGE2Mjk4Mzg4Yzg2MmI1ZjIyNDNlNzIyYWMxNWEyY2FkNzAwNWUyOWQ1NzIxMmE1YThiOSIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDEyY2EzNDVjY2Y3NWJmOWMwODA2YmNlNjk1YmU4ZGU4MDhiNzk5ODQyNTFhN2E1NGQyMDJjZjYxMDFkZDQ1MSIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiNGNjZjc5MzkxNTY5MjhjYzk4Y2ViZDIwYjJlYTA2OWM0ZWNlZGJlZWE5Nzk2MjBiN2IzNWUzYTY5Mjg3YWE4MyIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiM2I4MWNkZTZlMzVlZWI1MjRlMzM5ZDFlMjM1MzE5Y2Y1NTI5YjFhNTBhMmE2MDUyZmUzNWI1ZDQ4NWRkOWUxMyJ9"
         }
       ]
@@ -143,14 +143,32 @@
       "statement": "addressed_jids returns the mentioned JIDs from the first ContextInfo-carrying content kind, followed by the quoted message participant only when the message actually quotes one.",
       "evidence": [
         {
-          "resource": "repo://src/whatsapp.rs#L892-L919",
+          "resource": "repo://src/whatsapp.rs#L907-L934",
           "version": "repo-lines-v1:sha256:c4610a9307854ce51886400a8ff3f7a9ad2dc3e46bb9b921d235ed4860a24258:eyJzZWxlY3RlZExpbmVDb3VudCI6MjgsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImVlOTFkNTE1NmE1ODljNjkwOTQ5NjU5ZjdiMjRiMGJhYzkxODgyYWI1OWU5NzAyMGM5YTkyOGVhZWY3MjQ0NjMiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjhlZWExMmViMzJiZjExNjcwNTQ2NTFmMzk2ZTU3NmE0OGJmMzJhOGM5OTEzY2M2Njc5NmMwNDY1ODg4YTc4NDUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjAxNzFhZTQ1OWIzOWMzYTUyNGFhNjkyYjEwMzRhMGEyMDRiODA4ZjE2MjBkYzM1ZjM4OWY5MzViYjk1ZDcxYzMifQ"
         }
       ]
+    },
+    {
+      "id": "claim_83eac680b18f4f47ad42862e119e2161",
+      "statement": "An AppStateSyncFailed event is reported as an Error only when at least one collection failed fatally; retryable or skipped collections are not shown.",
+      "evidence": [
+        {
+          "resource": "repo://src/whatsapp.rs#L1045-L1048",
+          "version": "repo-lines-v1:sha256:af6676593e9e90c7ac6a4b2b64625d687ad77fa0265e7f46bb6b8dbef4446d86:eyJzZWxlY3RlZExpbmVDb3VudCI6NCwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiMzFiZTM3ZmI4YTA3YjI0YzY1ZmVjNGQ3MGI2NWNhNDAzNTU5ODQwOGE3NDhhYjg2YWY4YTY4YWJlMGJjMTNhYSIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDg5M2Y0MGUzNzgxZWI4OTZjODJkMDQ2YTA4MDY3YTExZTM1Y2RmMmYxZGZhZjI2ZmVhOTBiOGRiNjY3NjQxMyIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiYTAwZGIwNDBmMjkwOWJmOTc4ZmYwYTk1NmE1NWU4M2M2NTM0MThkZTZiYjY4ZjgwNDFkYTk3NmY4ZDNkMjI3MCIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiYjQ1MTJjNzhmN2UyZTJiODdhNDZhYThlZWUxYTg3YjIwZjZjMWE0Y2MwZmNkNDQ4ZDhmZjJmMWJlOGYxMGQ4YSJ9"
+        },
+        {
+          "resource": "repo://src/whatsapp.rs#L440-L449",
+          "version": "repo-lines-v1:sha256:55006adc8131456b9811de9f46520c07874aab146e6d5deb0b359a138bc063e8:eyJzZWxlY3RlZExpbmVDb3VudCI6MTAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjljNjAwZjllYTA2YzVjZTAxYjEzODExOGM5YjdhZDVjNTUxYzBhMmNjZDkxZTJmNWQxMWI4OTNiMWU5YzdjN2IiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjU1MjJjNDJjZDE5YzZkMDNkODA4YzQ1MjcyMDU1MjgyNWI3ZWExYzViNDkxYjIxODUxMWNhYzc0MTVjZjlkOWIiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjlhNDQ2MGQwYzUyNjkwYzM3YTQyOTI4MTc3NjkzNmQ4ZDA3YTJhYjg3MDEyYTRkMDY0MWNiMTQ4ZjkzOThjOGUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjQ3YTNlYmViMDcyOTcwMjY0MTFmZWVhZWViOTNkODBhMDZjOGQzNmE0OWE0NzgyNjhiZDFjNjJkMWRkOTk2YTkifQ"
+        },
+        {
+          "resource": "repo://src/whatsapp.rs#L808-L815",
+          "version": "repo-lines-v1:sha256:e6e70ed71f30e7f2d2201053fec0c50f7a478e2081980f40b73ee55fd11ccbfe:eyJzZWxlY3RlZExpbmVDb3VudCI6OCwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiN2ZmYzA3OTdhYmQ1NjAwYmU5MWY4MjMyNWZmZjhlMjllMDVmNDY3MGZiOTQ5YzdmODZkZTRlZmJhOTQ5ZmIyOSIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDEyY2EzNDVjY2Y3NWJmOWMwODA2YmNlNjk1YmU4ZGU4MDhiNzk5ODQyNTFhN2E1NGQyMDJjZjYxMDFkZDQ1MSIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiMTllNzYzNTVjZjExOTk0ODBjM2Y5OTgzNmIxNDg2Yzg0MWQ4OTFmYWFkOTA0YzYwOGUwOTMwMjJkMzc1N2FlZSIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiYzYxNmY0NTJmYTU5OWI0YjM5MWIwMmVjYjI1OTNjMzk3ODVhMDFjZDZiZTIwMmM0OTQxMjk2NGViYWQ3YzFmYyJ9"
+        }
+      ]
     }
   ],
   "verification": {
     "by": "openwiki/0.7.2",
-    "at": "2026-10-19T19:53:56.480Z"
+    "at": "2026-10-20T20:27:06.515Z"
   }
 }

+ 58 - 30
openwiki/.claims/operations/configuration-and-data.json

@@ -1,18 +1,18 @@
 {
   "schemaVersion": 1,
-  "pageVersion": "sha256:3063070e9f1cf52ceace2c864124dbf09583146e1039a9b795a9d7d8127517a6",
+  "pageVersion": "sha256:f973194dff48fa997deb016f452d6f1515458eef3b52aa111bc52439dafc1b81",
   "claims": [
     {
       "id": "claim_0519d797500c4fba86d6c64ac1e60df4",
       "statement": "Configuration is read only from config.toml and no environment variables are used.",
       "evidence": [
         {
-          "resource": "repo://src/config.rs#L1-L20",
-          "version": "repo-lines-v1:sha256:5029434b6f0ff10e22e3e03b21a6365b0796c5eef70104c83584b796c04583ac:eyJzZWxlY3RlZExpbmVDb3VudCI6MjAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImI0MGE0NTBjZTI3ZmY3MjYwYzM4M2U4NDk4MjhjNzY1NTVlZTk1ZTllM2JmMTJmOWYxY2IxY2Q2OGZlZGFkMDgiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjZmNzkwMmU2MjBjYmU0ZDIwMGU3NTVjYmY5YTMyOWYxMzFkYjFhM2UxZDQ2Mzg1MWU1YjFiOTE4NmM2YTVmYzUiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjowLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImUzYjBjNDQyOThmYzFjMTQ5YWZiZjRjODk5NmZiOTI0MjdhZTQxZTQ2NDliOTM0Y2E0OTU5OTFiNzg1MmI4NTUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjcxMzE1MjZlMmQ3ZWFkMDgwNmJkNzJlYjRjMGJlMjE1NjI4ZWZiMGMzMjc3OTM0NjVkYjM1NmQ2NTQ4NDAzN2YifQ"
+          "resource": "repo://src/config.rs#L1-L25",
+          "version": "repo-lines-v1:sha256:9e58106875f518f2506700924e5e96bab155046a8df51e59257dc94cac50930e:eyJzZWxlY3RlZExpbmVDb3VudCI6MjUsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImI0MGE0NTBjZTI3ZmY3MjYwYzM4M2U4NDk4MjhjNzY1NTVlZTk1ZTllM2JmMTJmOWYxY2IxY2Q2OGZlZGFkMDgiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjZmNzkwMmU2MjBjYmU0ZDIwMGU3NTVjYmY5YTMyOWYxMzFkYjFhM2UxZDQ2Mzg1MWU1YjFiOTE4NmM2YTVmYzUiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjowLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImUzYjBjNDQyOThmYzFjMTQ5YWZiZjRjODk5NmZiOTI0MjdhZTQxZTQ2NDliOTM0Y2E0OTU5OTFiNzg1MmI4NTUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjcxMzE1MjZlMmQ3ZWFkMDgwNmJkNzJlYjRjMGJlMjE1NjI4ZWZiMGMzMjc3OTM0NjVkYjM1NmQ2NTQ4NDAzN2YifQ"
         },
         {
-          "resource": "repo://src/config.rs#L226-L230",
-          "version": "repo-lines-v1:sha256:00d6f1fbf5d4c61080dee1e340448add384f342d5743d2721df28ea0644986d6:eyJzZWxlY3RlZExpbmVDb3VudCI6NSwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiMzI1NDU2YzAyNzFhNTg0ZjQ1NDczNzhiNDZlNWNhM2VlY2E5OTM3MmUzZjFjZjc5YjJmYmE3NTkxZDU4ZTE1YyIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDg5M2Y0MGUzNzgxZWI4OTZjODJkMDQ2YTA4MDY3YTExZTM1Y2RmMmYxZGZhZjI2ZmVhOTBiOGRiNjY3NjQxMyIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiNTRhNWZlMjlkYmU2NjQ4ZmQ1YzMwM2MwZTc0NzI5ZmI4NWQ1NjYzYjE5OTRlMDhjYjU1M2Y3ODA1Yzk2OGRlOCIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiZTZjOTEzMmNlNmFmZjhmYmMyYWYzNGJjYmZhYTU4NWE3ZGYxZTE4NWJiNzk5MWVjYjljODFhNTE4ODc1ZTI4ZSJ9"
+          "resource": "repo://src/config.rs#L254-L263",
+          "version": "repo-lines-v1:sha256:765917d933b8312e24ca9f330a3ebaad4a414c773af0d2ae857bbd7e091afe34:eyJzZWxlY3RlZExpbmVDb3VudCI6MTAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImQ4MDc5NGE2MDA2MzNhZTY5ZGFlMWYzOThjYTk3ZTc5Yzg3NDc5ZDFjNjE3MDg0ZTRkMTgyODZmMTlkODFjMTUiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjgzZDI4NDFkZmUxZTBkZDIxMmQwYzE1ZDAxZTVhYzkyMTBmNTgwMGI4YmRmZTk5M2FkZDAwYmQ5NGYwMTRkNmMiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImU2YzkxMzJjZTZhZmY4ZmJjMmFmMzRiY2JmYWE1ODVhN2RmMWUxODViYjc5OTFlY2I5YzgxYTUxODg3NWUyOGUifQ"
         }
       ]
     },
@@ -21,11 +21,11 @@
       "statement": "The data directory is the platform config directory joined with wh and is created at startup by ensure_data_dir.",
       "evidence": [
         {
-          "resource": "repo://src/config.rs#L144-L148",
+          "resource": "repo://src/config.rs#L166-L170",
           "version": "repo-lines-v1:sha256:dd4141940976146e96656f2de294f92e1275a16efba97aa2c7318676f34bdfa4:eyJzZWxlY3RlZExpbmVDb3VudCI6NSwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiMzVhMDYyMDkxMmIzMTUwZDZlYmQwNDQyOTUxZTdjNWEwODlmODMwMzYzNmJjODZiNmQxOWRmN2U3YTFmMWEzZCIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDEyY2EzNDVjY2Y3NWJmOWMwODA2YmNlNjk1YmU4ZGU4MDhiNzk5ODQyNTFhN2E1NGQyMDJjZjYxMDFkZDQ1MSIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiNGU1ZGI4YzkwMjczZTAyOTc3MTY3ZWMwNDM0N2M0MWQ5MmE1MTE4NDMxMTA0M2UyMTU2YmM4MGFiYjkwMWQ0NyIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiMDY4YzgxNzM5NzQwNTE2YzI4OWQ4ZWRlZWM0N2IwMjcxMGFmOWVmNWM4MjE4M2NhNWQwMTc4MWVhMjI1YjU1MCJ9"
         },
         {
-          "resource": "repo://src/config.rs#L258-L261",
+          "resource": "repo://src/config.rs#L293-L296",
           "version": "repo-lines-v1:sha256:70ea48c62bb6583f73eaa3d204fb92456b268569dccd85ba7c47f85f0175339f:eyJzZWxlY3RlZExpbmVDb3VudCI6NCwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiMDE1Zjk2ZjM2OWY4YjE1NjExZWVjMDljZTBjOThiYjNhZTNhM2U5Y2U2MDEzZGNmMGE3OWY4ZTM1MjVmMjMxNyIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDg5M2Y0MGUzNzgxZWI4OTZjODJkMDQ2YTA4MDY3YTExZTM1Y2RmMmYxZGZhZjI2ZmVhOTBiOGRiNjY3NjQxMyIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiM2UxNTlhZDA5YmYzMzllMTM2ZmY0NzVjMjc4NDNkMTYyY2JiMTI2ZGMxMmFiMDM3MjE3M2M2ODY4ZGM5NzUwMyIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiMTZjMzk0MWE5YjYxMDI1Yjg1ZWU5ZWEzNzYzOTUyYTViMTM5NmNkMmY2ZjczMTkyZGQ3NDYwNDc5OTlhYjY0MSJ9"
         }
       ]
@@ -35,30 +35,30 @@
       "statement": "session.db, history.db and the optional ai_requests.log live in the data directory next to config.toml.",
       "evidence": [
         {
-          "resource": "repo://src/config.rs#L263-L285",
+          "resource": "repo://src/config.rs#L298-L320",
           "version": "repo-lines-v1:sha256:f895d312efc12c901e18e3c47999e82cde54de595ed326f94dc9c80be206fd75:eyJzZWxlY3RlZExpbmVDb3VudCI6MjMsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImIxNjY4MjQ0NGNiOTU1NTQxNjJjOGIzNjYxMjg0YTJhNGMzMDIwZmQxODU3OTYxMDU2ZGE1MjMxM2Y2YjgzYjkiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjIxNTA5ZGYyZDNjMWJjODQxODQ2NDlhOTg3NzAwZTU4ZGVmNTJkZGQ1YzBkZTVlZWIwZjY0YTc1YmQ5MzBlYzEiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImE1MjE2MDRiMzVmYzQwYWYwMGNiYTRlZWEyZmQ1ZTJjM2FmMTg2M2IzOTExODVkN2JmNGJiMDllNzJkMGM4YjMifQ"
         },
         {
-          "resource": "repo://src/config.rs#L40-L49",
+          "resource": "repo://src/config.rs#L47-L56",
           "version": "repo-lines-v1:sha256:6624d58d13d3982b8829e7b759fc7c36a643278f2d243526a887428e34a0d469:eyJzZWxlY3RlZExpbmVDb3VudCI6MTAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImU1MDllZGUxYTUxZmE5MGM4ZmQ0YTNkODdmYjJhMGU5OTMwNzhkMjc0MzU1NGFjMGM4MmIzMTdmNzQ0OTY5ZmYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjFmNzZjMzcwNTRkNWY0YzFhNWZhZDE4YzFlYjI0NTNmODIxNTI0NDQ2M2ZkNjA4MzIwZDEzZDYxNmFlYmVhNmQiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjMwY2VjY2EzYjg3NDgwNDU3ZGVmZTlkOWVkY2I1N2VmMDdlN2ZlZTE2MTljZjdmYWEyNzM0Y2Y2M2Q5MzIxYjEiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjQ1N2RlMDJkYWE2M2RmYzAxZmQxNjkyNDc1ODY2ODA3OGM5NmJlMGM1ODkyZjQ4M2UzMDdmNjAyYTU0MDRkMzQifQ"
         }
       ]
     },
     {
       "id": "claim_4a0091b8b0c944f9916463712878acbe",
-      "statement": "Keys openai_api_key, openai_base_url, openai_model, openai_vision_model, system_prompt, ai_log, cooldown_seconds, group_cooldown_seconds and catch_up_seconds map to Config with defaults (base URL, model, ai_log false, cooldowns 30 and 300 seconds, catch-up 7200 seconds); system_prompt is not trimmed.",
+      "statement": "Keys openai_api_key, openai_base_url, openai_model, openai_vision_model, system_prompt, ai_log, cooldown_seconds, group_cooldown_seconds, catch_up_seconds, ai_retries and web_port map to Config with defaults (base URL, model, ai_log false, cooldowns 30 and 300 seconds, catch-up 7200 seconds, 2 retries, web panel disabled); system_prompt is not trimmed.",
       "evidence": [
         {
-          "resource": "repo://src/config.rs#L236-L252",
-          "version": "repo-lines-v1:sha256:a243422f2946158325855db5a4be2d0e714e34db5efbec661bff6b5543b6fd30:eyJzZWxlY3RlZExpbmVDb3VudCI6MTcsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImFjZWRhOTg0MDJkODQxZjI4MThkMTg2OTM4NWQ0MzA2OGVmNjkyODg0ZmRiOWMzMzFjOTc4NTY3Y2EwOGQ0NzkiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjkyOTlhMjc0NjgwMDNjZDA3M2I5MTZiYTUwMGYwYTE0OWZiMzBlZDE3M2M2NjFkNmE1Y2M2NjJjZTk0YzRlNzciLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6Ijg4ZWYzM2E3NjQ3YTM0NDVkNzE1NDU4YmE2Yzc0OGY2NmIyYWZiNzYxYzEyODc4NWU3MzRjZjIyOThjOTdkMzUifQ"
+          "resource": "repo://src/config.rs#L265-L287",
+          "version": "repo-lines-v1:sha256:980417922309d654c96024f5b863cf6ea53769c253161d25ff0d1c4d3a062c88:eyJzZWxlY3RlZExpbmVDb3VudCI6MjMsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6Ijc2NWZhNGU5MTRkN2UzOWE2MGJhZTc1M2NhMjE2YTg3NGE3NmZmZWY4NTc4OTY5NTZkNjg0MDM4YTY1MWJjZDAiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImM5MGRlZTM4YjdkMWZiYjMzNjNiMjY5Y2EyNjc4MTE4YTU2ZGNlZjlhMTU4MmE2N2Y3YjM4ODY2ZDZiYmFlN2UiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6Ijg4ZWYzM2E3NjQ3YTM0NDVkNzE1NDU4YmE2Yzc0OGY2NmIyYWZiNzYxYzEyODc4NWU3MzRjZjIyOThjOTdkMzUifQ"
         },
         {
-          "resource": "repo://src/config.rs#L29-L38",
-          "version": "repo-lines-v1:sha256:1c1f21cc650ea553949a8f8e67afaa313a368a92c0a1de217140dd63e56935c1:eyJzZWxlY3RlZExpbmVDb3VudCI6MTAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjdkNWE4ZjI1MGMwMjJlOTMwNzUxZGFlMTg0YjBhZGMxNTUwYWRkNzVlNGJiOTUyMDBiNTE0NmY0NzQ5MjdlODMiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjlkMDNhZDdjNjg3NGE4NDYyM2ZmYzQzYjhmMjI1MGFjN2FjNTE0NDdiN2FlZmYzMTliN2IxNDJhOTI5ODZlMzMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6Ijc2YTNkY2UwNTMyZWI3NmI2M2EyNmViYzdlZmNkNWE3YTViYzFjMTFhOWFiZGRhM2Y3ZjJmNzhkODc1Y2Y3M2YiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjdlODA1ZjRiMzVlMzg3NmExMjg5OTE1MjgyYWRhNWNkMzNhNzdmNTMyYmYyOWMxOGM4OWRhMjJiMTRjZjUwYmQifQ"
+          "resource": "repo://src/config.rs#L34-L45",
+          "version": "repo-lines-v1:sha256:20a9554333b6e6a9c3dc056ceac92d156d754d88fece885f6f018e8eb85b4f38:eyJzZWxlY3RlZExpbmVDb3VudCI6MTIsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjdkNWE4ZjI1MGMwMjJlOTMwNzUxZGFlMTg0YjBhZGMxNTUwYWRkNzVlNGJiOTUyMDBiNTE0NmY0NzQ5MjdlODMiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6ImQ1MDFkZTlkMjcyZmZkODVmYTk1OWMxN2JiMTdjYWZkMmJkYjM1YTgyMGQzNDA1NGRkOWQ4NThkNzUyMDMyODIiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6Ijc2YTNkY2UwNTMyZWI3NmI2M2EyNmViYzdlZmNkNWE3YTViYzFjMTFhOWFiZGRhM2Y3ZjJmNzhkODc1Y2Y3M2YiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjdlODA1ZjRiMzVlMzg3NmExMjg5OTE1MjgyYWRhNWNkMzNhNzdmNTMyYmYyOWMxOGM4OWRhMjJiMTRjZjUwYmQifQ"
         },
         {
-          "resource": "repo://src/config.rs#L60-L88",
-          "version": "repo-lines-v1:sha256:8608d1dda07d0453aa3eb5f7dcde2a1c037fd686021a636006e4efa0ae2e01d0:eyJzZWxlY3RlZExpbmVDb3VudCI6MjksImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjY2MWQ4OTAwZThkNDcyOWNhMDZlYTYzMjM0ZGI5OTMwMzljZGI5YzRhODA5OGE3ZDQzMmZiNzQ3ZDI5NDUzZDUiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjRlMzMzNjA1NjQyNmQ3MGE1ZDc5YjU3NWEwYTEyNDM5MzljZDE2Nzk2N2I5N2EyMDA3NGVjZGQxZjRiMjE0MDAiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjZlMWQ1YWEyZWYyYTBkODhkY2JlMWViZTI2YTg5OTdkOWVjODhjZGNmMmMzNDA1YzgwZjYwOTFmZmNhZDUzYmQifQ"
+          "resource": "repo://src/config.rs#L69-L103",
+          "version": "repo-lines-v1:sha256:7c6f8495ff37fead6b29d635192080812a646802f43e26c9dbaa5406f2f4c18f:eyJzZWxlY3RlZExpbmVDb3VudCI6MzUsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjY2MWQ4OTAwZThkNDcyOWNhMDZlYTYzMjM0ZGI5OTMwMzljZGI5YzRhODA5OGE3ZDQzMmZiNzQ3ZDI5NDUzZDUiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjRlMzMzNjA1NjQyNmQ3MGE1ZDc5YjU3NWEwYTEyNDM5MzljZDE2Nzk2N2I5N2EyMDA3NGVjZGQxZjRiMjE0MDAiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjZlMWQ1YWEyZWYyYTBkODhkY2JlMWViZTI2YTg5OTdkOWVjODhjZGNmMmMzNDA1YzgwZjYwOTFmZmNhZDUzYmQifQ"
         }
       ]
     },
@@ -67,7 +67,7 @@
       "statement": "A missing config file yields defaults, while unreadable or invalid files produce errors naming the path without echoing content; a BOM is tolerated.",
       "evidence": [
         {
-          "resource": "repo://src/config.rs#L165-L190",
+          "resource": "repo://src/config.rs#L187-L212",
           "version": "repo-lines-v1:sha256:98536507e8260febe788fbadf4507f155d98cae147436b79fb8b0f633fd9ecdf:eyJzZWxlY3RlZExpbmVDb3VudCI6MjYsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImE2NTEzYThiZDE0NzdlYTVhZTdhMWU5N2IzMjFhZWUxNmY1YjcwZmEyZDA3ZTMzMTAyYmNiY2M3MWE1N2JiMTEiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjRiNzAzOGM3MGY5ZDBmYzEzOTYwY2YyOTRkOTdhODRlOTUwMmNhZmI2Yzk1NjM2ODhiZjc4ZGQyNGY0NWE2YWUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjJkM2E4MDAwMjZjNmY2MDFiYmM4MDdlYWI0YmI1Nzk1ZTY4MjEzNGYxMWU3MTRmNzRkMWVlMzQzOTUxNjE4NDYifQ"
         }
       ]
@@ -77,7 +77,7 @@
       "statement": "An openai_base_url made only of slashes is rejected.",
       "evidence": [
         {
-          "resource": "repo://src/config.rs#L212-L219",
+          "resource": "repo://src/config.rs#L234-L241",
           "version": "repo-lines-v1:sha256:416d7587d25100ea2db4d28ac60e8ff2b5b07566b3bbad27544a561576d2d6d4:eyJzZWxlY3RlZExpbmVDb3VudCI6OCwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiZjg0NWUxMjk4NjExOTI3YTI5YjJhNjA0ZmZiMTBhOTcwMjc5ZjIwYjZhMjBiYTA4MGJiNDFmYWU1NGJkYmFmNiIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDEyY2EzNDVjY2Y3NWJmOWMwODA2YmNlNjk1YmU4ZGU4MDhiNzk5ODQyNTFhN2E1NGQyMDJjZjYxMDFkZDQ1MSIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiZmM2ZTJmOGQ3OGU4N2ViYmYxZTQwNWUyMmIxODU5MGViZjMzNzQ3OTY5MGVlNDVlZGIxNjMxZGVlYjJjZDQ0NiIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiZTM0ODM0OGE5MDUyNDI1NDNlZmRlZTgyZjg3NWRlMTk1NzU5Y2Q0MDkyODQ3ZDBlZWIyNzdmYWQ4M2Q3NzUxZiJ9"
         }
       ]
@@ -87,8 +87,8 @@
       "statement": "Config Debug masks the API key and FileConfig has no Debug implementation.",
       "evidence": [
         {
-          "resource": "repo://src/config.rs#L90-L138",
-          "version": "repo-lines-v1:sha256:e33abdd1327656fd1d23851fb659904bbcb139efe2a042374b4af5a47ae17609:eyJzZWxlY3RlZExpbmVDb3VudCI6NDksImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImQwOGE4NDgyODgzYmU5ZjVmMDIxNDY2ZTgyOGE2MjgwZTI5YTNiMjI0ZTQ4MTY4ZjZjMWQxMDY0MzI3YjlhMjYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjY0Zjc0NTFlMTZhYzBlYjU5ZWE3OGI4Nzk0OTdhMjVkNzM4NzUxYjYwZjE1ZmYyYjQ3ODBhM2RmMGYwYTIyNTIiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImFiNzFkMjFhZWI1ZGU3M2Q1YmI3YmU2NDg0YTMzODliNjY1YjdjMDc1YmM4MjlmMDIyYWE0ZDc1MjM2OTNhMTUifQ"
+          "resource": "repo://src/config.rs#L105-L160",
+          "version": "repo-lines-v1:sha256:5e6170855098f89234451aeac62ee28645a6864149176bea3af06a2c5f101537:eyJzZWxlY3RlZExpbmVDb3VudCI6NTYsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImQwOGE4NDgyODgzYmU5ZjVmMDIxNDY2ZTgyOGE2MjgwZTI5YTNiMjI0ZTQ4MTY4ZjZjMWQxMDY0MzI3YjlhMjYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjY0Zjc0NTFlMTZhYzBlYjU5ZWE3OGI4Nzk0OTdhMjVkNzM4NzUxYjYwZjE1ZmYyYjQ3ODBhM2RmMGYwYTIyNTIiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImFiNzFkMjFhZWI1ZGU3M2Q1YmI3YmU2NDg0YTMzODliNjY1YjdjMDc1YmM4MjlmMDIyYWE0ZDc1MjM2OTNhMTUifQ"
         }
       ]
     },
@@ -104,19 +104,19 @@
     },
     {
       "id": "claim_b8d35a34934748b7bba2314b03d93883",
-      "statement": "The cooldown and catch-up keys are Option<u32> in the file so negative values are rejected, and 0 disables the respective cooldown or the catch-up.",
+      "statement": "The cooldown, catch-up and ai_retries keys are Option<u32> in the file so negative values are rejected, and 0 disables the respective cooldown, the catch-up or retrying.",
       "evidence": [
         {
-          "resource": "repo://src/config.rs#L1-L17",
-          "version": "repo-lines-v1:sha256:dc79c794a5e3d2073767fa6cfa13bd4151134d61cb04fcfced77a76e618e78d1:eyJzZWxlY3RlZExpbmVDb3VudCI6MTcsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImI0MGE0NTBjZTI3ZmY3MjYwYzM4M2U4NDk4MjhjNzY1NTVlZTk1ZTllM2JmMTJmOWYxY2IxY2Q2OGZlZGFkMDgiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjU3MDEzOTMyY2E1OGY4ZjcyMTAwMmEyY2JiNzJhNzI3YTM1MzBkYWY0ZGU0NmYwZTE0MmRiMTI5NTIzZDNmOTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjowLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImUzYjBjNDQyOThmYzFjMTQ5YWZiZjRjODk5NmZiOTI0MjdhZTQxZTQ2NDliOTM0Y2E0OTU5OTFiNzg1MmI4NTUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImZmM2E0OTIzMjZjZmI2ZjY2YzdiNGY4NDI0Y2NjYWZjOWM5YWU4YzU4NGU0N2JjOGJiNTQ0ZjI0NGRjOGE3YWEifQ"
+          "resource": "repo://src/config.rs#L121-L128",
+          "version": "repo-lines-v1:sha256:8b75e8e687adfeec46a668e0a24355f29c7e83198e5dd972c8716af7e123f9ae:eyJzZWxlY3RlZExpbmVDb3VudCI6OCwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiYTVlZGFiZTdmOTczYjIwMzkwNGU4MTg1MzlkNzExMTk3ZTQwMTc4N2NkODVhZDAyYzNkNjdkYWM1OTMyNzc3YSIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiYjBiOTRjODBmNjUxYzk0ZTBmZTBmZWE4ZDc2NGUyMWFiMjc3NWFhZTkwZTVkYzczN2NjYWVmMTA1NDhjYTIzMCIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiYjg3MzFkYzc5ZDhiMzFhYzFlODZjMDMyOTQ2NDY0MTJmNjY1YzM2ZGNmZmM0MTQ4ZDVlMGJlOTE5ODdkNGRjNyIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiYjRmOGYzYzg1N2I3MjNlMDgyNWQ1MDA5MzQzOGI5ZmYyMjY2MzAxY2I1NzA3MTk4ZTc2NGZlODI0OTZhYjMwYSJ9"
         },
         {
-          "resource": "repo://src/config.rs#L447-L470",
-          "version": "repo-lines-v1:sha256:11c57a2a9a34d8871e2e9e9bd89958b082add6b857091394767f05f5307ae3d8:eyJzZWxlY3RlZExpbmVDb3VudCI6MjQsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImQzMjE4MGIxYWUzZmFiYTY3NDViZjMwNjlhMWFlODIzNjk2ZDUzNjI2MWNmNmVmM2U4ZjZiNjc1NjM3MDUzNTYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjkyYjVlZGY1OTQ5Y2ZlYTY3ODIyY2Y2OTExMzIzNjNkNjhkNmYxZTE2OWY1ZTBkNGY3YzE0ZTVmYTFlNmQ0ZTUiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImEwMGRiMDQwZjI5MDliZjk3OGZmMGE5NTZhNTVlODNjNjUzNDE4ZGU2YmI2OGY4MDQxZGE5NzZmOGQzZDIyNzAiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImZhY2Q0MzU5ZGJlOTczNTdmZWM4OTM2OTQzZjkzM2Q2YWMwNjE4ZGFiODQ1ZTMwODQxYjdjMGJiZjUwMzI4OGMifQ"
+          "resource": "repo://src/config.rs#L529-L547",
+          "version": "repo-lines-v1:sha256:b7c3b1c4f7fdf5b1a22d7cb0c229948c0d3aa1ae4712f0a8be1bf0100cf8fa85:eyJzZWxlY3RlZExpbmVDb3VudCI6MTksImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImRhOTVhYzZlMmVhOGY3NWFlNDRhZDI3NjY4ZTU1MzkwY2MzZmQ2ZjdjZjY0MmU4ODJjMWM4Nzc1ZjUxMjk2MDMiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjA1YTgyYmM5ZDU1NTVjOGUzY2Q0MTUzOTM2MmQwZWU4OTdkNTExN2U2MTRjZDkzN2JiOTliYWMwOGM0M2YyZWYiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImEwMGRiMDQwZjI5MDliZjk3OGZmMGE5NTZhNTVlODNjNjUzNDE4ZGU2YmI2OGY4MDQxZGE5NzZmOGQzZDIyNzAiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImRlMzMxZTU1MjViMTM5ODQzODA2NzZkN2MzNmNkYTBkMmNjMTgyZmMyYTE2N2QyZTVjOTU0NTE0YmI5ZTY5ZmEifQ"
         },
         {
-          "resource": "repo://src/config.rs#L90-L112",
-          "version": "repo-lines-v1:sha256:b8969ab8e45ac380a71bef0a4aa9ca3ad8888bf8b63837f95629a882b5c5011f:eyJzZWxlY3RlZExpbmVDb3VudCI6MjMsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImQwOGE4NDgyODgzYmU5ZjVmMDIxNDY2ZTgyOGE2MjgwZTI5YTNiMjI0ZTQ4MTY4ZjZjMWQxMDY0MzI3YjlhMjYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjY0Zjc0NTFlMTZhYzBlYjU5ZWE3OGI4Nzk0OTdhMjVkNzM4NzUxYjYwZjE1ZmYyYjQ3ODBhM2RmMGYwYTIyNTIiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjNhN2ZlNzVmMzU5ZTMyZTFkYmE4YWFjMDY1ZmMzYWFhMjY4YzcxZDc0NjUyYjU2OWNkNTJhYjEzYmZlYmMwN2UifQ"
+          "resource": "repo://src/config.rs#L7-L19",
+          "version": "repo-lines-v1:sha256:91944cf8df4c789256e6058f9182688671bf3be2314fcf2ad7893746cf2dc4eb:eyJzZWxlY3RlZExpbmVDb3VudCI6MTMsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImNkNDY5ZTYxYzVmYzhjZTc4ZGQzOWQ2ZWQyMTMyY2JlMDA0Mzk5NTlhYjc5MTY0ZTM4YjJhYzY5NDA3ZDU1NmMiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjAxNjUyNTRiZGUzY2I5ZjNkMjU1MDNkMGRlYzczZmZlOGY4MTcyMmUzNjc5Nzc4Y2NiMTRkNDA1M2MyMTJhYTQiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImQwOWMwYjg4Y2IwOTczNTY5NzIxYThlZDNjNjBhOTUzMDU1MTEwNjJjNGYzNmM2NjUzOTE1ZTU3ZGRmZTQ5NDUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6Ijg2MGZlMmRiYmY0ZjEzYjNkNDBjZDhmNjliMGQzY2Y5ZGM3ZjI4Y2U5N2FjZmZjNzFmODgzNWIxYmIyMGZhNmEifQ"
         }
       ]
     },
@@ -125,18 +125,46 @@
       "statement": "The AI request log omits the API key and redacts image data URLs but records the full request body and outcome, appending without truncation.",
       "evidence": [
         {
-          "resource": "repo://src/ai.rs#L200-L209",
+          "resource": "repo://src/ai.rs#L248-L257",
           "version": "repo-lines-v1:sha256:ecd1d611f74d5bde7229623335cb8f8bf5cfff645aa0dccc5adb5732d9287806:eyJzZWxlY3RlZExpbmVDb3VudCI6MTAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjY1NThiY2FiNTVlMjRlNWYzNGUzYzg3ZWFkMGI1MmRiNDExOWRhOGM5MmI3MDJmYjkyZTg4YjgyNGYxZDllODAiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImM1YzIyY2NmNTE5YjEyMWM0MDBlNmQ4MTg3YTA4MDI5ZjE5MmE0ZmViMDU1NWYyZTBkYzJmYjg4OWY3NWEwZTYiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjJmNjljNjZiNTk4NDRmMjg4YzY3Yzg4NWY2NzQwNmEwNmExNjg4OWI4ZTc4NDMzNDZiMWUzNmRkNjRlYWIzNTUifQ"
         },
         {
-          "resource": "repo://src/ai.rs#L350-L442",
+          "resource": "repo://src/ai.rs#L424-L516",
           "version": "repo-lines-v1:sha256:89a3eb5bccc99aa1700ff566d228266bde9e75070cec6fc4ff8f961fd2cb30f7:eyJzZWxlY3RlZExpbmVDb3VudCI6OTMsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjNhZmRkOGE1NjBlNzQ4ZmFmNGRlZDhhYjJmOWU4NmUyMTU5NDA1YTAxMGUwYzE4YjI2M2NhNDE4MzAxMzFjZTciLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjcwZmRhNDBjNDBhZTM2Yzk5ODVhZWRjMDE5MDM3NDJmMTViYmY5ZDY4N2IxZjk1MTVmZDkwOWU2ZGUzY2E2ZTkiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjJjZTgwZDQ1MzU3ZWY2NWZiNjUzODgzZWEzOTU1NDMyMDMzN2Q0YjA3MzcxNzQ1ZWE0MGFkYzRmMGFjNTU0YTcifQ"
         }
       ]
+    },
+    {
+      "id": "claim_8ffb06dc9f33422faa661f09a96c67a0",
+      "statement": "web_port is parsed as Option<u16>, so negative or above-65535 values fail TOML parsing, and 0 is rejected with an error naming the key; unset disables the web panel.",
+      "evidence": [
+        {
+          "resource": "repo://src/config.rs#L129-L131",
+          "version": "repo-lines-v1:sha256:b4f8f3c857b723e0825d50093438b9ff2266301cb5707198e764fe82496ab30a:eyJzZWxlY3RlZExpbmVDb3VudCI6MywiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiNzEzN2YxMDljNWYyMDU1OGE3MzUwZDRlYjdkMmU2ZTllNDk1NzIwY2Y0YWQzZTk4NjZlY2E2NDAxNWU3MGJkYSIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiN2U0MTE5NDE4OWJhMWY1NzIyMjFiMjA3N2JmMDU4ZmExYTNkZTA3ODdkMzIzZWMyYWI0MGU3MmEyMTRiYmM3OCIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiMjgyZmY5MDM4ZGNhMmQwMGZjZjczNzgzOTQwNWI2NGI3NmVlODMyNzA4NTVlMDMyZDU5YTU4YzYyMTc4NDkwNSIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiOTY4MTE0ZGZhNDU1MWJiNmMwMDBmNGUyNTVjNjhiMDYyMmZjYmUyY2YyZTJiYzJmZThhODNmYTg2ZmY2OTk2MCJ9"
+        },
+        {
+          "resource": "repo://src/config.rs#L243-L252",
+          "version": "repo-lines-v1:sha256:6d490312b71acd64e2e0d7d854caad2acb8ba41147fcb3bcf92195ba26fc850e:eyJzZWxlY3RlZExpbmVDb3VudCI6MTAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjM3NGJkYjgxZDEzY2MxOTQwZWE0Y2EzNTQ4NzZiODhiYmEyOWYxMmIzNTIzZTE1M2VlZDZiOGFlNWM4YjllYTciLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImEzMjJlODc4N2I1NGUwYzg5MmMxMzExNmFmOTM5ZjY5YTZkM2VjNzE2ZTMzMWY3N2JkN2E3OTFmYzAwMjFhOWYiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImUzNDgzNDhhOTA1MjQyNTQzZWZkZWU4MmY4NzVkZTE5NTc1OWNkNDA5Mjg0N2QwZWViMjc3ZmFkODNkNzc1MWYifQ"
+        },
+        {
+          "resource": "repo://src/config.rs#L451-L475",
+          "version": "repo-lines-v1:sha256:37db7f6e1b478b12a699a96825e0e7d8506be31e81194c64ea6f58027479b43f:eyJzZWxlY3RlZExpbmVDb3VudCI6MjUsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImE3NmFiMWQ5ZjdkNjNlNzQ4YjE1ZWQ4NzQzYjFjOGE2MWQxZmE4ODNmYzQ5NjkwMDM1NzUxMDk3N2UzMmJkMjQiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6ImFjZjZmZDFkMjdkMzRmN2Q2N2RhMWQ5MDI2M2I0MzllZjk3MjNmOTRmYTY5YTJmZDI5NzZlYmE2ZDQ3MzAzNDAiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImEwMGRiMDQwZjI5MDliZjk3OGZmMGE5NTZhNTVlODNjNjUzNDE4ZGU2YmI2OGY4MDQxZGE5NzZmOGQzZDIyNzAiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6Ijg5M2QyZGYwZGQzNzE4YmQ2M2NiMGFkYjE3NjllNWMxOWJmYzA4YzY4ZDM5NjEwYjU5NGM3NDM0MTE4YTQwMGQifQ"
+        }
+      ]
+    },
+    {
+      "id": "claim_919f934c898e4e68a57c6e267fd4238c",
+      "statement": "config.toml.global.template lists every supported key with its default value and leaves web_port commented out.",
+      "evidence": [
+        {
+          "resource": "repo://config.toml.global.template",
+          "version": "repo-file-v1:sha256:3b36a2e26714ee5ee10f7f4facca5cf94c8dcced7dd757911044af3066ba6070"
+        }
+      ]
     }
   ],
   "verification": {
     "by": "openwiki/0.7.2",
-    "at": "2026-10-19T19:53:56.480Z"
+    "at": "2026-10-20T20:27:06.515Z"
   }
 }

+ 23 - 9
openwiki/.claims/quickstart.json

@@ -1,6 +1,6 @@
 {
   "schemaVersion": 1,
-  "pageVersion": "sha256:b3c1d2b60ba5660ee0e8c7337427e01c7374daa9f8936232bbf6e8190f167d84",
+  "pageVersion": "sha256:f8e55a63196e59cd28b6114d94f0faa4326a003dbe6b743831920c4b4a7fb4c9",
   "claims": [
     {
       "id": "claim_6b1c328bf7f7452992ea8eeef561edd4",
@@ -27,22 +27,22 @@
       "statement": "config.toml, session.db, history.db and the optional ai_requests.log are located in the wh directory under the platform config directory.",
       "evidence": [
         {
-          "resource": "repo://src/config.rs#L144-L148",
+          "resource": "repo://src/config.rs#L166-L170",
           "version": "repo-lines-v1:sha256:dd4141940976146e96656f2de294f92e1275a16efba97aa2c7318676f34bdfa4:eyJzZWxlY3RlZExpbmVDb3VudCI6NSwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiMzVhMDYyMDkxMmIzMTUwZDZlYmQwNDQyOTUxZTdjNWEwODlmODMwMzYzNmJjODZiNmQxOWRmN2U3YTFmMWEzZCIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDEyY2EzNDVjY2Y3NWJmOWMwODA2YmNlNjk1YmU4ZGU4MDhiNzk5ODQyNTFhN2E1NGQyMDJjZjYxMDFkZDQ1MSIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiNGU1ZGI4YzkwMjczZTAyOTc3MTY3ZWMwNDM0N2M0MWQ5MmE1MTE4NDMxMTA0M2UyMTU2YmM4MGFiYjkwMWQ0NyIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiMDY4YzgxNzM5NzQwNTE2YzI4OWQ4ZWRlZWM0N2IwMjcxMGFmOWVmNWM4MjE4M2NhNWQwMTc4MWVhMjI1YjU1MCJ9"
         },
         {
-          "resource": "repo://src/config.rs#L40-L49",
+          "resource": "repo://src/config.rs#L47-L56",
           "version": "repo-lines-v1:sha256:6624d58d13d3982b8829e7b759fc7c36a643278f2d243526a887428e34a0d469:eyJzZWxlY3RlZExpbmVDb3VudCI6MTAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImU1MDllZGUxYTUxZmE5MGM4ZmQ0YTNkODdmYjJhMGU5OTMwNzhkMjc0MzU1NGFjMGM4MmIzMTdmNzQ0OTY5ZmYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjFmNzZjMzcwNTRkNWY0YzFhNWZhZDE4YzFlYjI0NTNmODIxNTI0NDQ2M2ZkNjA4MzIwZDEzZDYxNmFlYmVhNmQiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjMwY2VjY2EzYjg3NDgwNDU3ZGVmZTlkOWVkY2I1N2VmMDdlN2ZlZTE2MTljZjdmYWEyNzM0Y2Y2M2Q5MzIxYjEiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjQ1N2RlMDJkYWE2M2RmYzAxZmQxNjkyNDc1ODY2ODA3OGM5NmJlMGM1ODkyZjQ4M2UzMDdmNjAyYTU0MDRkMzQifQ"
         }
       ]
     },
     {
       "id": "claim_f1a67c370b204d5484f31751987d20d9",
-      "statement": "main loads the configuration, opens the history, starts the WhatsApp client and input thread, and runs the TUI loop.",
+      "statement": "main loads the configuration, opens the history, starts the WhatsApp client, the optional web panel and the input thread, and runs the TUI loop.",
       "evidence": [
         {
-          "resource": "repo://src/main.rs#L28-L51",
-          "version": "repo-lines-v1:sha256:a1f78dd345a599ce923351dc782f73a2a62e23022449fe5ca2ec531b7f066305:eyJzZWxlY3RlZExpbmVDb3VudCI6MjQsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImExZDA2ZTA0OGNkMjcyNzM4ZDgzN2IwYzBlYzZhYWIwYjAzMTQ1N2U0NzVmZDQ5YWJiY2JhZmM1ZjczYzAyMGQiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjczZTczMWU3YjM0NmQ1YzQyMzJhYzA0ODFlYmY5ZWZhNTgxNTljMTY4ZmFhNTQ4YzYwNDZiODI1Yjg2Y2ZmMDQiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjcyOGNjMGI0ZGIzNjFhYzMxMDc4YmMxOTZiZDY1MjkxMzVhNmFmYzQ1ZWNhZTkyMWY2MTcxMjI1ZTJmNGNkOTgifQ"
+          "resource": "repo://src/main.rs#L30-L54",
+          "version": "repo-lines-v1:sha256:78c36720946a6da37fed1551aa53c38d6a246801eca122beca85edd71edb50f1:eyJzZWxlY3RlZExpbmVDb3VudCI6MjUsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImExZDA2ZTA0OGNkMjcyNzM4ZDgzN2IwYzBlYzZhYWIwYjAzMTQ1N2U0NzVmZDQ5YWJiY2JhZmM1ZjczYzAyMGQiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImZhNDUwZGE5MjlmMjA2MmRjNmMxMzBhZTc4MDU2NWE2MGUyYTkyZmI4YjYzNTA5MTZkNTdiM2EwYzA1MDM4MTkiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjcyOGNjMGI0ZGIzNjFhYzMxMDc4YmMxOTZiZDY1MjkxMzVhNmFmYzQ1ZWNhZTkyMWY2MTcxMjI1ZTJmNGNkOTgifQ"
         }
       ]
     },
@@ -51,7 +51,7 @@
       "statement": "Cargo.toml allows the linker_messages rustc lint to silence the informational MSVC linker output.",
       "evidence": [
         {
-          "resource": "repo://Cargo.toml#L25-L27",
+          "resource": "repo://Cargo.toml#L35-L37",
           "version": "repo-lines-v1:sha256:80aab8b256e2ca1c4af789b43f42de75561d162750340e207f0e9dcadf41552b:eyJzZWxlY3RlZExpbmVDb3VudCI6MywiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiYWU3NWQ5NjYzNmEzNTFhYTA3MWViNmE4NDE2MWI0YzI0MDVkNDI4NzA0MmVhOTQwNDBiM2E5N2NjZjQxMjE0YiIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiYzM4ZmZlYWFkMDE1YjEyYjhkN2VlMDAzMmY3MjEzYzc3ODMyOGMyM2QwZDE0ZWYzYTNiYzYxZGIxMTBkMGE1ZCIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiODM5MThmNWUzMjViZTk0YWJmYzc5MTY4OTEzMzAyMTg3MTlhYzlkN2QxM2ExOWU2YjEyYWYyYjM2NTFmMTI1MCIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjAsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiZTNiMGM0NDI5OGZjMWMxNDlhZmJmNGM4OTk2ZmI5MjQyN2FlNDFlNDY0OWI5MzRjYTQ5NTk5MWI3ODUyYjg1NSJ9"
         }
       ]
@@ -61,7 +61,7 @@
       "statement": "The AI autoreply can be enabled for private chats and groups but not for broadcasts or newsletters.",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L885-L898",
+          "resource": "repo://src/app.rs#L1127-L1140",
           "version": "repo-lines-v1:sha256:76ea2311dd543d33bf1c52b4cee8997184cb67643a8ba3b2f715abf2fea34667:eyJzZWxlY3RlZExpbmVDb3VudCI6MTQsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjA0NDI0MGFiOGNkMTM1NDE2ZTQ5MjMyYTEzMGIzYzlkYmI2NWZmYzg0OGNjYzY3OWJhYTQxYWQzNjY4ZTA3MjQiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjE5ZTc2MzU1Y2YxMTk5NDgwYzNmOTk4MzZiMTQ4NmM4NDFkODkxZmFhZDkwNGM2MDhlMDkzMDIyZDM3NTdhZWUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6Ijg5NGUwY2FkODFjNTBiOWY4MWE5ODI2ZjA1YzUyZjY0MGE0ZGM3NzdiYWQyNGM2MGEwMjQ5NzhlZWJmMWRjMGMifQ"
         },
         {
@@ -69,10 +69,24 @@
           "version": "repo-lines-v1:sha256:0ec556787987c44ceb565f6d300f0cf6acd13afd25b5e78a34de780c044b82fc:eyJzZWxlY3RlZExpbmVDb3VudCI6MzIsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjEzNDhjYzQxMDQ1OTNmYjgwNTEzZjBiMmUyNDQwYWYwZjc0M2Y5Yzk5Nzk0YWQzZGNiZGQ4ZjY0YjM4ZDBhNmMiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjFmOThmZjFmNmFmYzAyNDYzN2IxMzgyOWJjNmFlMGJkYjA0NmU3YWQwMjQ0MWZmYTAzZDE4YmI4NzI0NmE5OTQiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjZhYzE3ZjhmYjIwNjZkNTU5ZDU2MmYzNTkwODY4ZDRlNTMwYjIwZmQzNzdkNDkwZTJmZjE4Njc3Y2M3YTBlZTkifQ"
         }
       ]
+    },
+    {
+      "id": "claim_f9fef3eb56c84ffba6d3f150690d3647",
+      "statement": "Commands use function keys: F2 switches chats/contacts, F3 toggles the open chat's autoreply, F4 toggles the global autoreply and F10 quits.",
+      "evidence": [
+        {
+          "resource": "repo://src/app.rs#L22-L29",
+          "version": "repo-lines-v1:sha256:1b77b4b729c472be09a45c5473cf8594fc6554a18cc8f686ece6245ff2aa26ec:eyJzZWxlY3RlZExpbmVDb3VudCI6OCwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiYmQxOTM5NWQwMDUxZDZhMDgzMGQyZDk5ZmE1OThkNTg3YTI5ZmQ1MDQ5OTA0ZDU5Y2I1MjZiZDdjNzg4MGIwZSIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiYWY2MjM3MDg5YjkxZDEwN2I2NjQ1Nzk3M2E4YmEyYTcxZDg3MDQ0ZWE5MWU5MThjZGUyMDVmODZhY2Q0NmM5MyIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiMWVmZGQwMDhhMTYzNjZhM2YxN2I2NjUzYjI3NzIxOGRjNDY1NGIwYWNhMTkwYjViYWJkNzA2YjIzYjEwNDA3NyIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiZjM1MGFjYmZhYzdlNDEzZmFkNWYxMzk0NTQwYWE3MWM3YWY3ZWZmMDA4MDliYmMxYTlhNzQxZGRhNDFiOGYwZCJ9"
+        },
+        {
+          "resource": "repo://src/app.rs#L573-L603",
+          "version": "repo-lines-v1:sha256:37edcb8b1691dbaa467f7d59b80cdc3781376ce6ce1fc1783f167c52ae5ce169:eyJzZWxlY3RlZExpbmVDb3VudCI6MzEsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjNmODVjMjY1ZjBmMjNkOTA0NDcwMjYxNWZiM2NkMzQ2ODMxYTFiZWNiOTRlYzNkODYyNDYxYzFkNWEwMGQzNDUiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjE5OTRiMGIxZDNhNDA1MzRjYTFiZTVmNDdiNTRlNWY3ZDg3ZTZlMGMxMWE3ZjU5ZjA0NGU2ZGJmNTkyYzNlZDYiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6Ijk2ZWU5ZDllODg2ODFmN2JmM2M4ZmZiZjEzZDgyODk2ODM4MGY2Y2E0M2Y1ZGE4ODI5MmU1MjY3ZDRjMDhlOWYifQ"
+        }
+      ]
     }
   ],
   "verification": {
     "by": "openwiki/0.7.2",
-    "at": "2026-10-19T19:53:56.480Z"
+    "at": "2026-10-20T20:27:06.515Z"
   }
 }

+ 46 - 32
openwiki/.claims/testing/unit-tests.json

@@ -1,17 +1,17 @@
 {
   "schemaVersion": 1,
-  "pageVersion": "sha256:c68cbef2ff467d2cea57ad59f059cb2f524cd4728d83dbb97ee74719dc672a37",
+  "pageVersion": "sha256:a01bacd1db7d8ffec9c6c644ce544b4408c7a27be2ca73f04ff774e65a7810cb",
   "claims": [
     {
       "id": "claim_2e232262c6d940c7ba52a945b96928f6",
       "statement": "Each source module except main.rs has an in-file cfg(test) test module.",
       "evidence": [
         {
-          "resource": "repo://src/ai.rs#L452-L453",
+          "resource": "repo://src/ai.rs#L526-L527",
           "version": "repo-lines-v1:sha256:03c5b4b27fb001def5489d5e0264e20f2538b8214c3df8ceda3354aaee53c2c8:eyJzZWxlY3RlZExpbmVDb3VudCI6MiwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiMjVjZjVmYzIzZjJhNmQyM2MzZWE1MWU3MmY1ZTg5NmM3NTU1ZmJhYTA1Y2Q1YjFlYTE4MmJmOTYxOGE4ZTgxYiIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiMWUwYWYzZmQ0YTRjM2ExZDQ3YzdmNDcwZWQwZDg2ZDkwZDlkZTIxMGE1ZWIxNDU1ZmJkOTQ0ODJhNDc2NWI3MiIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiZWRjODMxMjQ5NjYxNGM2YzllMTE0NTc0YTc5ZDkxODU3MDM0YmJlM2IzMDk5MmYyNzYwZjcxNjY0ZjAwNWM2OSIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiOTM0OTA4YTE2NjZlZThkZWFiMmIyYTc2NjdjODQ0OGZiYjRkNDZlMzJhYjZjNTY4YmI3MjMyMGU1ZWJmMDlmMyJ9"
         },
         {
-          "resource": "repo://src/app.rs#L909-L910",
+          "resource": "repo://src/app.rs#L1167-L1168",
           "version": "repo-lines-v1:sha256:03c5b4b27fb001def5489d5e0264e20f2538b8214c3df8ceda3354aaee53c2c8:eyJzZWxlY3RlZExpbmVDb3VudCI6MiwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiMjVjZjVmYzIzZjJhNmQyM2MzZWE1MWU3MmY1ZTg5NmM3NTU1ZmJhYTA1Y2Q1YjFlYTE4MmJmOTYxOGE4ZTgxYiIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiMWUwYWYzZmQ0YTRjM2ExZDQ3YzdmNDcwZWQwZDg2ZDkwZDlkZTIxMGE1ZWIxNDU1ZmJkOTQ0ODJhNDc2NWI3MiIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiZjY5NGY2NjJmYWIyMzFlOGQ4ZGIwOTRiNTU3OTgxNWYwOTlkYTQwZGU3ZThiOTY1YWUwNDU2YWRjMDQ5YjM4NiIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiOGJlOTk2NWNlNGEzMmY5N2Y5ZjMzYzU1MzhjZWY5ZDE0Njc4YjE0NGQ4ODg1ZDY2Mjk4YTZkZTJjODg3NTZlMSJ9"
         },
         {
@@ -19,19 +19,19 @@
           "version": "repo-lines-v1:sha256:03c5b4b27fb001def5489d5e0264e20f2538b8214c3df8ceda3354aaee53c2c8:eyJzZWxlY3RlZExpbmVDb3VudCI6MiwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiMjVjZjVmYzIzZjJhNmQyM2MzZWE1MWU3MmY1ZTg5NmM3NTU1ZmJhYTA1Y2Q1YjFlYTE4MmJmOTYxOGE4ZTgxYiIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiMWUwYWYzZmQ0YTRjM2ExZDQ3YzdmNDcwZWQwZDg2ZDkwZDlkZTIxMGE1ZWIxNDU1ZmJkOTQ0ODJhNDc2NWI3MiIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiOTMyZjlmMDdlZDQ0ZTc0ZjliN2Y1ZGEzNTY1NjIxZjExNGRiNzdlZGY1NDc2NzZjNjZiNzRjMWMwNzQzN2MyMSIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiN2M0NzcyZjY4MTNiYmFmYmNhYjU4MWZlN2JiZmIwOTI4NDRlMGZhOWU3MjQwNmMxNzNjNmYyNTMxMjgyMTZmMSJ9"
         },
         {
-          "resource": "repo://src/config.rs#L288-L289",
+          "resource": "repo://src/config.rs#L323-L324",
           "version": "repo-lines-v1:sha256:03c5b4b27fb001def5489d5e0264e20f2538b8214c3df8ceda3354aaee53c2c8:eyJzZWxlY3RlZExpbmVDb3VudCI6MiwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiMjVjZjVmYzIzZjJhNmQyM2MzZWE1MWU3MmY1ZTg5NmM3NTU1ZmJhYTA1Y2Q1YjFlYTE4MmJmOTYxOGE4ZTgxYiIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiMWUwYWYzZmQ0YTRjM2ExZDQ3YzdmNDcwZWQwZDg2ZDkwZDlkZTIxMGE1ZWIxNDU1ZmJkOTQ0ODJhNDc2NWI3MiIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiMTllNzYzNTVjZjExOTk0ODBjM2Y5OTgzNmIxNDg2Yzg0MWQ4OTFmYWFkOTA0YzYwOGUwOTMwMjJkMzc1N2FlZSIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiNGEwYTUzOWYxODQ2ODA0NzgzMDRlNjEzNTEzNGQyM2FhMjE2ZmZkZTBmMTYwMzJmYTZkNzYyOGQ0MzIxNzFhNSJ9"
         },
         {
-          "resource": "repo://src/history.rs#L418-L419",
+          "resource": "repo://src/history.rs#L481-L482",
           "version": "repo-lines-v1:sha256:03c5b4b27fb001def5489d5e0264e20f2538b8214c3df8ceda3354aaee53c2c8:eyJzZWxlY3RlZExpbmVDb3VudCI6MiwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiMjVjZjVmYzIzZjJhNmQyM2MzZWE1MWU3MmY1ZTg5NmM3NTU1ZmJhYTA1Y2Q1YjFlYTE4MmJmOTYxOGE4ZTgxYiIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiMWUwYWYzZmQ0YTRjM2ExZDQ3YzdmNDcwZWQwZDg2ZDkwZDlkZTIxMGE1ZWIxNDU1ZmJkOTQ0ODJhNDc2NWI3MiIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiNjA2M2M0ZDFhZjhjZDg5YmIwYmRkNzY1N2ZmY2JmNjgwNmE2MTBkNzlkNmM4ZDRjMzA3YzI0MWRmMzkwMDUyMiIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiMjgzMTBhMmUzZTliNGU0MTRjY2FiZmY2ZTdkMTc0YjgwMzVlNmZjZTRmODVmMTdjZTA4MTNmOWZlZTkzOTYxZiJ9"
         },
         {
-          "resource": "repo://src/ui.rs#L508-L509",
+          "resource": "repo://src/ui.rs#L558-L559",
           "version": "repo-lines-v1:sha256:03c5b4b27fb001def5489d5e0264e20f2538b8214c3df8ceda3354aaee53c2c8:eyJzZWxlY3RlZExpbmVDb3VudCI6MiwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiMjVjZjVmYzIzZjJhNmQyM2MzZWE1MWU3MmY1ZTg5NmM3NTU1ZmJhYTA1Y2Q1YjFlYTE4MmJmOTYxOGE4ZTgxYiIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiMWUwYWYzZmQ0YTRjM2ExZDQ3YzdmNDcwZWQwZDg2ZDkwZDlkZTIxMGE1ZWIxNDU1ZmJkOTQ0ODJhNDc2NWI3MiIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiMzViMjE2ODczZmZmNmEwNTc3MDU5NWM1OWNhZDNlNmE5NzM1MmJiNDE3ZDYwYzc2MmVhNDM4MWYxOTkwMjg3OSIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiNDVlYjIxMDg3NDU0NjZhOTVjZjg5YjVhZjdmMDZhMDA2NGU0OGRiMDVkMmMyOWU5Y2YzZDZkMGQxNmY3MjY5ZiJ9"
         },
         {
-          "resource": "repo://src/whatsapp.rs#L990-L991",
+          "resource": "repo://src/whatsapp.rs#L1005-L1006",
           "version": "repo-lines-v1:sha256:03c5b4b27fb001def5489d5e0264e20f2538b8214c3df8ceda3354aaee53c2c8:eyJzZWxlY3RlZExpbmVDb3VudCI6MiwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiMjVjZjVmYzIzZjJhNmQyM2MzZWE1MWU3MmY1ZTg5NmM3NTU1ZmJhYTA1Y2Q1YjFlYTE4MmJmOTYxOGE4ZTgxYiIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiMWUwYWYzZmQ0YTRjM2ExZDQ3YzdmNDcwZWQwZDg2ZDkwZDlkZTIxMGE1ZWIxNDU1ZmJkOTQ0ODJhNDc2NWI3MiIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiZDZkMTNkYTUzZGRmNTRlMDlhOWZiMzc3ODc0MTRhOTYxNDZkNDcxYzVlYmM2Y2UyNmNmOWEzODRmMTg3NzhmMSIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiMWU1MWM0MjBiN2E3NDU0MmZlNGYwMjY4Yzk0YTYwZjRmYjAwZTdhODI0NmFhNTlkNzQ0NTkzYmY3OGUyN2M5MCJ9"
         }
       ]
@@ -41,8 +41,8 @@
       "statement": "Cargo.toml declares only regular dependencies and no dev-dependencies.",
       "evidence": [
         {
-          "resource": "repo://Cargo.toml#L1-L27",
-          "version": "repo-lines-v1:sha256:a83265d8e74fb76c5882e205a013fd2151b3dc63d5db2f2a3e5224aeccde899d:eyJzZWxlY3RlZExpbmVDb3VudCI6MjcsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjhhM2NkNWE4MWIzZjlhNjIxYWE0OTNkOTBjNDVmNDJhYjU3MWQ0ZTQyYjhhZTVhZmYzNTFjYjBhMDJkMDZkODIiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6ImMzOGZmZWFhZDAxNWIxMmI4ZDdlZTAwMzJmNzIxM2M3NzgzMjhjMjNkMGQxNGVmM2EzYmM2MWRiMTEwZDBhNWQiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjowLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImUzYjBjNDQyOThmYzFjMTQ5YWZiZjRjODk5NmZiOTI0MjdhZTQxZTQ2NDliOTM0Y2E0OTU5OTFiNzg1MmI4NTUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjowLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImUzYjBjNDQyOThmYzFjMTQ5YWZiZjRjODk5NmZiOTI0MjdhZTQxZTQ2NDliOTM0Y2E0OTU5OTFiNzg1MmI4NTUifQ"
+          "resource": "repo://Cargo.toml#L1-L37",
+          "version": "repo-lines-v1:sha256:61a45133c26cb38839e1ab763b6ecd5f7aeaf15aa3a58fef3840a2fc44b8b6a8:eyJzZWxlY3RlZExpbmVDb3VudCI6MzcsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjhhM2NkNWE4MWIzZjlhNjIxYWE0OTNkOTBjNDVmNDJhYjU3MWQ0ZTQyYjhhZTVhZmYzNTFjYjBhMDJkMDZkODIiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6ImMzOGZmZWFhZDAxNWIxMmI4ZDdlZTAwMzJmNzIxM2M3NzgzMjhjMjNkMGQxNGVmM2EzYmM2MWRiMTEwZDBhNWQiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjowLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImUzYjBjNDQyOThmYzFjMTQ5YWZiZjRjODk5NmZiOTI0MjdhZTQxZTQ2NDliOTM0Y2E0OTU5OTFiNzg1MmI4NTUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjowLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImUzYjBjNDQyOThmYzFjMTQ5YWZiZjRjODk5NmZiOTI0MjdhZTQxZTQ2NDliOTM0Y2E0OTU5OTFiNzg1MmI4NTUifQ"
         }
       ]
     },
@@ -58,21 +58,21 @@
     },
     {
       "id": "claim_879517863bdd4ae5b7cc952b3e39e8a4",
-      "statement": "history tests cover deduplication, ordering, messages_since, chat names, autoreply flags, contacts and on-disk opening.",
+      "statement": "history tests cover deduplication, ordering, messages_since, chat names, per-chat and global autoreply flags, chat deletion, contacts and on-disk opening.",
       "evidence": [
         {
-          "resource": "repo://src/history.rs#L418-L663",
-          "version": "repo-lines-v1:sha256:cc837e674ada6149be57f022f902b8439eba315c59ac874e7d65bf28f1e75d02:eyJzZWxlY3RlZExpbmVDb3VudCI6MjQ2LCJmaXJzdFNlbGVjdGVkTGluZUhhc2giOiIyNWNmNWZjMjNmMmE2ZDIzYzNlYTUxZTcyZjVlODk2Yzc1NTVmYmFhMDVjZDViMWVhMTgyYmY5NjE4YThlODFiIiwibGFzdFNlbGVjdGVkTGluZUhhc2giOiI0MTJjYTM0NWNjZjc1YmY5YzA4MDZiY2U2OTViZThkZTgwOGI3OTk4NDI1MWE3YTU0ZDIwMmNmNjEwMWRkNDUxIiwicHJlY2VkaW5nQ29udGV4dExpbmVDb3VudCI6MywicHJlY2VkaW5nQ29udGV4dEhhc2giOiI2MDYzYzRkMWFmOGNkODliYjBiZGQ3NjU3ZmZjYmY2ODA2YTYxMGQ3OWQ2YzhkNGMzMDdjMjQxZGYzOTAwNTIyIiwiZm9sbG93aW5nQ29udGV4dExpbmVDb3VudCI6MCwiZm9sbG93aW5nQ29udGV4dEhhc2giOiJlM2IwYzQ0Mjk4ZmMxYzE0OWFmYmY0Yzg5OTZmYjkyNDI3YWU0MWU0NjQ5YjkzNGNhNDk1OTkxYjc4NTJiODU1In0"
+          "resource": "repo://src/history.rs#L481-L763",
+          "version": "repo-lines-v1:sha256:1d69fc86c05572c4af6df08c56af463f1a0de79555132c853c3e8b7c07eab17d:eyJzZWxlY3RlZExpbmVDb3VudCI6MjgzLCJmaXJzdFNlbGVjdGVkTGluZUhhc2giOiIyNWNmNWZjMjNmMmE2ZDIzYzNlYTUxZTcyZjVlODk2Yzc1NTVmYmFhMDVjZDViMWVhMTgyYmY5NjE4YThlODFiIiwibGFzdFNlbGVjdGVkTGluZUhhc2giOiI0MTJjYTM0NWNjZjc1YmY5YzA4MDZiY2U2OTViZThkZTgwOGI3OTk4NDI1MWE3YTU0ZDIwMmNmNjEwMWRkNDUxIiwicHJlY2VkaW5nQ29udGV4dExpbmVDb3VudCI6MywicHJlY2VkaW5nQ29udGV4dEhhc2giOiI2MDYzYzRkMWFmOGNkODliYjBiZGQ3NjU3ZmZjYmY2ODA2YTYxMGQ3OWQ2YzhkNGMzMDdjMjQxZGYzOTAwNTIyIiwiZm9sbG93aW5nQ29udGV4dExpbmVDb3VudCI6MCwiZm9sbG93aW5nQ29udGV4dEhhc2giOiJlM2IwYzQ0Mjk4ZmMxYzE0OWFmYmY0Yzg5OTZmYjkyNDI3YWU0MWU0NjQ5YjkzNGNhNDk1OTkxYjc4NTJiODU1In0"
         }
       ]
     },
     {
       "id": "claim_a4df2645cea34004b214e75e9d87c9e5",
-      "statement": "ui tests cover sender labels, clock time, QR text, wrapping, visible lines, input tail, title and selection clamping.",
+      "statement": "ui tests cover the footer key help, sender labels, clock time, QR text, wrapping, visible lines, input tail, messages and list titles and selection clamping.",
       "evidence": [
         {
-          "resource": "repo://src/ui.rs#L536-L636",
-          "version": "repo-lines-v1:sha256:b4faf7471bde683dfd2f5bf5fde7d21ca4db9271f2b31412c8c621e91b13e58d:eyJzZWxlY3RlZExpbmVDb3VudCI6MTAxLCJmaXJzdFNlbGVjdGVkTGluZUhhc2giOiI5NmQ0MTY4YWI4OGJiZjllY2NiMjEyYmY4NWM3ZjJlOGY5NmIwZjNmNTE2NzI0ZTdmZTU4NzMyYmNhMzhlZTYyIiwibGFzdFNlbGVjdGVkTGluZUhhc2giOiI0MTJjYTM0NWNjZjc1YmY5YzA4MDZiY2U2OTViZThkZTgwOGI3OTk4NDI1MWE3YTU0ZDIwMmNmNjEwMWRkNDUxIiwicHJlY2VkaW5nQ29udGV4dExpbmVDb3VudCI6MywicHJlY2VkaW5nQ29udGV4dEhhc2giOiJhMDBkYjA0MGYyOTA5YmY5NzhmZjBhOTU2YTU1ZTgzYzY1MzQxOGRlNmJiNjhmODA0MWRhOTc2ZjhkM2QyMjcwIiwiZm9sbG93aW5nQ29udGV4dExpbmVDb3VudCI6MCwiZm9sbG93aW5nQ29udGV4dEhhc2giOiJlM2IwYzQ0Mjk4ZmMxYzE0OWFmYmY0Yzg5OTZmYjkyNDI3YWU0MWU0NjQ5YjkzNGNhNDk1OTkxYjc4NTJiODU1In0"
+          "resource": "repo://src/ui.rs#L558-L730",
+          "version": "repo-lines-v1:sha256:2bb51360845dc96ea63e9ba03c980bf2957a86649ecdd32efa1bcf97463d56b1:eyJzZWxlY3RlZExpbmVDb3VudCI6MTczLCJmaXJzdFNlbGVjdGVkTGluZUhhc2giOiIyNWNmNWZjMjNmMmE2ZDIzYzNlYTUxZTcyZjVlODk2Yzc1NTVmYmFhMDVjZDViMWVhMTgyYmY5NjE4YThlODFiIiwibGFzdFNlbGVjdGVkTGluZUhhc2giOiI0MTJjYTM0NWNjZjc1YmY5YzA4MDZiY2U2OTViZThkZTgwOGI3OTk4NDI1MWE3YTU0ZDIwMmNmNjEwMWRkNDUxIiwicHJlY2VkaW5nQ29udGV4dExpbmVDb3VudCI6MywicHJlY2VkaW5nQ29udGV4dEhhc2giOiI5YTc1YTI2ZGRjNmU4NmI3YjZjZjliNWFjNTAwZjBiNWM4ZmM5MWRmZWE4Yjc1YjdkN2NiMDhjYjY3N2ZkOThmIiwiZm9sbG93aW5nQ29udGV4dExpbmVDb3VudCI6MCwiZm9sbG93aW5nQ29udGV4dEhhc2giOiJlM2IwYzQ0Mjk4ZmMxYzE0OWFmYmY0Yzg5OTZmYjkyNDI3YWU0MWU0NjQ5YjkzNGNhNDk1OTkxYjc4NTJiODU1In0"
         }
       ]
     },
@@ -81,46 +81,50 @@
       "statement": "History tests use an in-memory database helper and the on-disk test writes under the system temporary directory.",
       "evidence": [
         {
-          "resource": "repo://src/history.rs#L648-L662",
+          "resource": "repo://src/history.rs#L748-L762",
           "version": "repo-lines-v1:sha256:47bb26a3e316232f3605730556cc5136a6388c441f30c963b8d723852177dc9f:eyJzZWxlY3RlZExpbmVDb3VudCI6MTUsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjkyYjVlZGY1OTQ5Y2ZlYTY3ODIyY2Y2OTExMzIzNjNkNjhkNmYxZTE2OWY1ZTBkNGY3YzE0ZTVmYTFlNmQ0ZTUiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjdjOWI2OGRlMGIyZmFhYWM3OTQwMzEwNmMxNGRkOWE5MDgyODA4NzEzZmE0MjBjMWUyMmE2OGM1MTE3ZTE0MWMiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjoxLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEifQ"
         },
         {
-          "resource": "repo://src/history.rs#L74-L80",
+          "resource": "repo://src/history.rs#L79-L85",
           "version": "repo-lines-v1:sha256:5c13f3ecfca20f282a95114cf2752173bde3a7eb69f54778891a66c4a626c76f:eyJzZWxlY3RlZExpbmVDb3VudCI6NywiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiNjg5ZWI3YTE3N2JjMWI2YWVjYzNlMTQxNDE2ZGZiMDg3OTYyNDYwMjNlY2YzNDQ5OTdkNDNjZTE1MzRjMWM4MiIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiYjlkYWFmZmFjMzJjZTI2NTE4MGQwMTAzZWRkYjIyYmY1MTAwYjViMTdhZmUyZGNmN2Y1ZTkxYzg0Yzc4Y2Y1ZiIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiOTg3MGE1MGI0M2VjNGFlZDBlMGQ1ZTRiMzBjYTc3YmU3NDdkMzNhMWIxYmE0NmY3Mjg4MzM1YjZjNmRjMjdhMCIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiZDIxMTE4MGQzZDhjMWQ3NGU0NDNmNjNkYThhYjg0N2NlZGIzNjA5MzBlMzhhYTA5YTkwNGU0ODg1OTg2MWZjMSJ9"
         }
       ]
     },
     {
       "id": "claim_1814788a665c474fbe9623328521c079",
-      "statement": "The suite contains 186 test functions: autoreply 77, config 26, ai 21, whatsapp 26, history 14, ui 11, app 11.",
+      "statement": "The suite contains 214 test functions: autoreply 77, config 32, whatsapp 27, ai 25, history 16, ui 14, app 14, web 9.",
       "evidence": [
         {
-          "resource": "repo://src/ai.rs#L452-L739",
-          "version": "repo-lines-v1:sha256:e254a022732c78d25dae9d23c510a1774b4fbfb9d38ddffa796cb853daecbd13:eyJzZWxlY3RlZExpbmVDb3VudCI6Mjg4LCJmaXJzdFNlbGVjdGVkTGluZUhhc2giOiIyNWNmNWZjMjNmMmE2ZDIzYzNlYTUxZTcyZjVlODk2Yzc1NTVmYmFhMDVjZDViMWVhMTgyYmY5NjE4YThlODFiIiwibGFzdFNlbGVjdGVkTGluZUhhc2giOiI0MTJjYTM0NWNjZjc1YmY5YzA4MDZiY2U2OTViZThkZTgwOGI3OTk4NDI1MWE3YTU0ZDIwMmNmNjEwMWRkNDUxIiwicHJlY2VkaW5nQ29udGV4dExpbmVDb3VudCI6MywicHJlY2VkaW5nQ29udGV4dEhhc2giOiJlZGM4MzEyNDk2NjE0YzZjOWUxMTQ1NzRhNzlkOTE4NTcwMzRiYmUzYjMwOTkyZjI3NjBmNzE2NjRmMDA1YzY5IiwiZm9sbG93aW5nQ29udGV4dExpbmVDb3VudCI6MCwiZm9sbG93aW5nQ29udGV4dEhhc2giOiJlM2IwYzQ0Mjk4ZmMxYzE0OWFmYmY0Yzg5OTZmYjkyNDI3YWU0MWU0NjQ5YjkzNGNhNDk1OTkxYjc4NTJiODU1In0"
+          "resource": "repo://src/ai.rs#L526-L850",
+          "version": "repo-lines-v1:sha256:39880d0b9281bc470c8334739ad41bf46e3b1b911fc7af86d57c688a64e7e335:eyJzZWxlY3RlZExpbmVDb3VudCI6MzI1LCJmaXJzdFNlbGVjdGVkTGluZUhhc2giOiIyNWNmNWZjMjNmMmE2ZDIzYzNlYTUxZTcyZjVlODk2Yzc1NTVmYmFhMDVjZDViMWVhMTgyYmY5NjE4YThlODFiIiwibGFzdFNlbGVjdGVkTGluZUhhc2giOiI0MTJjYTM0NWNjZjc1YmY5YzA4MDZiY2U2OTViZThkZTgwOGI3OTk4NDI1MWE3YTU0ZDIwMmNmNjEwMWRkNDUxIiwicHJlY2VkaW5nQ29udGV4dExpbmVDb3VudCI6MywicHJlY2VkaW5nQ29udGV4dEhhc2giOiJlZGM4MzEyNDk2NjE0YzZjOWUxMTQ1NzRhNzlkOTE4NTcwMzRiYmUzYjMwOTkyZjI3NjBmNzE2NjRmMDA1YzY5IiwiZm9sbG93aW5nQ29udGV4dExpbmVDb3VudCI6MCwiZm9sbG93aW5nQ29udGV4dEhhc2giOiJlM2IwYzQ0Mjk4ZmMxYzE0OWFmYmY0Yzg5OTZmYjkyNDI3YWU0MWU0NjQ5YjkzNGNhNDk1OTkxYjc4NTJiODU1In0"
         },
         {
-          "resource": "repo://src/app.rs#L909-L991",
-          "version": "repo-lines-v1:sha256:4a0fbb1da628ff5042a7a671a6c6213927205ca89bf5684b7020d87693f81bcc:eyJzZWxlY3RlZExpbmVDb3VudCI6ODMsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjI1Y2Y1ZmMyM2YyYTZkMjNjM2VhNTFlNzJmNWU4OTZjNzU1NWZiYWEwNWNkNWIxZWExODJiZjk2MThhOGU4MWIiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImY2OTRmNjYyZmFiMjMxZThkOGRiMDk0YjU1Nzk4MTVmMDk5ZGE0MGRlN2U4Yjk2NWFlMDQ1NmFkYzA0OWIzODYiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjowLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImUzYjBjNDQyOThmYzFjMTQ5YWZiZjRjODk5NmZiOTI0MjdhZTQxZTQ2NDliOTM0Y2E0OTU5OTFiNzg1MmI4NTUifQ"
+          "resource": "repo://src/app.rs#L1167-L1295",
+          "version": "repo-lines-v1:sha256:bc3ce2ecf6be61a9ab36898b788851fb831cf4bcc6aebdb1206ec8df826e9505:eyJzZWxlY3RlZExpbmVDb3VudCI6MTI5LCJmaXJzdFNlbGVjdGVkTGluZUhhc2giOiIyNWNmNWZjMjNmMmE2ZDIzYzNlYTUxZTcyZjVlODk2Yzc1NTVmYmFhMDVjZDViMWVhMTgyYmY5NjE4YThlODFiIiwibGFzdFNlbGVjdGVkTGluZUhhc2giOiI0MTJjYTM0NWNjZjc1YmY5YzA4MDZiY2U2OTViZThkZTgwOGI3OTk4NDI1MWE3YTU0ZDIwMmNmNjEwMWRkNDUxIiwicHJlY2VkaW5nQ29udGV4dExpbmVDb3VudCI6MywicHJlY2VkaW5nQ29udGV4dEhhc2giOiJmNjk0ZjY2MmZhYjIzMWU4ZDhkYjA5NGI1NTc5ODE1ZjA5OWRhNDBkZTdlOGI5NjVhZTA0NTZhZGMwNDliMzg2IiwiZm9sbG93aW5nQ29udGV4dExpbmVDb3VudCI6MCwiZm9sbG93aW5nQ29udGV4dEhhc2giOiJlM2IwYzQ0Mjk4ZmMxYzE0OWFmYmY0Yzg5OTZmYjkyNDI3YWU0MWU0NjQ5YjkzNGNhNDk1OTkxYjc4NTJiODU1In0"
         },
         {
           "resource": "repo://src/autoreply.rs#L528-L1279",
           "version": "repo-lines-v1:sha256:d591faa1c1e584a76e3e96176666068ce2c59a3d79328f8ff996a71b0abcdd6c:eyJzZWxlY3RlZExpbmVDb3VudCI6NzUyLCJmaXJzdFNlbGVjdGVkTGluZUhhc2giOiIyNWNmNWZjMjNmMmE2ZDIzYzNlYTUxZTcyZjVlODk2Yzc1NTVmYmFhMDVjZDViMWVhMTgyYmY5NjE4YThlODFiIiwibGFzdFNlbGVjdGVkTGluZUhhc2giOiI0MTJjYTM0NWNjZjc1YmY5YzA4MDZiY2U2OTViZThkZTgwOGI3OTk4NDI1MWE3YTU0ZDIwMmNmNjEwMWRkNDUxIiwicHJlY2VkaW5nQ29udGV4dExpbmVDb3VudCI6MywicHJlY2VkaW5nQ29udGV4dEhhc2giOiI5MzJmOWYwN2VkNDRlNzRmOWI3ZjVkYTM1NjU2MjFmMTE0ZGI3N2VkZjU0NzY3NmM2NmI3NGMxYzA3NDM3YzIxIiwiZm9sbG93aW5nQ29udGV4dExpbmVDb3VudCI6MCwiZm9sbG93aW5nQ29udGV4dEhhc2giOiJlM2IwYzQ0Mjk4ZmMxYzE0OWFmYmY0Yzg5OTZmYjkyNDI3YWU0MWU0NjQ5YjkzNGNhNDk1OTkxYjc4NTJiODU1In0"
         },
         {
-          "resource": "repo://src/config.rs#L288-L560",
-          "version": "repo-lines-v1:sha256:bb7c0d6ce30753799a7f630a7a851592b55f1a7bd8df4a3542575d07593c30bb:eyJzZWxlY3RlZExpbmVDb3VudCI6MjczLCJmaXJzdFNlbGVjdGVkTGluZUhhc2giOiIyNWNmNWZjMjNmMmE2ZDIzYzNlYTUxZTcyZjVlODk2Yzc1NTVmYmFhMDVjZDViMWVhMTgyYmY5NjE4YThlODFiIiwibGFzdFNlbGVjdGVkTGluZUhhc2giOiI0MTJjYTM0NWNjZjc1YmY5YzA4MDZiY2U2OTViZThkZTgwOGI3OTk4NDI1MWE3YTU0ZDIwMmNmNjEwMWRkNDUxIiwicHJlY2VkaW5nQ29udGV4dExpbmVDb3VudCI6MywicHJlY2VkaW5nQ29udGV4dEhhc2giOiIxOWU3NjM1NWNmMTE5OTQ4MGMzZjk5ODM2YjE0ODZjODQxZDg5MWZhYWQ5MDRjNjA4ZTA5MzAyMmQzNzU3YWVlIiwiZm9sbG93aW5nQ29udGV4dExpbmVDb3VudCI6MCwiZm9sbG93aW5nQ29udGV4dEhhc2giOiJlM2IwYzQ0Mjk4ZmMxYzE0OWFmYmY0Yzg5OTZmYjkyNDI3YWU0MWU0NjQ5YjkzNGNhNDk1OTkxYjc4NTJiODU1In0"
+          "resource": "repo://src/config.rs#L323-L636",
+          "version": "repo-lines-v1:sha256:bceb8ba81c1ab6dfce9ecafd1fcf9cc1260d95a063990abea051a038a91cdec8:eyJzZWxlY3RlZExpbmVDb3VudCI6MzE0LCJmaXJzdFNlbGVjdGVkTGluZUhhc2giOiIyNWNmNWZjMjNmMmE2ZDIzYzNlYTUxZTcyZjVlODk2Yzc1NTVmYmFhMDVjZDViMWVhMTgyYmY5NjE4YThlODFiIiwibGFzdFNlbGVjdGVkTGluZUhhc2giOiI0MTJjYTM0NWNjZjc1YmY5YzA4MDZiY2U2OTViZThkZTgwOGI3OTk4NDI1MWE3YTU0ZDIwMmNmNjEwMWRkNDUxIiwicHJlY2VkaW5nQ29udGV4dExpbmVDb3VudCI6MywicHJlY2VkaW5nQ29udGV4dEhhc2giOiIxOWU3NjM1NWNmMTE5OTQ4MGMzZjk5ODM2YjE0ODZjODQxZDg5MWZhYWQ5MDRjNjA4ZTA5MzAyMmQzNzU3YWVlIiwiZm9sbG93aW5nQ29udGV4dExpbmVDb3VudCI6MCwiZm9sbG93aW5nQ29udGV4dEhhc2giOiJlM2IwYzQ0Mjk4ZmMxYzE0OWFmYmY0Yzg5OTZmYjkyNDI3YWU0MWU0NjQ5YjkzNGNhNDk1OTkxYjc4NTJiODU1In0"
         },
         {
-          "resource": "repo://src/history.rs#L418-L663",
-          "version": "repo-lines-v1:sha256:cc837e674ada6149be57f022f902b8439eba315c59ac874e7d65bf28f1e75d02:eyJzZWxlY3RlZExpbmVDb3VudCI6MjQ2LCJmaXJzdFNlbGVjdGVkTGluZUhhc2giOiIyNWNmNWZjMjNmMmE2ZDIzYzNlYTUxZTcyZjVlODk2Yzc1NTVmYmFhMDVjZDViMWVhMTgyYmY5NjE4YThlODFiIiwibGFzdFNlbGVjdGVkTGluZUhhc2giOiI0MTJjYTM0NWNjZjc1YmY5YzA4MDZiY2U2OTViZThkZTgwOGI3OTk4NDI1MWE3YTU0ZDIwMmNmNjEwMWRkNDUxIiwicHJlY2VkaW5nQ29udGV4dExpbmVDb3VudCI6MywicHJlY2VkaW5nQ29udGV4dEhhc2giOiI2MDYzYzRkMWFmOGNkODliYjBiZGQ3NjU3ZmZjYmY2ODA2YTYxMGQ3OWQ2YzhkNGMzMDdjMjQxZGYzOTAwNTIyIiwiZm9sbG93aW5nQ29udGV4dExpbmVDb3VudCI6MCwiZm9sbG93aW5nQ29udGV4dEhhc2giOiJlM2IwYzQ0Mjk4ZmMxYzE0OWFmYmY0Yzg5OTZmYjkyNDI3YWU0MWU0NjQ5YjkzNGNhNDk1OTkxYjc4NTJiODU1In0"
+          "resource": "repo://src/history.rs#L481-L763",
+          "version": "repo-lines-v1:sha256:1d69fc86c05572c4af6df08c56af463f1a0de79555132c853c3e8b7c07eab17d:eyJzZWxlY3RlZExpbmVDb3VudCI6MjgzLCJmaXJzdFNlbGVjdGVkTGluZUhhc2giOiIyNWNmNWZjMjNmMmE2ZDIzYzNlYTUxZTcyZjVlODk2Yzc1NTVmYmFhMDVjZDViMWVhMTgyYmY5NjE4YThlODFiIiwibGFzdFNlbGVjdGVkTGluZUhhc2giOiI0MTJjYTM0NWNjZjc1YmY5YzA4MDZiY2U2OTViZThkZTgwOGI3OTk4NDI1MWE3YTU0ZDIwMmNmNjEwMWRkNDUxIiwicHJlY2VkaW5nQ29udGV4dExpbmVDb3VudCI6MywicHJlY2VkaW5nQ29udGV4dEhhc2giOiI2MDYzYzRkMWFmOGNkODliYjBiZGQ3NjU3ZmZjYmY2ODA2YTYxMGQ3OWQ2YzhkNGMzMDdjMjQxZGYzOTAwNTIyIiwiZm9sbG93aW5nQ29udGV4dExpbmVDb3VudCI6MCwiZm9sbG93aW5nQ29udGV4dEhhc2giOiJlM2IwYzQ0Mjk4ZmMxYzE0OWFmYmY0Yzg5OTZmYjkyNDI3YWU0MWU0NjQ5YjkzNGNhNDk1OTkxYjc4NTJiODU1In0"
         },
         {
-          "resource": "repo://src/ui.rs#L508-L636",
-          "version": "repo-lines-v1:sha256:a947a4eef06a94fd07b233edf634d8c588f385a462e9ca30f6ca08335c081a4a:eyJzZWxlY3RlZExpbmVDb3VudCI6MTI5LCJmaXJzdFNlbGVjdGVkTGluZUhhc2giOiIyNWNmNWZjMjNmMmE2ZDIzYzNlYTUxZTcyZjVlODk2Yzc1NTVmYmFhMDVjZDViMWVhMTgyYmY5NjE4YThlODFiIiwibGFzdFNlbGVjdGVkTGluZUhhc2giOiI0MTJjYTM0NWNjZjc1YmY5YzA4MDZiY2U2OTViZThkZTgwOGI3OTk4NDI1MWE3YTU0ZDIwMmNmNjEwMWRkNDUxIiwicHJlY2VkaW5nQ29udGV4dExpbmVDb3VudCI6MywicHJlY2VkaW5nQ29udGV4dEhhc2giOiIzNWIyMTY4NzNmZmY2YTA1NzcwNTk1YzU5Y2FkM2U2YTk3MzUyYmI0MTdkNjBjNzYyZWE0MzgxZjE5OTAyODc5IiwiZm9sbG93aW5nQ29udGV4dExpbmVDb3VudCI6MCwiZm9sbG93aW5nQ29udGV4dEhhc2giOiJlM2IwYzQ0Mjk4ZmMxYzE0OWFmYmY0Yzg5OTZmYjkyNDI3YWU0MWU0NjQ5YjkzNGNhNDk1OTkxYjc4NTJiODU1In0"
+          "resource": "repo://src/ui.rs#L558-L730",
+          "version": "repo-lines-v1:sha256:2bb51360845dc96ea63e9ba03c980bf2957a86649ecdd32efa1bcf97463d56b1:eyJzZWxlY3RlZExpbmVDb3VudCI6MTczLCJmaXJzdFNlbGVjdGVkTGluZUhhc2giOiIyNWNmNWZjMjNmMmE2ZDIzYzNlYTUxZTcyZjVlODk2Yzc1NTVmYmFhMDVjZDViMWVhMTgyYmY5NjE4YThlODFiIiwibGFzdFNlbGVjdGVkTGluZUhhc2giOiI0MTJjYTM0NWNjZjc1YmY5YzA4MDZiY2U2OTViZThkZTgwOGI3OTk4NDI1MWE3YTU0ZDIwMmNmNjEwMWRkNDUxIiwicHJlY2VkaW5nQ29udGV4dExpbmVDb3VudCI6MywicHJlY2VkaW5nQ29udGV4dEhhc2giOiI5YTc1YTI2ZGRjNmU4NmI3YjZjZjliNWFjNTAwZjBiNWM4ZmM5MWRmZWE4Yjc1YjdkN2NiMDhjYjY3N2ZkOThmIiwiZm9sbG93aW5nQ29udGV4dExpbmVDb3VudCI6MCwiZm9sbG93aW5nQ29udGV4dEhhc2giOiJlM2IwYzQ0Mjk4ZmMxYzE0OWFmYmY0Yzg5OTZmYjkyNDI3YWU0MWU0NjQ5YjkzNGNhNDk1OTkxYjc4NTJiODU1In0"
         },
         {
-          "resource": "repo://src/whatsapp.rs#L990-L1282",
-          "version": "repo-lines-v1:sha256:437f8e30ee4af86204f689c841fec9dece4acbf9ee78697a07f0edfd7469914d:eyJzZWxlY3RlZExpbmVDb3VudCI6MjkzLCJmaXJzdFNlbGVjdGVkTGluZUhhc2giOiIyNWNmNWZjMjNmMmE2ZDIzYzNlYTUxZTcyZjVlODk2Yzc1NTVmYmFhMDVjZDViMWVhMTgyYmY5NjE4YThlODFiIiwibGFzdFNlbGVjdGVkTGluZUhhc2giOiI0MTJjYTM0NWNjZjc1YmY5YzA4MDZiY2U2OTViZThkZTgwOGI3OTk4NDI1MWE3YTU0ZDIwMmNmNjEwMWRkNDUxIiwicHJlY2VkaW5nQ29udGV4dExpbmVDb3VudCI6MywicHJlY2VkaW5nQ29udGV4dEhhc2giOiJkNmQxM2RhNTNkZGY1NGUwOWE5ZmIzNzc4NzQxNGE5NjE0NmQ0NzFjNWViYzZjZTI2Y2Y5YTM4NGYxODc3OGYxIiwiZm9sbG93aW5nQ29udGV4dExpbmVDb3VudCI6MCwiZm9sbG93aW5nQ29udGV4dEhhc2giOiJlM2IwYzQ0Mjk4ZmMxYzE0OWFmYmY0Yzg5OTZmYjkyNDI3YWU0MWU0NjQ5YjkzNGNhNDk1OTkxYjc4NTJiODU1In0"
+          "resource": "repo://src/web.rs#L366-L445",
+          "version": "repo-lines-v1:sha256:932c4dcc02d7a1f510bc2e787ba8755c60639a9c77352797a663dc6ac0e063f3:eyJzZWxlY3RlZExpbmVDb3VudCI6ODAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjI1Y2Y1ZmMyM2YyYTZkMjNjM2VhNTFlNzJmNWU4OTZjNzU1NWZiYWEwNWNkNWIxZWExODJiZjk2MThhOGU4MWIiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjhmZDM2OGIzODI2OTRjMDYxZWMzMDkyOTYzNjNkYTBlYWNkNzI3NDlmZTQzYmFiNTM4ZWMyMjlhMmM5MDE0NTkiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjowLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImUzYjBjNDQyOThmYzFjMTQ5YWZiZjRjODk5NmZiOTI0MjdhZTQxZTQ2NDliOTM0Y2E0OTU5OTFiNzg1MmI4NTUifQ"
+        },
+        {
+          "resource": "repo://src/whatsapp.rs#L1005-L1303",
+          "version": "repo-lines-v1:sha256:c2be861e5deb66fd99846a5a0dc87032f6f1f6384c37d39d688c3f13c9427c63:eyJzZWxlY3RlZExpbmVDb3VudCI6Mjk5LCJmaXJzdFNlbGVjdGVkTGluZUhhc2giOiIyNWNmNWZjMjNmMmE2ZDIzYzNlYTUxZTcyZjVlODk2Yzc1NTVmYmFhMDVjZDViMWVhMTgyYmY5NjE4YThlODFiIiwibGFzdFNlbGVjdGVkTGluZUhhc2giOiI0MTJjYTM0NWNjZjc1YmY5YzA4MDZiY2U2OTViZThkZTgwOGI3OTk4NDI1MWE3YTU0ZDIwMmNmNjEwMWRkNDUxIiwicHJlY2VkaW5nQ29udGV4dExpbmVDb3VudCI6MywicHJlY2VkaW5nQ29udGV4dEhhc2giOiJkNmQxM2RhNTNkZGY1NGUwOWE5ZmIzNzc4NzQxNGE5NjE0NmQ0NzFjNWViYzZjZTI2Y2Y5YTM4NGYxODc3OGYxIiwiZm9sbG93aW5nQ29udGV4dExpbmVDb3VudCI6MCwiZm9sbG93aW5nQ29udGV4dEhhc2giOiJlM2IwYzQ0Mjk4ZmMxYzE0OWFmYmY0Yzg5OTZmYjkyNDI3YWU0MWU0NjQ5YjkzNGNhNDk1OTkxYjc4NTJiODU1In0"
         }
       ]
     },
@@ -129,14 +133,24 @@
       "statement": "AI tests check log records and append-only writes, using a per-process temporary file for the append test.",
       "evidence": [
         {
-          "resource": "repo://src/ai.rs#L687-L731",
+          "resource": "repo://src/ai.rs#L768-L812",
           "version": "repo-lines-v1:sha256:72ae9920c7dd147c055f69fff83f0efcaa72533a47bee96c58a159af5688aa90:eyJzZWxlY3RlZExpbmVDb3VudCI6NDUsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjBjMzZlMGI1ZTQxNmY4NDQ1N2EzODNjZGNlNDU0Y2QzY2E2NmQ5OTFlMzRjOTlmZDQ4NzQ2NzIxZTA4ZmRjZTkiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImEwMGRiMDQwZjI5MDliZjk3OGZmMGE5NTZhNTVlODNjNjUzNDE4ZGU2YmI2OGY4MDQxZGE5NzZmOGQzZDIyNzAiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImRmMzliNWQwMzM4NGE5NWU4MjgyY2FiNWM0NDY3ZGFkZTM2OWRjNjJlOTVkMDZmMTNhYzgyOTkwMjRhZDE5YmQifQ"
         }
       ]
+    },
+    {
+      "id": "claim_51223006b05b4b7f8d976e63537b9e22",
+      "statement": "web tests cover query stripping, route resolution, the X-Wh-Request header check, which routes require it, and request body trimming, rejection and size limit.",
+      "evidence": [
+        {
+          "resource": "repo://src/web.rs#L366-L445",
+          "version": "repo-lines-v1:sha256:932c4dcc02d7a1f510bc2e787ba8755c60639a9c77352797a663dc6ac0e063f3:eyJzZWxlY3RlZExpbmVDb3VudCI6ODAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjI1Y2Y1ZmMyM2YyYTZkMjNjM2VhNTFlNzJmNWU4OTZjNzU1NWZiYWEwNWNkNWIxZWExODJiZjk2MThhOGU4MWIiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjhmZDM2OGIzODI2OTRjMDYxZWMzMDkyOTYzNjNkYTBlYWNkNzI3NDlmZTQzYmFiNTM4ZWMyMjlhMmM5MDE0NTkiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjowLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImUzYjBjNDQyOThmYzFjMTQ5YWZiZjRjODk5NmZiOTI0MjdhZTQxZTQ2NDliOTM0Y2E0OTU5OTFiNzg1MmI4NTUifQ"
+        }
+      ]
     }
   ],
   "verification": {
     "by": "openwiki/0.7.2",
-    "at": "2026-10-19T19:53:56.480Z"
+    "at": "2026-10-20T20:27:06.515Z"
   }
 }

+ 56 - 28
openwiki/.claims/workflows/ai-autoreply.json

@@ -1,6 +1,6 @@
 {
   "schemaVersion": 1,
-  "pageVersion": "sha256:eb01f5f500741db0c4fe6150c60f34a21f09961bad77eaf7779a0075d6a2ee6d",
+  "pageVersion": "sha256:ccc0e095054a5eb2aa06cbab3cb29b6a721ba01910ea260ce580279a25de25d8",
   "claims": [
     {
       "id": "claim_4abb79458e064f78a8d565a7069cb356",
@@ -14,15 +14,15 @@
     },
     {
       "id": "claim_bd1031863e03434ea4649d5866821b34",
-      "statement": "Enabling autoreply requires a configured AI and a private chat or group, while disabling is always allowed, and the flag is persisted.",
+      "statement": "Enabling a chat's autoreply requires a configured AI and a private chat or group, while disabling is always allowed, and the flag is persisted.",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L605-L624",
-          "version": "repo-lines-v1:sha256:51d6d6426fedb162314e30c78add489199bf678d7b5030f289a017684c6415ef:eyJzZWxlY3RlZExpbmVDb3VudCI6MjAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjRlZmZlOWM3MmQ5ZTYzOGI1NTYwMjBhM2VjODIzOGM1ZDJlMzk0ZTdkMjk5ZjFlNzE2ZmQyYjFkZGU1ZDQ3MGEiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjkzNzNhMTgxNzI2YzQ3ZjdjNzlmOTI3ZDI2NzhhYzQ3NjYzY2VhMzI1OGYxOGEwNTM0ZTg2NDVlYmFlOTlkMGUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImU2ZWQ1MmZlYjhjZTliMjNiYmNiYTkwMGFkYjk0MzE3MjA4ODNhZTk0ODY4NmQyNjhlNmRkMjFlNjljYTU3YjkifQ"
+          "resource": "repo://src/app.rs#L1127-L1140",
+          "version": "repo-lines-v1:sha256:76ea2311dd543d33bf1c52b4cee8997184cb67643a8ba3b2f715abf2fea34667:eyJzZWxlY3RlZExpbmVDb3VudCI6MTQsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjA0NDI0MGFiOGNkMTM1NDE2ZTQ5MjMyYTEzMGIzYzlkYmI2NWZmYzg0OGNjYzY3OWJhYTQxYWQzNjY4ZTA3MjQiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjE5ZTc2MzU1Y2YxMTk5NDgwYzNmOTk4MzZiMTQ4NmM4NDFkODkxZmFhZDkwNGM2MDhlMDkzMDIyZDM3NTdhZWUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjQ0MDNkOWZmNDhiN2ZiNmQ3MjNkZTVjOWIzMTg5MzEzZTI0NjcwYTFhOTYyYWFlMWU2MTg0YTI5ZWZjNzFjNWQifQ"
         },
         {
-          "resource": "repo://src/app.rs#L885-L898",
-          "version": "repo-lines-v1:sha256:76ea2311dd543d33bf1c52b4cee8997184cb67643a8ba3b2f715abf2fea34667:eyJzZWxlY3RlZExpbmVDb3VudCI6MTQsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjA0NDI0MGFiOGNkMTM1NDE2ZTQ5MjMyYTEzMGIzYzlkYmI2NWZmYzg0OGNjYzY3OWJhYTQxYWQzNjY4ZTA3MjQiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjE5ZTc2MzU1Y2YxMTk5NDgwYzNmOTk4MzZiMTQ4NmM4NDFkODkxZmFhZDkwNGM2MDhlMDkzMDIyZDM3NTdhZWUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6Ijg5NGUwY2FkODFjNTBiOWY4MWE5ODI2ZjA1YzUyZjY0MGE0ZGM3NzdiYWQyNGM2MGEwMjQ5NzhlZWJmMWRjMGMifQ"
+          "resource": "repo://src/app.rs#L763-L794",
+          "version": "repo-lines-v1:sha256:c4358a68adc4872cb86ae5a6cf9f5e527660022add8d3e68770933ca4f7e6ea6:eyJzZWxlY3RlZExpbmVDb3VudCI6MzIsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImEzYTUwNTFjOTU0MGU0MGQ2ZDM4ZTgzMWIzNTIxZWM3MWRiN2M5N2ZjN2IwYzRiZWQ1ZDlmYzY1YWVjMjBkNjkiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjkzNzNhMTgxNzI2YzQ3ZjdjNzlmOTI3ZDI2NzhhYzQ3NjYzY2VhMzI1OGYxOGEwNTM0ZTg2NDVlYmFlOTlkMGUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImUzZWNmNGIzNzRlN2M0ODFlZjUzODQ1Y2Y1YjlmNDI5MDk3NmQxYTlmNDlkOWY5NWJjNjIxNWEyY2VhMDYyOTAifQ"
         }
       ]
     },
@@ -38,14 +38,18 @@
     },
     {
       "id": "claim_01e8cc3ee34a49b6a579c25728a8efa1",
-      "statement": "Only newly saved messages reach maybe_autoreply, which requires an AI client and system prompt and receives the addressed_to JIDs.",
+      "statement": "Only newly saved messages reach maybe_autoreply, which requires an AI client and system prompt, treats the chat as enabled when the global or per-chat autoreply is on, and receives the addressed_to JIDs.",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L267-L315",
-          "version": "repo-lines-v1:sha256:ca8f7a75fdaf722cb92f44a51cbdf6cb156b724100673fe626d55a064d5c4af3:eyJzZWxlY3RlZExpbmVDb3VudCI6NDksImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6Ijg4ZTRiZTg5ZDNlZDdkYzY5MGQxOGMyN2NmY2I1MmExYWFjNGI5NmZmZDU0ZmFjOTIzOTczZmQxY2Q3ZGNhNWEiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjE2MmVlNTdlNDFhOGMyMGFlMDE2NGRmMjYxNDM5ZGNjNDk5ZDM1MjA4MzQxNTE1MTZkMDQ3ZThkZWFlOGE0NjYiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6Ijk0NDE2M2IzN2JlYTdlZjFjNmJiMDZkODAzMTQxOGRmZjZjNTBlNjgzMmUyMWVkZTI2ZjlhN2VjODA5MDlhMTIifQ"
+          "resource": "repo://src/app.rs#L304-L346",
+          "version": "repo-lines-v1:sha256:155b5cdb56d090f18143cef6a1591677f8c4bce133e1ab20f4e42b6361ece72f:eyJzZWxlY3RlZExpbmVDb3VudCI6NDMsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjYyNjcxZjMwOGZhYjk0MTMwNzgwNDU4MzFhNDMxYjJjZTM3MzhhMTEyMzMyNDg4OGY2ZTgzNTY1ODhjYjNmOTciLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImQ1Zjg3MzRjYWZkNWU0MTNiNTUyZDMzZjFmMThlOTEyNWI2YTkwMTFhODU2OTM2YmJjMzRmMzAxMGM4Yzg1ZjkiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6Ijk0NDE2M2IzN2JlYTdlZjFjNmJiMDZkODAzMTQxOGRmZjZjNTBlNjgzMmUyMWVkZTI2ZjlhN2VjODA5MDlhMTIifQ"
         },
         {
-          "resource": "repo://src/app.rs#L389-L395",
+          "resource": "repo://src/app.rs#L366-L372",
+          "version": "repo-lines-v1:sha256:9190cc2d950eb45e132eae01c1ae01b5fb4af82470bb42fae8b4e0227a315710:eyJzZWxlY3RlZExpbmVDb3VudCI6NywiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiYmMzZGZhMjZjNzdiNTNkZDcyZWZmZGI1MmEyNWFlNGY5YzMwNTdiZTk2NWI0MzRlMTMyOWIwMTM2MGZmNGViNyIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDg5M2Y0MGUzNzgxZWI4OTZjODJkMDQ2YTA4MDY3YTExZTM1Y2RmMmYxZGZhZjI2ZmVhOTBiOGRiNjY3NjQxMyIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiNjM0ODk4ZWNhYTE1OWQ3NDg1NDNjOTU0Y2NkNTAxZGNiMmRjZGFlMzE4YTk2Y2JhNDE3NWNmNTM0YTkzYjU5MSIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiMjIwZTI2OTMzMWFlNTY1MmJhMzE1ZWJhN2ZjNGU1YzE1YWE0OWEzN2IyMTY0Zjc2NGViY2JhMDU4MTlkYzI0MCJ9"
+        },
+        {
+          "resource": "repo://src/app.rs#L430-L436",
           "version": "repo-lines-v1:sha256:8954f8190e201652fd68eb454a728e5e4173257eddebb901b8a0b8d4c773564e:eyJzZWxlY3RlZExpbmVDb3VudCI6NywiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiODA1ZTgyYjdjMjlkNzVkZDI3YmZhYTZiYjkxMzZiOTMwMjQyYWEzYjJkNmM5NjI4ZWVmMmY3ZjIxNzdlMzhhNiIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDg5M2Y0MGUzNzgxZWI4OTZjODJkMDQ2YTA4MDY3YTExZTM1Y2RmMmYxZGZhZjI2ZmVhOTBiOGRiNjY3NjQxMyIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiMjRmNGQ3NzU2OTA1OWI4NjMyNGU2YTVmNGY2YWU4YWI4NjM4YmUzNTY5YjM5MjRlZDJkOWJiZjY0MzczMjUyNCIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiZTRhNDZkM2QxYTE0NjM2YTRmZGVmODJkZWRhMWZlM2UzMjgzYjA3ZTBlMzc1ZWU5NGQyODIwZDRiYzI5MzFmNCJ9"
         }
       ]
@@ -87,7 +91,7 @@
       "statement": "record_reply is invoked in start_reply before the AI request is spawned and clears any pending deferral of the chat.",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L397-L416",
+          "resource": "repo://src/app.rs#L438-L457",
           "version": "repo-lines-v1:sha256:77c613038db86228ee90623150e68a8e459a6add92955e4833a1ac3e5f863adc:eyJzZWxlY3RlZExpbmVDb3VudCI6MjAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImU0YTg3YjlhYzc5ZmRlZWFlMjkyODk0ZTMxNDQ3NzU4ZWQwYjdiNTFlZmMwMjJkN2YxNDhlZTU5MTY1MDY2N2QiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjFiYWU5M2RkYzVmMDAxZTlkYTEyOWJmNDgwOTM0NzBiNWVkOGYzYzc3Y2MwNjI0NmIyY2U5MTg3ZjEyMzRiNGQiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjA2ZWViM2RhYjZhYTAxODdiMjY5ZjlmNGY1NjUyODE3ZTljNDE0ZTQzZjQ1Nzc5OWY4YWJlMjliMjhiM2IzZmQifQ"
         },
         {
@@ -115,22 +119,22 @@
       "statement": "With an image and a vision model one task downloads the image, tries the vision completion and falls back to a text completion, combining errors if both fail.",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L418-L449",
+          "resource": "repo://src/app.rs#L459-L490",
           "version": "repo-lines-v1:sha256:885842d6f63043fbcf5c591add81a58c3216ed176aadc89bb23c76042527af03:eyJzZWxlY3RlZExpbmVDb3VudCI6MzIsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImQ2Njg5ODM3ODc3YzUxMGVhYWU0ZDE5YmFhMjkxYWI0NmQ2ZDhjMGEzMGU0M2I5MjViNDNiY2I4NTc4ZTdlMDQiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjE2MmVlNTdlNDFhOGMyMGFlMDE2NGRmMjYxNDM5ZGNjNDk5ZDM1MjA4MzQxNTE1MTZkMDQ3ZThkZWFlOGE0NjYiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImQwMjI2ZGQ4YmZkY2JkMDhlNjUxMjUzOGY4MmU5YWNiOTEyNzg2NTgwNDEwMmQ2YjRlMzQ4NmRmMjU4YjMxNmYifQ"
         },
         {
-          "resource": "repo://src/app.rs#L781-L844",
+          "resource": "repo://src/app.rs#L1023-L1086",
           "version": "repo-lines-v1:sha256:c97db5fc22afa0f7a649b64cc9adfad1301d31a11a6024b3c54cf65fe46016df:eyJzZWxlY3RlZExpbmVDb3VudCI6NjQsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6Ijk0MTM1OGQzZmUwMGY3ZDM4YWM4MjNjMDc4OWNlZmU2NTNhMTE2YzkyZmNjNWMyYjYwNWY4ODA0MjA2MzRjYTAiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImQxZTM5MDcyMDkzN2I1OWI2MzAyZjNjZGUwY2Y5NzgyMTUxZjc2OTg0MDM4Yjc4ZjBiYzJmNjkxYjFmZTcwM2EiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImMwMDY1Njk0Y2MyNGUxNGQxNjdiNzI0YjgxNDZhNDIyYzI4MGUyNjE4MmNhNjFmMDVlMmQ2M2I1MTRiNTQxYjUifQ"
         }
       ]
     },
     {
       "id": "claim_045b183e6c6b444c84449e47227a98cb",
-      "statement": "AI replies are discarded if autoreply was disabled meanwhile, otherwise sent and stored with is_ai true.",
+      "statement": "AI replies are discarded if both the global and per-chat autoreply were disabled meanwhile, otherwise sent and stored with is_ai true.",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L465-L522",
-          "version": "repo-lines-v1:sha256:c3eaf2c14e923f56550d8c6b4a5e3496a2bf65fa25a0232776d085154bc05810:eyJzZWxlY3RlZExpbmVDb3VudCI6NTgsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImEwMWQxYjkzMjk5YjRiOTc3ZWNiOGUzMWE0MDhmMWUxZjczMjA5OGY3ZmI4MjMxYzllOTgwMmJhNmQzNmRlYzciLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjQxN2NmMGY3N2ZlYjJiMGFiOWViN2M5NWUyZmNhZjQ5Mzg3ZDYyMjk4ZmYyZGE2ZGYyZGQwYmRlMWQwMGE2OGYiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjYzYjQ0YTU2ODZkYjM0MjNlMDVmOGMxOWMwYmMyNzlmMDA1NTE4NDJmMzgwZjIxMTM1ZTkzMTZhY2IwNmQ5ODEifQ"
+          "resource": "repo://src/app.rs#L506-L564",
+          "version": "repo-lines-v1:sha256:20e1a027e2d853ac00e51605760fdd2bc3cb9bc4f976288e27576a8323a29479:eyJzZWxlY3RlZExpbmVDb3VudCI6NTksImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImEwMWQxYjkzMjk5YjRiOTc3ZWNiOGUzMWE0MDhmMWUxZjczMjA5OGY3ZmI4MjMxYzllOTgwMmJhNmQzNmRlYzciLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjQxN2NmMGY3N2ZlYjJiMGFiOWViN2M5NWUyZmNhZjQ5Mzg3ZDYyMjk4ZmYyZGE2ZGYyZGQwYmRlMWQwMGE2OGYiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjYzYjQ0YTU2ODZkYjM0MjNlMDVmOGMxOWMwYmMyNzlmMDA1NTE4NDJmMzgwZjIxMTM1ZTkzMTZhY2IwNmQ5ODEifQ"
         }
       ]
     },
@@ -139,8 +143,8 @@
       "statement": "Cooldowns come from cooldown_seconds and group_cooldown_seconds; group messages not addressed to the user use the group cooldown, while private chats and group messages that mention an own JID or quote an own message use the private cooldown.",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L165-L199",
-          "version": "repo-lines-v1:sha256:a25933c1c4dff0de5b2f69ae0e9d8a49c1940e8a4b0643b39cd8981e74a78bbd:eyJzZWxlY3RlZExpbmVDb3VudCI6MzUsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjdmNTYzZjU4ZTBlOWQyYjgxMjgwMTZhOWEyZGZhMTU2YjgzNTM0NWYzOTkzYTVhYjdkNjVhNWU4ODFmMDg0Y2UiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjBhOGJlZTA0NjIzODBmZjdmN2Y0ZDZhYzE2MGViODRmYjk3YjgwZGRjMjM2ZGFmZWY4NTQ4OTYxYjcwZDQ4ZmQiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImU3M2U3ZTFjNDIxZDk5NzQ5ZWVmNzNjNjJiNmE4ODAyODhjNGNhZDM4ZTYwZWQyMTNjNmM1ZDI1ZjAyMDU3OGMifQ"
+          "resource": "repo://src/app.rs#L191-L229",
+          "version": "repo-lines-v1:sha256:b8e7258ce42c14156856b5548408686e166ef6c02cf336560ffc75ccd7ecf145:eyJzZWxlY3RlZExpbmVDb3VudCI6MzksImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjdmNTYzZjU4ZTBlOWQyYjgxMjgwMTZhOWEyZGZhMTU2YjgzNTM0NWYzOTkzYTVhYjdkNjVhNWU4ODFmMDg0Y2UiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjBhOGJlZTA0NjIzODBmZjdmN2Y0ZDZhYzE2MGViODRmYjk3YjgwZGRjMjM2ZGFmZWY4NTQ4OTYxYjcwZDQ4ZmQiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImU3M2U3ZTFjNDIxZDk5NzQ5ZWVmNzNjNjJiNmE4ODAyODhjNGNhZDM4ZTYwZWQyMTNjNmM1ZDI1ZjAyMDU3OGMifQ"
         },
         {
           "resource": "repo://src/autoreply.rs#L322-L346",
@@ -154,14 +158,14 @@
     },
     {
       "id": "claim_aa50de18f27a4f1ba952016d6c32b9f0",
-      "statement": "In private chats a message blocked only by the cooldown marks the chat pending and a tokio timer emits CooldownElapsed, after which one text-only reply is sent if the deferral is still pending; groups never defer.",
+      "statement": "In private chats a message blocked only by the cooldown marks the chat pending and a tokio timer emits CooldownElapsed, after which one text-only reply is sent if the deferral is still pending and the chat is still enabled; groups never defer.",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L305-L332",
-          "version": "repo-lines-v1:sha256:91ec3385dbb5835ccce818fd691bc5017fa8ffaa0bf2e13d9e1bcc9b3720087c:eyJzZWxlY3RlZExpbmVDb3VudCI6MjgsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjgxZWU1NzQ0N2I0ZTUxM2M5MzIwZTY2ZmNjODc5NTUzZTMxYjY5ZjQ3ZWE4OGUzNGQwZGQ1MjM3YWRmM2NjODgiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImUxYzg5NTg5NTI1Njg3YTExOGM4NjA3NTY1MmM4ZjdlNGE1YWY3OTIyY2FmOGUzYjE0ZGE4MzAwYjM4ZmM0MGQiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjIyMGUyNjkzMzFhZTU2NTJiYTMxNWViYTdmYzRlNWMxNWFhNDlhMzdiMjE2NGY3NjRlYmNiYTA1ODE5ZGMyNDAifQ"
+          "resource": "repo://src/app.rs#L315-L364",
+          "version": "repo-lines-v1:sha256:0f24c79bfda4f961235e5abf567bb77dfafce7f2765ead440107588291772518:eyJzZWxlY3RlZExpbmVDb3VudCI6NTAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjMyMTY3MzlmZjBiNWNmNWE1YTZiNjY4NDAwY2RkNDZjMTY2MTQ3NjFmMGUzZmY4YzBjYWI0ZTkwMWM1ZTFiNjEiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImRhOWQ4MzUyMGI4MzFmNTE4YThlYzVjMTUzYmI1ZTE5ODgwNjI2ZWZmNjJhODRkNGVkMDkxNWNmMTNjMzI3NTgiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImNmMGY4ZWU3NGMyYmNhMjZlNTlmY2NmYjUzZmQyYWI2OGJjNzIwZTY2MGZiZjM3M2IwZjFhNTY4ZmIyNjBjOTUifQ"
         },
         {
-          "resource": "repo://src/app.rs#L744-L755",
+          "resource": "repo://src/app.rs#L986-L997",
           "version": "repo-lines-v1:sha256:c3506143ab1af95e5059073dbe2e9099d3c5115a9502bf0faa04a3d2578cf991:eyJzZWxlY3RlZExpbmVDb3VudCI6MTIsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImZiNzQ3YTE0ZDllZGE0MjlmN2RlOTA1M2UxYmMyNzMzODZhNDQyZWE4MDQzYzI0NWE5ZDQ1M2JmMjk2Y2I2NmIiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjRjNjg0ZTk4OWVmNzk4ODJiNmIyOTMwNDdiMTJiNDg4YjkwNjlhYmUzMzc2ZTVhNTNhOGEyZTI5YjY4ODcyMTAiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjA1ZDJlYmFjZThlMTE0NjAzZTYyZDc1YjVmNmRjODFjMzJiMzgzMmI4YjBiMGUzZDVhZDY0OTMyMDQ4YTZkMzgifQ"
         },
         {
@@ -172,15 +176,15 @@
     },
     {
       "id": "claim_d8b08d3eeb4d432f82bfd00abc49cbd2",
-      "statement": "On the first OfflineSyncCompleted per process, catch_up sends one text-only reply to each autoreply-enabled non-group chat whose last message is from the contact, older than app start and within catch_up_seconds, and reports the count in the status.",
+      "statement": "On the first OfflineSyncCompleted per process, catch_up sends one text-only reply to each enabled non-group chat (all chats when the global autoreply is on) whose last message is from the contact, older than app start and within catch_up_seconds, and reports the count in the status.",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L33-L33",
+          "resource": "repo://src/app.rs#L36-L36",
           "version": "repo-lines-v1:sha256:e4cb6140de212ef27ad95651543bae53cb782d2cd46e494db3057b17e46a2635:eyJzZWxlY3RlZExpbmVDb3VudCI6MSwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiZTRjYjYxNDBkZTIxMmVmMjdhZDk1NjUxNTQzYmFlNTNjYjc4MmQyY2Q0NmU0OTRkYjMwNTdiMTdlNDZhMjYzNSIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiZTRjYjYxNDBkZTIxMmVmMjdhZDk1NjUxNTQzYmFlNTNjYjc4MmQyY2Q0NmU0OTRkYjMwNTdiMTdlNDZhMjYzNSIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiOWRjMmZmMjdhMGM0NWJjOTE0NmMzMWI4ZTc1NWIzMjc3YjEyYTVjMjRkOGJiZGQ2N2Q2MWU2OTYwMWNjOWIxNSIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiZjY4OWU1OTZlNGI2YmY2YmM1M2M5NmMxYTdmMTk4MGY4ZDE1NTRhMDQ0NTAxMmI0ZDRjMDlhY2JmZjY2YmI3OSJ9"
         },
         {
-          "resource": "repo://src/app.rs#L334-L387",
-          "version": "repo-lines-v1:sha256:7b732bdff31637aad1a767205e928e6c5e8dbef03947ccae737a2e10683ad0dc:eyJzZWxlY3RlZExpbmVDb3VudCI6NTQsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImM5ZmJjMjYxZDk1MDVkNTBjZTU1MWY4ZDI4OTQ2MTZjMWM2MGY3NTlhZTAwYTE1OWY4Yzg3OGVmMWJjM2Y2ZWEiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjYzNDg5OGVjYWExNTlkNzQ4NTQzYzk1NGNjZDUwMWRjYjJkY2RhZTMxOGE5NmNiYTQxNzVjZjUzNGE5M2I1OTEiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6Ijc4OTU5NGE5NzIzN2Y2NjVkMmU0Y2RhMTIyZmY1NTIyNzJjMjVmZTFhMTI0NmZjMGNkNjllMDQ4MjE2ZTNlYWMifQ"
+          "resource": "repo://src/app.rs#L374-L428",
+          "version": "repo-lines-v1:sha256:1b48cd00806f0dcaf496a0f9d76527769cfe0ca58e68ae4518b62289b190ab8d:eyJzZWxlY3RlZExpbmVDb3VudCI6NTUsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImM5ZmJjMjYxZDk1MDVkNTBjZTU1MWY4ZDI4OTQ2MTZjMWM2MGY3NTlhZTAwYTE1OWY4Yzg3OGVmMWJjM2Y2ZWEiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImJkYTA0OWEwZDljNDFjMmNjZGQ4OWY2MmQ2Zjc1OTA0ODBjNjQyZWI3Mzc5NTcwM2Q0MTQ1Y2I5MzQ2Y2VhZjIiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6Ijc4OTU5NGE5NzIzN2Y2NjVkMmU0Y2RhMTIyZmY1NTIyNzJjMjVmZTFhMTI0NmZjMGNkNjllMDQ4MjE2ZTNlYWMifQ"
         },
         {
           "resource": "repo://src/autoreply.rs#L284-L307",
@@ -193,7 +197,7 @@
       "statement": "The AI context is loaded with messages_since from local midnight of yesterday, capped at CONTEXT_MESSAGES.",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L397-L416",
+          "resource": "repo://src/app.rs#L438-L457",
           "version": "repo-lines-v1:sha256:77c613038db86228ee90623150e68a8e459a6add92955e4833a1ac3e5f863adc:eyJzZWxlY3RlZExpbmVDb3VudCI6MjAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImU0YTg3YjlhYzc5ZmRlZWFlMjkyODk0ZTMxNDQ3NzU4ZWQwYjdiNTFlZmMwMjJkN2YxNDhlZTU5MTY1MDY2N2QiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjFiYWU5M2RkYzVmMDAxZTlkYTEyOWJmNDgwOTM0NzBiNWVkOGYzYzc3Y2MwNjI0NmIyY2U5MTg3ZjEyMzRiNGQiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjA2ZWViM2RhYjZhYTAxODdiMjY5ZjlmNGY1NjUyODE3ZTljNDE0ZTQzZjQ1Nzc5OWY4YWJlMjliMjhiM2IzZmQifQ"
         },
         {
@@ -201,10 +205,34 @@
           "version": "repo-lines-v1:sha256:50ee4ad69b117378616ca21b79b698134c88777e95ce91db1351b2411baa697f:eyJzZWxlY3RlZExpbmVDb3VudCI6MjIsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImZkNDcwYzIwOWRjNTFhNjVkMGE4MzYzYjE3ZmI5NGI2MTZiOTMwNjU4MTIxZTc3ZjFmMTcwZGU2OWEzMGZiYzEiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjFhZWNlZmYwMmE0Y2M3YzRkNDU0ODYxNjk0OTM2OTFmNzk3Y2U5ZWZkOWFhZjU1OTQwZGE3NWIwNWVhM2RiY2IiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjIwYzRkZTBiYjA5ZGM2YTUzMmVjOGUxOGQ0ODFjMGQyZWJjMDA3ODNlMzhiODk4MDMyNzJiMmY4ZTMwOTJhMzIifQ"
         }
       ]
+    },
+    {
+      "id": "claim_bfe577e08f7e4f61bae3ac4b2b733930",
+      "statement": "The global autoreply does not bypass the chat-type rule: should_reply still requires a private chat or group, so broadcasts, status and newsletters are never answered.",
+      "evidence": [
+        {
+          "resource": "repo://src/autoreply.rs#L258-L282",
+          "version": "repo-lines-v1:sha256:5c6b334c7ae6b4472da173dee6b729d06f5dcbdc994c15293c1090b8a2bbd3dd:eyJzZWxlY3RlZExpbmVDb3VudCI6MjUsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImI3MWJmYWRhZDk1ZTAyNzJiOTdkYmQ2Y2JjMzhkY2U5MWZlOGZiMDFmN2IwMzI3MzA4MDc3Y2U0M2IzZGI0MDIiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjJhOGZmNzc1N2U2ODI2MGYyM2Q1ODU0ZWFlMTExNDQ1NDM3MDNlMzE4MWQxMjIyOGU5MzRmOTM4NmQ1ZjBmYWYiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjJmYmE5MjI2NzJlMWM1MjY1NTMwZjBkYTNlN2RlZDBmOWQxZjUxNjY4YjJlMWMyZWI2MThlYWEzM2VlZmZkMDMifQ"
+        }
+      ]
+    },
+    {
+      "id": "claim_f9d4f2290de14443a15f57c2419e65b7",
+      "statement": "The global autoreply is persisted separately from the per-chat flags and toggling it does not change any per-chat flag.",
+      "evidence": [
+        {
+          "resource": "repo://src/app.rs#L839-L856",
+          "version": "repo-lines-v1:sha256:05cde78eab7927a8b5c33ce971f85c0e210ce60936eada88f8c9481585c1519e:eyJzZWxlY3RlZExpbmVDb3VudCI6MTgsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjlhMjY2MjVlMDNiMmE0MDg1NDg0YjQ2NjUzOTllOWRmOWFhZDQzYmVkNGQxNWFkODEwODMzMGIyMDhjNmM5ZTYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6Ijk5OWJkZWZlZWFjYTYzNDBlMDI5MGRjODZlODg5Yzg5ZDVlMTg5YWE1YmE0NGI1ODViM2I3MDczNGY0YTYyZDgiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImU2ZWQ1MmZlYjhjZTliMjNiYmNiYTkwMGFkYjk0MzE3MjA4ODNhZTk0ODY4NmQyNjhlNmRkMjFlNjljYTU3YjkifQ"
+        },
+        {
+          "resource": "repo://src/history.rs#L285-L315",
+          "version": "repo-lines-v1:sha256:7446cd3d43ecedda818c27075602eb8b9f1b3ef669ea39edf3a6c5520cc4bec3:eyJzZWxlY3RlZExpbmVDb3VudCI6MzEsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImE2OTlhMTQ5OTg2YzgxZjQ4OGYwMjQxMmU4Zjk0YjE1NjdhNWQ2OTEzZTNiYTg0NjJlMmFmYzY5NDJkYWYxMTMiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjRjZTNjZDVlMGE4OTI3NjIwMWQ2ZWU2OWE2ZGM4ZTEyMzgxNDVmMzEyYmE2ODQxZjUzMGYzNDQ0MmU0Njc2NjIiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6Ijk4NTAwNmMxYjY0YjM5MzI5ZWZhMmUxZmU2YTEwMWRiYzg0MTIzYjkxNjkyYjM1MjhjOTg4YzhhOGVmNGRjY2QifQ"
+        }
+      ]
     }
   ],
   "verification": {
     "by": "openwiki/0.7.2",
-    "at": "2026-10-19T19:53:56.480Z"
+    "at": "2026-10-20T20:27:06.515Z"
   }
 }

+ 2 - 2
openwiki/.last-update.json

@@ -1,7 +1,7 @@
 {
-  "updatedAt": "2026-10-19T20:20:31.364Z",
+  "updatedAt": "2026-10-20T20:35:25.184Z",
   "command": "update",
-  "gitHead": "b254ca3163ff19715f276a1c6098d0d64fd4f351",
+  "gitHead": "8229b35a985f6217af4aeaa5f86474e815225859",
   "model": "host-agent/claude",
   "status": "complete",
   "language": "en"

+ 47 - 40
openwiki/.page-manifest.json

@@ -2,74 +2,81 @@
   "schemaVersion": 1,
   "pages": {
     "/openwiki/architecture/app-state.md": {
-      "pageVersion": "sha256:0bfeac2d0428869dc0ca2ded7ee01713526fb6f573a921826fe0a1b96b3d4c7b",
+      "pageVersion": "sha256:34c88f57821c236e84d8cbbd5e3868cc3ff6629a5c6da55afacfbd3989da5c5a",
       "completedBy": "claude-code",
-      "completedRunId": "e1fa6f16-1a67-4470-b80c-7ce826448f22",
-      "gitHead": "b254ca3163ff19715f276a1c6098d0d64fd4f351",
-      "sourceFingerprint": "sha256:c0d3de4ad75905c7a4a80d3e96ac888849b707b066f1b88b878b7853b9d3a792"
+      "completedRunId": "22858bff-a907-4f99-a9b6-ae9a801d890d",
+      "gitHead": "8229b35a985f6217af4aeaa5f86474e815225859",
+      "sourceFingerprint": "sha256:3b21dde5e9b745e08dd416e8c87b4d81d9fe5026c49dd9001bf190232d2e76a7"
     },
     "/openwiki/architecture/runtime-and-event-loop.md": {
-      "pageVersion": "sha256:302d26e89266adfff5cc6a109b6aff5744e957b32642d4337dc08f42da914d70",
+      "pageVersion": "sha256:7cbc08bb6eae59bb32a760bcc976c115e6702ecca67c1710b68b7a232fb16f49",
       "completedBy": "claude-code",
-      "completedRunId": "e1fa6f16-1a67-4470-b80c-7ce826448f22",
-      "gitHead": "b254ca3163ff19715f276a1c6098d0d64fd4f351",
-      "sourceFingerprint": "sha256:c0d3de4ad75905c7a4a80d3e96ac888849b707b066f1b88b878b7853b9d3a792"
+      "completedRunId": "22858bff-a907-4f99-a9b6-ae9a801d890d",
+      "gitHead": "8229b35a985f6217af4aeaa5f86474e815225859",
+      "sourceFingerprint": "sha256:3b21dde5e9b745e08dd416e8c87b4d81d9fe5026c49dd9001bf190232d2e76a7"
     },
     "/openwiki/architecture/terminal-ui.md": {
-      "pageVersion": "sha256:c631191230dc5883f73a1cabd5eff75e1e0e569f34653b9bf8a81d41e68016d9",
+      "pageVersion": "sha256:52543285487729f07ef002bcc4cb827b749e6ec744b94cd40269ba91da70160f",
       "completedBy": "claude-code",
-      "completedRunId": "e1fa6f16-1a67-4470-b80c-7ce826448f22",
-      "gitHead": "b254ca3163ff19715f276a1c6098d0d64fd4f351",
-      "sourceFingerprint": "sha256:c0d3de4ad75905c7a4a80d3e96ac888849b707b066f1b88b878b7853b9d3a792"
+      "completedRunId": "22858bff-a907-4f99-a9b6-ae9a801d890d",
+      "gitHead": "8229b35a985f6217af4aeaa5f86474e815225859",
+      "sourceFingerprint": "sha256:3b21dde5e9b745e08dd416e8c87b4d81d9fe5026c49dd9001bf190232d2e76a7"
     },
     "/openwiki/concepts/history-store.md": {
-      "pageVersion": "sha256:85e34ab27e1a8c9fd95caffe685dc5671c2f174151fe7b0a2b4a400ba9c7d847",
+      "pageVersion": "sha256:af82818f0b14ff0161574d8625a43bed0807de3f4b8dc152a3283e1a40bccf24",
       "completedBy": "claude-code",
-      "completedRunId": "e1fa6f16-1a67-4470-b80c-7ce826448f22",
-      "gitHead": "b254ca3163ff19715f276a1c6098d0d64fd4f351",
-      "sourceFingerprint": "sha256:c0d3de4ad75905c7a4a80d3e96ac888849b707b066f1b88b878b7853b9d3a792"
+      "completedRunId": "22858bff-a907-4f99-a9b6-ae9a801d890d",
+      "gitHead": "8229b35a985f6217af4aeaa5f86474e815225859",
+      "sourceFingerprint": "sha256:3b21dde5e9b745e08dd416e8c87b4d81d9fe5026c49dd9001bf190232d2e76a7"
     },
     "/openwiki/integrations/ai-client.md": {
-      "pageVersion": "sha256:21cc0fb63966a544bddde383e86cf876395c9fc874b47d7f29a65bec3fdf80e7",
+      "pageVersion": "sha256:e30d17eaaf9e0d8e88f0bc51ae0a0c586c23d7bea67344aa86f5188912c80441",
       "completedBy": "claude-code",
-      "completedRunId": "e1fa6f16-1a67-4470-b80c-7ce826448f22",
-      "gitHead": "b254ca3163ff19715f276a1c6098d0d64fd4f351",
-      "sourceFingerprint": "sha256:c0d3de4ad75905c7a4a80d3e96ac888849b707b066f1b88b878b7853b9d3a792"
+      "completedRunId": "22858bff-a907-4f99-a9b6-ae9a801d890d",
+      "gitHead": "8229b35a985f6217af4aeaa5f86474e815225859",
+      "sourceFingerprint": "sha256:3b21dde5e9b745e08dd416e8c87b4d81d9fe5026c49dd9001bf190232d2e76a7"
+    },
+    "/openwiki/integrations/web-control-panel.md": {
+      "pageVersion": "sha256:659021f213da32179df3e732784be9d211b7b699c07342e5abae11b3d55b0d63",
+      "completedBy": "claude-code",
+      "completedRunId": "22858bff-a907-4f99-a9b6-ae9a801d890d",
+      "gitHead": "8229b35a985f6217af4aeaa5f86474e815225859",
+      "sourceFingerprint": "sha256:3b21dde5e9b745e08dd416e8c87b4d81d9fe5026c49dd9001bf190232d2e76a7"
     },
     "/openwiki/integrations/whatsapp-client.md": {
-      "pageVersion": "sha256:eb9c685422f5830657b3b8f06d76614ab7cbf6d4115f92f597ebaf608fa5b7f6",
+      "pageVersion": "sha256:2d56656fd165a43e6f66b741865ee0cc8e711720783c722f71a531e3b8ec0c37",
       "completedBy": "claude-code",
-      "completedRunId": "e1fa6f16-1a67-4470-b80c-7ce826448f22",
-      "gitHead": "b254ca3163ff19715f276a1c6098d0d64fd4f351",
-      "sourceFingerprint": "sha256:c0d3de4ad75905c7a4a80d3e96ac888849b707b066f1b88b878b7853b9d3a792"
+      "completedRunId": "22858bff-a907-4f99-a9b6-ae9a801d890d",
+      "gitHead": "8229b35a985f6217af4aeaa5f86474e815225859",
+      "sourceFingerprint": "sha256:3b21dde5e9b745e08dd416e8c87b4d81d9fe5026c49dd9001bf190232d2e76a7"
     },
     "/openwiki/operations/configuration-and-data.md": {
-      "pageVersion": "sha256:3063070e9f1cf52ceace2c864124dbf09583146e1039a9b795a9d7d8127517a6",
+      "pageVersion": "sha256:f973194dff48fa997deb016f452d6f1515458eef3b52aa111bc52439dafc1b81",
       "completedBy": "claude-code",
-      "completedRunId": "e1fa6f16-1a67-4470-b80c-7ce826448f22",
-      "gitHead": "b254ca3163ff19715f276a1c6098d0d64fd4f351",
-      "sourceFingerprint": "sha256:c0d3de4ad75905c7a4a80d3e96ac888849b707b066f1b88b878b7853b9d3a792"
+      "completedRunId": "22858bff-a907-4f99-a9b6-ae9a801d890d",
+      "gitHead": "8229b35a985f6217af4aeaa5f86474e815225859",
+      "sourceFingerprint": "sha256:3b21dde5e9b745e08dd416e8c87b4d81d9fe5026c49dd9001bf190232d2e76a7"
     },
     "/openwiki/quickstart.md": {
-      "pageVersion": "sha256:b3c1d2b60ba5660ee0e8c7337427e01c7374daa9f8936232bbf6e8190f167d84",
+      "pageVersion": "sha256:f8e55a63196e59cd28b6114d94f0faa4326a003dbe6b743831920c4b4a7fb4c9",
       "completedBy": "claude-code",
-      "completedRunId": "e1fa6f16-1a67-4470-b80c-7ce826448f22",
-      "gitHead": "b254ca3163ff19715f276a1c6098d0d64fd4f351",
-      "sourceFingerprint": "sha256:c0d3de4ad75905c7a4a80d3e96ac888849b707b066f1b88b878b7853b9d3a792"
+      "completedRunId": "22858bff-a907-4f99-a9b6-ae9a801d890d",
+      "gitHead": "8229b35a985f6217af4aeaa5f86474e815225859",
+      "sourceFingerprint": "sha256:3b21dde5e9b745e08dd416e8c87b4d81d9fe5026c49dd9001bf190232d2e76a7"
     },
     "/openwiki/testing/unit-tests.md": {
-      "pageVersion": "sha256:c68cbef2ff467d2cea57ad59f059cb2f524cd4728d83dbb97ee74719dc672a37",
+      "pageVersion": "sha256:a01bacd1db7d8ffec9c6c644ce544b4408c7a27be2ca73f04ff774e65a7810cb",
       "completedBy": "claude-code",
-      "completedRunId": "e1fa6f16-1a67-4470-b80c-7ce826448f22",
-      "gitHead": "b254ca3163ff19715f276a1c6098d0d64fd4f351",
-      "sourceFingerprint": "sha256:c0d3de4ad75905c7a4a80d3e96ac888849b707b066f1b88b878b7853b9d3a792"
+      "completedRunId": "22858bff-a907-4f99-a9b6-ae9a801d890d",
+      "gitHead": "8229b35a985f6217af4aeaa5f86474e815225859",
+      "sourceFingerprint": "sha256:3b21dde5e9b745e08dd416e8c87b4d81d9fe5026c49dd9001bf190232d2e76a7"
     },
     "/openwiki/workflows/ai-autoreply.md": {
-      "pageVersion": "sha256:eb01f5f500741db0c4fe6150c60f34a21f09961bad77eaf7779a0075d6a2ee6d",
+      "pageVersion": "sha256:ccc0e095054a5eb2aa06cbab3cb29b6a721ba01910ea260ce580279a25de25d8",
       "completedBy": "claude-code",
-      "completedRunId": "e1fa6f16-1a67-4470-b80c-7ce826448f22",
-      "gitHead": "b254ca3163ff19715f276a1c6098d0d64fd4f351",
-      "sourceFingerprint": "sha256:c0d3de4ad75905c7a4a80d3e96ac888849b707b066f1b88b878b7853b9d3a792"
+      "completedRunId": "22858bff-a907-4f99-a9b6-ae9a801d890d",
+      "gitHead": "8229b35a985f6217af4aeaa5f86474e815225859",
+      "sourceFingerprint": "sha256:3b21dde5e9b745e08dd416e8c87b4d81d9fe5026c49dd9001bf190232d2e76a7"
     }
   }
 }

+ 142 - 79
openwiki/architecture/app-state.md

@@ -1,15 +1,15 @@
 ---
 type: architecture
 title: Application state and event handling
-description: How the App struct in src/app.rs holds UI state, routes AppEvent values, handles keys, ingests messages and runs background tasks.
-tags: [app, state, events, keybindings, tokio]
+description: How the App struct in src/app.rs holds UI state, routes AppEvent values (keys, WhatsApp, AI, timers, web panel), handles function keys, the list filter and chat deletion, toggles global and per-chat autoreply, and runs background tasks.
+tags: [app, state, events, keybindings, tokio, web-panel]
 verified:
   - by: openwiki/0.7.2
-    at: 2026-10-19T19:53:56.480Z
+    at: 2026-10-20T20:27:06.515Z
 sources:
   - id: openwiki-source-9fec9d1af408501c9c455161
     resource: repo://src/app.rs
-generated: { by: "claude-code", at: "2026-10-19T19:53:56.480Z" }
+generated: { by: "claude-code", at: "2026-10-20T20:27:06.515Z" }
 ---
 
 # Application state and event handling
@@ -18,12 +18,12 @@ generated: { by: "claude-code", at: "2026-10-19T19:53:56.480Z" }
 [Runtime and event loop](runtime-and-event-loop.md) feeds every input into
 `App::handle_event`, and the [terminal UI](terminal-ui.md) renders the public
 fields of `App` after each event. `App` is the only component that mutates the
-[history store](../concepts/history-store.md) in response to user and WhatsApp
-activity.
+[history store](../concepts/history-store.md) in response to user, WhatsApp
+and [web panel](../integrations/web-control-panel.md) activity.
 
 ## Events
 
-`AppEvent` (`src/app.rs:56-79`) is the single message type of the main channel:
+`AppEvent` (`src/app.rs:73-98`) is the single message type of the main channel:
 
 | Variant | Producer | Meaning |
 |---|---|---|
@@ -33,39 +33,41 @@ activity.
 | `AiReply { chat_jid, result }` | AI background task | Generated reply text, or an error string. |
 | `CooldownElapsed { chat_jid }` | cooldown timer task | The cooldown of a chat with a deferred reply has ended. |
 | `Sent { chat_jid, text, is_ai, result }` | send background task | Outcome of a text send. |
+| `Web(WebRequest)` | `wh-web` thread | A command from the [web control panel](../integrations/web-control-panel.md) with a reply channel. |
 
-`handle_event` (`src/app.rs:206-220`) dispatches each variant. Only fatal
+`handle_event` (`src/app.rs:236-251`) dispatches each variant. Only fatal
 history (SQLite) errors propagate as `Err`; every other failure is turned into
 a `status` text shown in the footer.
 
 ## State
 
-`App` (`src/app.rs:128-158`) exposes UI fields read by the renderer: `screen`,
-`qr_code`, `focus`, `list_mode`, `chats`, `contacts`, `selected`, `open_chat`,
-`open_chat_name`, `open_chat_autoreply`, `messages`, `input`, `status` and
-`should_quit`. Private fields hold the `History`, the `Config`, an optional
-`AiClient`, the autoreply `ReplyGate`, the `own_jids_known` and
-`catch_up_done` flags, the `WaHandle` and a clone of the event sender used by
-background tasks.
-
-- `Screen` (`src/app.rs:81-87`): `Connecting` → `Pairing` (QR shown) → `Main`.
-- `Focus` (`src/app.rs:89-94`): `List` or `Input`.
-- `ListMode` (`src/app.rs:96-101`): `Chats` or `Contacts`.
-
-`App::new` (`src/app.rs:165-199`) builds the AI client with `build_ai`, loads
-chats and contacts from history, starts on `Connecting` with list focus in
-chat mode, and creates the `ReplyGate` with the current time and the
-`Cooldowns { private, group }` taken from `cooldown_seconds` and
-`group_cooldown_seconds`, so that backlog messages are never auto-answered
-live.
-
-`build_ai` (`src/app.rs:875-883`) returns no client when `system_prompt` is
+`App` (`src/app.rs:147-184`) exposes UI fields read by the renderer: `screen`,
+`qr_code`, `focus`, `list_mode`, `chats`, `contacts`, `selected`, `filter`,
+`open_chat`, `open_chat_name`, `open_chat_autoreply`, `global_autoreply`,
+`messages`, `input`, `status` and `should_quit`. Private fields hold the
+`History`, the `Config`, an optional `AiClient`, the autoreply `ReplyGate`,
+the `own_jids_known` and `catch_up_done` flags, `pending_delete` (the chat
+awaiting a deletion confirmation), the `WaHandle` and a clone of the event
+sender used by background tasks.
+
+- `Screen` (`src/app.rs:100-106`): `Connecting` → `Pairing` (QR shown) → `Main`.
+- `Focus` (`src/app.rs:108-113`): `List` or `Input`.
+- `ListMode` (`src/app.rs:115-120`): `Chats` or `Contacts`.
+
+`App::new` (`src/app.rs:191-229`) builds the AI client with `build_ai`, loads
+chats, contacts and the persisted global autoreply flag from history, starts on
+`Connecting` with list focus in chat mode and an empty filter, and creates the
+`ReplyGate` with the current time and the `Cooldowns { private, group }` taken
+from `cooldown_seconds` and `group_cooldown_seconds`, so that backlog messages
+are never auto-answered live.
+
+`build_ai` (`src/app.rs:1117-1125`) returns no client when `system_prompt` is
 missing (status `STATUS_AI_NOT_CONFIGURED`) and reports a client build error
 with the `AI non disponibile` prefix.
 
 ## WhatsApp events
 
-`handle_wa_event` (`src/app.rs:226-251`):
+`handle_wa_event` (`src/app.rs:257-282`):
 
 - `Qr` → switch to `Pairing` and store the QR payload.
 - `Connected` → switch to `Main`, status `Connesso`, refresh own JIDs.
@@ -74,9 +76,9 @@ with the `AI non disponibile` prefix.
 - `OfflineSyncCompleted` → `on_offline_sync_completed`, the startup catch-up
   (see below).
 - `Contact` → `on_contact`: upsert the contact, reload contacts and chats and
-  refresh the open chat name (`src/app.rs:257-265`).
+  refresh the open chat name (`src/app.rs:288-296`).
 - `Message { message, image, addressed_to }` → `on_message`
-  (`src/app.rs:273-282`).
+  (`src/app.rs:304-313`).
 
 `on_message` saves the message and stops immediately when
 `History::save_message` reports a duplicate, so history sync replays never
@@ -85,95 +87,156 @@ open conversation and then go through `maybe_autoreply` together with the
 `addressed_to` JIDs (mentions/quotes) used by the group cooldown (see
 [AI autoreply](../workflows/ai-autoreply.md)).
 
-Own JIDs are fetched by `refresh_own_jids` (`src/app.rs:456-463`); the list is
+Own JIDs are fetched by `refresh_own_jids` (`src/app.rs:497-504`); the list is
 empty while the device is not paired, so the refresh is retried later
 (`own_jids_known` stays false until it succeeds).
 
 ## Autoreply triggers
 
-Three paths start an AI reply, all through `start_reply`
-(`src/app.rs:403-416`), which records the reply in the gate (cancelling any
+A chat is "enabled" when the global autoreply is on **or** its per-chat flag
+is on (`autoreply_enabled`, `src/app.rs:370-372`); every path below uses this
+check. Three paths start an AI reply, all through `start_reply`
+(`src/app.rs:444-457`), which records the reply in the gate (cancelling any
 pending deferral), loads the context with
 `History::messages_since(chat_jid, context_start(now), CONTEXT_MESSAGES)` and
-calls `spawn_reply` (`src/app.rs:426-449`) to build the turns and spawn the AI
-task. `ai_setup` (`src/app.rs:393-395`) returns nothing, and so stops every
+calls `spawn_reply` (`src/app.rs:467-490`) to build the turns and spawn the AI
+task. `ai_setup` (`src/app.rs:434-436`) returns nothing, and so stops every
 path, when the AI client or the system prompt is missing.
 
-- **Live message** — `maybe_autoreply` (`src/app.rs:293-315`) replies when
+- **Live message** — `maybe_autoreply` (`src/app.rs:324-346`) replies when
   `ReplyGate::should_reply` allows it. Otherwise, if
   `ReplyGate::deferral_due` returns a due time (private chats blocked only by
   the cooldown), the chat is marked pending and `spawn_cooldown_timer`
-  (`src/app.rs:749-755`) sleeps until then and emits `CooldownElapsed`.
-- **Deferred reply** — `handle_cooldown_elapsed` (`src/app.rs:324-332`)
+  (`src/app.rs:991-997`) sleeps until then and emits `CooldownElapsed`.
+- **Deferred reply** — `handle_cooldown_elapsed` (`src/app.rs:356-364`)
   consumes the pending mark with `take_pending`; if it was still pending and
-  autoreply is still enabled it sends one text-only reply (no image).
-- **Startup catch-up** — `on_offline_sync_completed` (`src/app.rs:338-344`)
+  the chat is still enabled it sends one text-only reply (no image).
+- **Startup catch-up** — `on_offline_sync_completed` (`src/app.rs:378-384`)
   runs `catch_up` once per process (`catch_up_done`). `catch_up`
-  (`src/app.rs:353-369`) asks `catch_up_chats` (`src/app.rs:376-387`) for the
-  autoreply-enabled chats whose latest stored message passes
-  `ReplyGate::catch_up_due` within `catch_up_seconds`, starts a text-only
-  reply for each and sets the status `Risposte di recupero: N` when N > 0.
+  (`src/app.rs:393-409`) asks `catch_up_chats` (`src/app.rs:416-428`) for the
+  enabled chats (all chats when the global autoreply is on) whose latest
+  stored message passes `ReplyGate::catch_up_due` within `catch_up_seconds`,
+  starts a text-only reply for each and sets the status
+  `Risposte di recupero: N` when N > 0.
 
 ## Key bindings
 
-`handle_key` (`src/app.rs:528-550`):
+Commands use function keys (`src/app.rs:22-29`); `handle_key`
+(`src/app.rs:573-603`) dispatches them:
 
 | Key | Where | Action |
 |---|---|---|
-| Ctrl+C | any screen | Quit (`should_quit = true`). |
-| Ctrl+A | main screen | Toggle autoreply for the open chat. |
+| F10 | any screen | Quit (`should_quit = true`). |
+| F2 | main screen, any focus | Switch chat list / contact list; selection resets to 0, filter kept. |
+| F3 | main screen, any focus | Toggle autoreply for the open chat. |
+| F4 | main screen, any focus | Toggle the global autoreply ("AI tutte"). |
 | Tab | main screen | Switch focus between list and input. |
-| F2 | main screen | Switch chat list / contact list; selection resets to 0. |
-| Up / Down | list focus | Move selection, clamped to the list. |
+| Up / Down | list focus | Move selection, clamped to the filtered list. |
 | Enter | list focus | Open the selected chat or contact. |
+| characters / Backspace | list focus | Edit the list filter; selection resets to 0. |
+| Esc | list focus | Clear the filter (or cancel a pending deletion). |
+| Delete (Canc) | list focus, chat mode | Ask to delete the selected chat; a second Delete confirms. |
 | characters / Backspace | input focus | Edit the input line. |
 | Esc | input focus | Return focus to the list. |
 | Enter | input focus | Send the trimmed input to the open chat. |
 
-Non-main screens ignore everything except Ctrl+C. `is_command`
-(`src/app.rs:905-907`) treats Ctrl only as a command when Alt is not held,
-because Windows reports AltGr as Ctrl+Alt (needed for `@`, `#`, `€` on the
-Italian layout). Switching list mode with F2 also makes the event loop clear
-the terminal (see [Runtime and event loop](runtime-and-event-loop.md)).
+Non-main screens ignore everything except F10. Ctrl chords are swallowed
+(they cancel a pending deletion and do nothing else) so they never type into
+the input or the filter; `is_command` (`src/app.rs:1163-1165`) treats Ctrl as
+a command only when Alt is not held, because Windows reports AltGr as
+Ctrl+Alt (needed for `@`, `#`, `€` on the Italian layout). Switching list
+mode with F2 also makes the event loop clear the terminal (see
+[Runtime and event loop](runtime-and-event-loop.md)).
+
+### List filter
+
+Typing while the list has focus edits `filter` (`handle_list_key`,
+`edit_filter`, `src/app.rs:610-638`). `visible_chats` / `visible_contacts`
+(`src/app.rs:710-720`) keep the entries whose name contains the filter,
+case-insensitively (`matches_filter`, `src/app.rs:935-937`); selection,
+opening and deletion all work on the filtered list.
+
+### Local chat deletion
+
+`request_delete` (`src/app.rs:657-674`) works only in chat mode: the first
+Delete stores the selected chat in `pending_delete` and shows
+`Eliminare la chat <name>? Canc conferma, Esc annulla`; a second Delete on
+the same chat calls `confirm_delete` (`src/app.rs:681-693`), which removes
+the chat's messages and row from the history (no WhatsApp call), drops any
+deferred reply for it, closes it if open and shows `Chat <name> eliminata`.
+Any other key (Tab, F2, F3, F4, list keys, Ctrl chords) cancels the pending
+deletion with `Eliminazione annullata` (`cancel_pending_delete`,
+`src/app.rs:644-650`).
+
+### Opening and toggling
+
+`open_selected` (`src/app.rs:862-878`) loads display name, per-chat
+autoreply flag and the last `MESSAGES_SHOWN` (200) messages of the selected
+visible entry, then moves focus to the input. The `reload_*` helpers
+(`src/app.rs:895-920`) keep `selected` inside the active filtered list.
+
+`toggle_autoreply` (F3, `src/app.rs:768-773`) delegates to
+`toggle_chat_autoreply` (`src/app.rs:781-794`), which always allows
+disabling. Enabling goes through `can_enable_autoreply`
+(`src/app.rs:1132-1140`), which first requires an AI client and then a chat
+accepted by `supports_autoreply` (private chats and groups; broadcasts,
+status and newsletters are refused with
+`Autoreply disponibile solo nelle chat private e nei gruppi`). The flag is
+persisted with `History::set_autoreply` and the status shows
+`Autoreply ON|OFF per <name>`.
 
-`open_selected` (`src/app.rs:630-644`) loads display name, autoreply flag and
-the last `MESSAGES_SHOWN` (200) messages, then moves focus to the input. The
-`reload_*` helpers (`src/app.rs:661-686`) keep `selected` inside the active list.
+`toggle_global_autoreply` (F4, `src/app.rs:845-856`) refuses to enable
+without an AI client (`STATUS_AI_NOT_CONFIGURED`), persists the flag with
+`History::set_global_autoreply`, updates `global_autoreply` and shows
+`Autoreply globale ON|OFF`. Disabling is always allowed.
 
-`toggle_autoreply` (`src/app.rs:610-624`) always allows disabling. Enabling
-goes through `can_enable_autoreply` (`src/app.rs:890-898`), which first
-requires an AI client and then a chat accepted by `supports_autoreply`
-(private chats and groups; broadcasts, status and newsletters are refused with
-`Autoreply disponibile solo nelle chat private e nei gruppi`). The flag is
-persisted with `History::set_autoreply`.
+## Web panel requests
+
+`handle_web_request` (`src/app.rs:801-812`) applies a `WebCommand` and then
+always sends back a `WebState` snapshot on the request's oneshot channel:
+
+- `State` → nothing, just the snapshot;
+- `ToggleGlobal` → `toggle_global_autoreply` (same rules as F4);
+- `ToggleChat(jid)` → `toggle_web_chat` (`src/app.rs:819-825`): if the JID is
+  not among the panel's chats the status becomes `Chat non trovata`,
+  otherwise `toggle_chat_autoreply` (same rules as F3);
+- `Quit` → `should_quit = true`.
+
+`web_state` (`src/app.rs:831-837`) returns the global flag, the status line
+and `web_chats` (`src/app.rs:1146-1156`): the TUI chat list in the same order,
+restricted to chats that support autoreply. See
+[Web control panel](../integrations/web-control-panel.md).
 
 ## Sending and background tasks
 
 All network work runs in tokio tasks that report back through the event
 channel; `send_event` ignores a closed channel because that only happens on
-shutdown (`src/app.rs:739-742`).
+shutdown (`src/app.rs:981-984`).
 
-- `submit_input` (`src/app.rs:586-594`) ignores blank input or no open chat,
+- `submit_input` (`src/app.rs:742-750`) ignores blank input or no open chat,
   clears the input and calls `spawn_send` with `is_ai: false`.
-- `spawn_send` (`src/app.rs:859-868`) calls `WaHandle::send_text` and emits
+- `spawn_send` (`src/app.rs:1101-1110`) calls `WaHandle::send_text` and emits
   `Sent`.
-- `handle_sent` (`src/app.rs:498-522`) stores a successful send as a
+- `handle_sent` (`src/app.rs:540-564`) stores a successful send as a
   `from_me` message (with `is_ai` preserved) and reloads the views; a failure
   sets `Invio fallito: …`.
-- `spawn_ai_request` (`src/app.rs:762-772`) runs a text completion and emits
+- `spawn_ai_request` (`src/app.rs:1004-1014`) runs a text completion and emits
   `AiReply`.
-- `spawn_vision_request` (`src/app.rs:781-791`) runs one task that downloads
+- `spawn_vision_request` (`src/app.rs:1023-1033`) runs one task that downloads
   the image and calls the vision model, falling back to the text courtesy
-  turns on failure (`complete_with_fallback`, `src/app.rs:799-812`). When both
-  fail, `combine_errors` joins the two messages. Image bytes stay in memory.
-- `send_ai_reply_if_enabled` (`src/app.rs:483-492`) re-checks the persisted
-  autoreply flag before sending an AI reply, discarding it with
-  `Risposta AI scartata: autoreply disattivato` if the user turned it off
-  meanwhile.
+  turns on failure (`complete_with_fallback`, `src/app.rs:1041-1054`). When
+  both fail, `combine_errors` joins the two messages. Image bytes stay in
+  memory.
+- `send_ai_reply_if_enabled` (`src/app.rs:525-534`) re-checks
+  `autoreply_enabled` (global or per chat) before sending an AI reply,
+  discarding it with `Risposta AI scartata: autoreply disattivato` if both
+  were turned off meanwhile.
 
 ## Tests
 
-Unit tests at `src/app.rs:909` onward cover `can_enable_autoreply` (private,
+Unit tests at `src/app.rs:1167` onward cover `can_enable_autoreply` (private,
 self-chat LID, groups allowed; broadcasts/newsletters and missing AI refused),
-`vision_applies`, `combine_errors`, selection clamping/stepping, the toggle
-helpers and `is_command`. See [Unit tests](../testing/unit-tests.md).
+`web_chats` filtering and order, `vision_applies`, `combine_errors`,
+`matches_filter`, selection clamping/stepping, the toggle helpers,
+`is_command` and the fixed function-key codes (F2, F3, F4, F10). See
+[Unit tests](../testing/unit-tests.md).

+ 3 - 3
openwiki/architecture/index.md

@@ -1,5 +1,5 @@
 # Files
 
-- [Application state and event handling](app-state.md) - How the App struct in src/app.rs holds UI state, routes AppEvent values, handles keys, ingests messages and runs background tasks.
-- [Runtime and event loop](runtime-and-event-loop.md) - Process startup in src/main.rs, the tokio runtime, the two event channels, the input thread, the WhatsApp forwarder and the draw/recv TUI loop.
-- [Terminal UI rendering](terminal-ui.md) - How src/ui.rs draws the connecting, QR pairing and main screens with ratatui, including message formatting, wrapping, the input box and the footer.
+- [Application state and event handling](app-state.md) - How the App struct in src/app.rs holds UI state, routes AppEvent values (keys, WhatsApp, AI, timers, web panel), handles function keys, the list filter and chat deletion, toggles global and per-chat autoreply, and runs background tasks.
+- [Runtime and event loop](runtime-and-event-loop.md) - Process startup in src/main.rs, the tokio runtime, the two event channels, the input thread, the WhatsApp forwarder, the optional web panel thread and the draw/recv TUI loop.
+- [Terminal UI rendering](terminal-ui.md) - How src/ui.rs draws the connecting, QR pairing and main screens with ratatui, including the filtered list with global autoreply marker, message formatting, wrapping, the input box and the footer key help.

+ 36 - 26
openwiki/architecture/runtime-and-event-loop.md

@@ -1,15 +1,15 @@
 ---
 type: architecture
 title: Runtime and event loop
-description: Process startup in src/main.rs, the tokio runtime, the two event channels, the input thread, the WhatsApp forwarder and the draw/recv TUI loop.
-tags: [runtime, tokio, event-loop, ratatui, crossterm]
+description: Process startup in src/main.rs, the tokio runtime, the two event channels, the input thread, the WhatsApp forwarder, the optional web panel thread and the draw/recv TUI loop.
+tags: [runtime, tokio, event-loop, ratatui, crossterm, web-panel]
 sources:
   - id: openwiki-source-b55a21a31ede1b56cd31a6a6
     resource: repo://src/main.rs
-generated: { by: "claude-code", at: "2026-10-19T19:53:56.480Z" }
+generated: { by: "claude-code", at: "2026-10-20T20:27:06.515Z" }
 verified:
   - by: openwiki/0.7.2
-    at: 2026-10-19T19:53:56.480Z
+    at: 2026-10-20T20:27:06.515Z
 ---
 
 # Runtime and event loop
@@ -20,7 +20,7 @@ through [`App::handle_event`](app-state.md), so `App` needs no locking.
 
 ## Startup sequence
 
-`main` (`src/main.rs:28-51`) runs on the multi-threaded tokio runtime
+`main` (`src/main.rs:30-54`) runs on the multi-threaded tokio runtime
 (`#[tokio::main]`) and:
 
 1. Loads the configuration with `Config::load` and creates the data directory
@@ -32,50 +32,60 @@ through [`App::handle_event`](app-state.md), so `App` needs no locking.
 4. Starts the [WhatsApp client](../integrations/whatsapp-client.md) with
    `whatsapp::start(session_db_path, wa_tx)`.
 5. Spawns the WhatsApp forwarder.
-6. Builds `App` from `AppDeps { config, history, wa, events }`.
-7. Spawns the input thread.
-8. Runs the TUI until quit.
+6. Starts the optional [web control panel](../integrations/web-control-panel.md)
+   with `start_web_panel` (`src/main.rs:74-79`): nothing happens when
+   `web_port` is unset, otherwise `web::start(port, app_tx)` binds the port.
+7. Builds `App` from `AppDeps { config, history, wa, events }`.
+8. Spawns the input thread.
+9. Runs the TUI until quit.
 
 Any failure before the TUI starts aborts with an `anyhow` context such as
-`cannot load configuration`, `cannot open history database` or
-`cannot start WhatsApp client`.
+`cannot load configuration`, `cannot open history database`,
+`cannot start WhatsApp client` or `cannot start web server on port <port>`
+(for example when the port is already in use).
 
 ## Event sources
 
 ```
- crossterm (blocking)        whatsapp-rust bot           tokio tasks in App
-   "wh-input" thread          Translator task           (AI, vision, send)
-        |                          |                          |
-        | AppEvent::Key/Redraw     | WaEvent                  | AiReply / Sent
-        v                          v                          |
-   app_tx  <---------  WhatsApp forwarder (Wa(..))            |
-        ^                                                     |
-        +-----------------------------------------------------+
+ crossterm (blocking)     whatsapp-rust bot      tiny_http (blocking)    tokio tasks in App
+   "wh-input" thread       Translator task         "wh-web" thread       (AI, vision, send,
+        |                        |                       |                cooldown timers)
+        | Key/Redraw             | WaEvent               | Web(WebRequest)      |
+        v                        v                       v                      | AiReply / Sent /
+   app_tx  <------  WhatsApp forwarder (Wa(..))  <-------+                      | CooldownElapsed
+        ^                                                                       |
+        +-----------------------------------------------------------------------+
         |
      app_rx  -->  event_loop: draw ui::render -> recv -> app.handle_event
 ```
 
-- **WhatsApp forwarder** (`src/main.rs:57-65`): a tokio task that wraps each
+- **WhatsApp forwarder** (`src/main.rs:60-68`): a tokio task that wraps each
   `WaEvent` in `AppEvent::Wa` and stops when either channel side closes.
-- **Input thread** (`src/main.rs:71-102`): a named OS thread (`wh-input`)
+- **Web thread** (`wh-web`, see [Web control panel](../integrations/web-control-panel.md)):
+  a blocking HTTP server on its own OS thread; each API request is sent as
+  `AppEvent::Web` and the thread waits on a oneshot channel for the state
+  snapshot that `App` sends back after handling it.
+- **Input thread** (`src/main.rs:85-116`): a named OS thread (`wh-input`)
   because `crossterm::event::read` blocks. On a read error it sends
   `WaEvent::Error("terminal input error: …")` and stops instead of spinning.
-- **Event mapping** (`src/main.rs:109-115`): only `KeyEventKind::Press` keys
+- **Event mapping** (`src/main.rs:123-129`): only `KeyEventKind::Press` keys
   become `AppEvent::Key` (Windows also emits release and repeat events);
   `Resize` becomes `AppEvent::Redraw`; everything else is dropped.
 - **Background tasks** spawned by `App` (AI completions, image download,
-  sends) hold a clone of the sender and report with `AiReply` / `Sent`.
+  sends, cooldown timers) hold a clone of the sender and report with
+  `AiReply` / `Sent` / `CooldownElapsed`.
 
 ## TUI loop
 
-`run_tui` (`src/main.rs:121-126`) calls `ratatui::init`, runs the loop and
+`run_tui` (`src/main.rs:135-140`) calls `ratatui::init`, runs the loop and
 always calls `ratatui::restore`, so raw mode and the alternate screen are
 left even on errors.
 
-`event_loop` (`src/main.rs:133-152`) repeats while `!app.should_quit`:
+`event_loop` (`src/main.rs:147-166`) repeats while `!app.should_quit`:
 draw a frame with [`ui::render`](terminal-ui.md), await the next event, and
 pass it to `app.handle_event`. The loop also ends cleanly when the event
 channel closes. Draw errors and fatal `handle_event` errors end the process.
+`should_quit` is set by F10 or by a quit request from the web panel.
 
 Before dispatching, the loop records `list_mode_before = app.list_mode`; if
 the event changed the list mode (F2 switches chats/contacts), it calls
@@ -83,5 +93,5 @@ the event changed the list mode (F2 switches chats/contacts), it calls
 list cannot leave stale cells behind.
 
 Because rendering happens only after an event, the screen is redrawn on key
-presses, resizes, WhatsApp events and background task results — there is no
-timer tick.
+presses, resizes, WhatsApp events, web panel requests and background task
+results — there is no timer tick.

+ 51 - 38
openwiki/architecture/terminal-ui.md

@@ -1,15 +1,15 @@
 ---
 type: architecture
 title: Terminal UI rendering
-description: How src/ui.rs draws the connecting, QR pairing and main screens with ratatui, including message formatting, wrapping, the input box and the footer.
+description: How src/ui.rs draws the connecting, QR pairing and main screens with ratatui, including the filtered list with global autoreply marker, message formatting, wrapping, the input box and the footer key help.
 tags: [ui, ratatui, tui, qr-code, rendering]
 sources:
   - id: openwiki-source-6bddea306b9b87f180f1fc02
     resource: repo://src/ui.rs
-generated: { by: "claude-code", at: "2026-10-19T18:58:46.219Z" }
+generated: { by: "claude-code", at: "2026-10-20T20:27:06.515Z" }
 verified:
   - by: openwiki/0.7.2
-    at: 2026-10-19T19:53:56.480Z
+    at: 2026-10-20T20:27:06.515Z
 ---
 
 # Terminal UI rendering
@@ -18,11 +18,11 @@ verified:
 [application state](app-state.md) and fills a ratatui frame. It never mutates
 `App`; the [event loop](runtime-and-event-loop.md) calls it once per event.
 All user-facing strings are Italian constants at the top of the file
-(`src/ui.rs:21-91`).
+(`src/ui.rs:21-99`).
 
 ## Screens
 
-`render` (`src/ui.rs:97-104`) dispatches on `app.screen`:
+`render` (`src/ui.rs:105-112`) dispatches on `app.screen`:
 
 | Screen | Function | Content |
 |---|---|---|
@@ -30,18 +30,18 @@ All user-facing strings are Italian constants at the top of the file
 | `Pairing` | `render_pairing` | Title, QR code, pairing steps ("WhatsApp > Impostazioni > Dispositivi collegati > Collega un dispositivo"), status |
 | `Main` | `render_main` | List pane, messages pane, input pane, footer |
 
-`render_centered` (`src/ui.rs:142-155`) centers a block of lines vertically
+`render_centered` (`src/ui.rs:150-163`) centers a block of lines vertically
 and horizontally.
 
 ### QR code
 
 The QR payload comes from the [WhatsApp client](../integrations/whatsapp-client.md)
-(`WaEvent::Qr`). `qr_to_text` (`src/ui.rs:182-191`) encodes it with the
+(`WaEvent::Qr`). `qr_to_text` (`src/ui.rs:190-199`) encodes it with the
 `qrcode` crate using `Dense1x2` half-block characters and a quiet zone;
 colors are inverted (dark modules drawn as empty cells) so the code scans on a
 dark terminal background.
 
-`qr_block_lines` (`src/ui.rs:161-175`) replaces the QR with a single line when:
+`qr_block_lines` (`src/ui.rs:169-183`) replaces the QR with a single line when:
 
 - no code has arrived yet: "In attesa del codice QR...";
 - encoding fails: "Errore generazione QR: <error>";
@@ -50,67 +50,80 @@ dark terminal background.
 
 ## Main screen layout
 
-`render_main` (`src/ui.rs:197-212`):
+`render_main` (`src/ui.rs:205-220`):
 
 ```
-+-----------------+--------------------------------------+
-| Chat [F2: ...]  |  <name> ── AI: ON                     |
-| > Alice ●AI     |  10:02 Alice: ciao                    |
-|   Bob           |  10:03 Tu: ciao!                      |
-|                 |  10:03 [AI] Tu: ...                   |
-|   (30%)         +--------------------------------------+
-|                 | Messaggio                             |
-|                 | > typed text_                         |
-+-----------------+--------------------------------------+
- <status>   Tab:focus  Enter:apri/invia  F2:chat/contatti ...
++---------------------------------+--------------------------------------+
+| Chat [F2: Contatti] ●AI tutte   |  <name> ── AI: ON                     |
+|   Cerca: mar                    |  10:02 Alice: ciao                    |
+| > Mario ●AI                     |  10:03 Tu: ciao!                      |
+|   Marta                         |  10:03 [AI] Tu: ...                   |
+|   (30%)                         +--------------------------------------+
+|                                 | Messaggio                             |
+|                                 | > typed text_                         |
++---------------------------------+--------------------------------------+
+ <status>   Tab:focus  Enter:apri/invia  F2:chat/contatti  Canc:elimina  F3:AI chat  F4:AI tutte  F10:esci
 ```
 
-- **List pane** (30% width, `src/ui.rs:232-274`): chats or contacts depending
-  on `list_mode`, title "Chat [F2: Contatti]" / "Contatti [F2: Chat]". The
-  selection is reversed with a `"> "` marker and clamped by `selected_index`.
-  Chats with autoreply enabled show a green ` ●AI` marker.
-- **Messages pane** (`src/ui.rs:280-315`): title is
-  ` <name> ── AI: ON|OFF ` or " Nessuna chat aperta ". All messages are
+(The list title is a single line: base title, then the global marker, then
+the search text.)
+
+- **List pane** (30% width, `src/ui.rs:240-259`): the filtered chats or
+  contacts (`App::visible_chats` / `visible_contacts`) depending on
+  `list_mode`. The title (`list_title`, `src/ui.rs:266-275`) is
+  "Chat [F2: Contatti]" / "Contatti [F2: Chat]", followed — in chat mode
+  only — by `●AI tutte` when the global autoreply is on, and by
+  `Cerca: <filter>` when a filter is typed. The selection is reversed with a
+  `"> "` marker and clamped by `selected_index`. Chats whose per-chat
+  autoreply is enabled show a green ` ●AI` marker (`chat_item`,
+  `src/ui.rs:289-296`).
+- **Messages pane** (`src/ui.rs:310-346`): title is
+  ` <name> ── AI: ON|OFF ` or " Nessuna chat aperta "; `AI: ON` is shown when
+  the open chat's flag **or** the global autoreply is on. All messages are
   wrapped to the inner width and only the last rows that fit are kept, so the
   newest messages are always visible (no scrolling).
-- **Input pane** (3 rows, `src/ui.rs:461-491`): shows `"> " + input`; when
+- **Input pane** (3 rows, `src/ui.rs:492-504`): shows `"> " + input`; when
   the text is wider than the box only the tail is shown (`tail_fitting`), and
   the terminal cursor is placed after it while the input has focus.
-- **Footer** (1 row, `src/ui.rs:497-506`): bold status text followed by the
-  dimmed key help
-  `Tab:focus  Enter:apri/invia  F2:chat/contatti  Ctrl+A:AI chat  Ctrl+C:esci`.
+- **Footer** (1 row, `src/ui.rs:528-556`): bold status text followed by the
+  key help
+  `Tab:focus  Enter:apri/invia  F2:chat/contatti  Canc:elimina  F3:AI chat  F4:AI tutte  F10:esci`.
+  The help is dimmed, except `F4:AI tutte`, which is drawn green while the
+  global autoreply is on (`key_help_spans`).
 
 The focused pane (list or input) has a yellow border (`pane_block`,
-`src/ui.rs:218-226`).
+`src/ui.rs:226-234`).
 
 ## Message formatting
 
-`format_message_line` (`src/ui.rs:346-362`) renders a
+`format_message_line` (`src/ui.rs:377-397`) renders a
 [stored message](../concepts/history-store.md) as `HH:MM <sender>: <text>` in
 the local time zone; out-of-range timestamps show `--:--`
-(`src/ui.rs:368-376`).
+(`format_clock_time`, `src/ui.rs:399-408`).
 
-`sender_label` (`src/ui.rs:383-396`) picks:
+`sender_label` (`src/ui.rs:414-431`) picks:
 
 1. `Tu` for own messages, `[AI] Tu` when the message was generated by the
    [AI autoreply](../workflows/ai-autoreply.md);
 2. otherwise the trimmed, non-blank sender name;
 3. otherwise the user part of the sender JID (text before `@`).
 
-Colors (`message_style`, `src/ui.rs:334-340`): AI messages cyan, own messages
+Colors (`message_style`, `src/ui.rs:365-371`): AI messages cyan, own messages
 green, incoming messages default.
 
 ## Wrapping and widths
 
 Widths are measured in terminal cells through ratatui's `Line::width`, so
-wide characters (CJK, emoji) count as two cells (`src/ui.rs:410-422`).
-`wrap_text` (`src/ui.rs:428-455`) splits on `\n`, wraps each paragraph by
+wide characters (CJK, emoji) count as two cells (`src/ui.rs:441-453`).
+`wrap_text` (`src/ui.rs:459-486`) splits on `\n`, wraps each paragraph by
 characters (not words), drops control characters and always emits at least
 one row per paragraph.
 
 ## Tests
 
-Unit tests in `src/ui.rs:508-636` cover sender labels and fallbacks, clock
+Unit tests in `src/ui.rs:558-730` cover the footer key help (F4 highlighted
+only when the global autoreply is on), sender labels and fallbacks, clock
 formatting in a fixed time zone, QR text generation and its error/missing
 cases, wrapping, keeping the latest visible lines, `tail_fitting`, the
-messages title and selection clamping. See [Unit tests](../testing/unit-tests.md).
+messages title, the list title (global marker and filter) and selection
+clamping. See [Unit tests](../testing/unit-tests.md).

+ 49 - 28
openwiki/concepts/history-store.md

@@ -1,15 +1,15 @@
 ---
 type: concept
 title: Message history store
-description: The local SQLite database in src/history.rs that stores messages, chat summaries, contacts and the per-chat AI autoreply flag.
+description: The local SQLite database in src/history.rs that stores messages, chat summaries, contacts, the per-chat AI autoreply flag and the global autoreply setting, and supports local chat deletion.
 tags: [sqlite, rusqlite, history, persistence, schema]
 sources:
   - id: openwiki-source-ce0c4ea8cd3b15431fa691ce
     resource: repo://src/history.rs
-generated: { by: "claude-code", at: "2026-10-19T19:53:56.480Z" }
+generated: { by: "claude-code", at: "2026-10-20T20:27:06.515Z" }
 verified:
   - by: openwiki/0.7.2
-    at: 2026-10-19T19:53:56.480Z
+    at: 2026-10-20T20:27:06.515Z
 ---
 
 # Message history store
@@ -23,12 +23,13 @@ synchronously from the event loop.
 
 ## Opening
 
-- `History::open(path)` (`src/history.rs:60-69`) creates missing parent
+- `History::open(path)` (`src/history.rs:65-74`) creates missing parent
   directories, opens the file and runs the schema script.
-- `History::open_in_memory()` (`src/history.rs:76-81`) exists only under
+- `History::open_in_memory()` (`src/history.rs:80-86`) exists only under
   `#[cfg(test)]`.
 - The schema is applied with `CREATE TABLE/INDEX IF NOT EXISTS`
-  (`src/history.rs:285-311`); there are no migrations.
+  (`schema_sql`, `src/history.rs:344-374`); there are no migrations, so an
+  existing `history.db` simply gains the new `settings` table on next start.
 
 ## Schema
 
@@ -37,66 +38,86 @@ synchronously from the event loop.
 | `messages` | `message_id` (PK), `chat_jid`, `sender_jid`, `sender_name`, `text`, `timestamp`, `from_me`, `is_ai` | Index `idx_messages_chat_timestamp` on `(chat_jid, timestamp)` |
 | `chats` | `jid` (PK), `name`, `last_timestamp` (default 0), `autoreply` (default 0) | One row per chat |
 | `contacts` | `jid` (PK), `name` (NOT NULL) | Names from the address book / push names |
+| `settings` | `key` (PK), `value` (INTEGER NOT NULL) | App-wide flags; currently only `global_autoreply` |
 
-Rust types (`src/history.rs:23-53`): `StoredMessage`, `ChatSummary`
+Rust types (`src/history.rs:26-53`): `StoredMessage`, `ChatSummary`
 (`jid`, `name`, `last_timestamp`, `autoreply`) and `Contact` (`jid`, `name`).
 
 ## Saving messages and deduplication
 
-`save_message` (`src/history.rs:100-111`) runs in one transaction:
+`save_message` (`src/history.rs:105-116`) runs in one transaction:
 
 1. `INSERT OR IGNORE` into `messages` — the WhatsApp message id is the
-   primary key, so redelivered messages are ignored (`src/history.rs:317-338`).
+   primary key, so redelivered messages are ignored (`src/history.rs:380-402`).
 2. Only if the row was new, upsert the `chats` row
-   (`src/history.rs:345-358`): `last_timestamp` keeps the maximum seen and the
-   name is set only when the chat has no non-empty name yet.
+   (`upsert_chat_for_message`, `src/history.rs:408-421`): `last_timestamp`
+   keeps the maximum seen and the name is set only when the chat has no
+   non-empty name yet.
 3. Return `true` for a new message, `false` for a duplicate.
 
 The chat name candidate is the trimmed sender push name of an **incoming**
-message; own messages never name a chat (`src/history.rs:364-374`).
+message; own messages never name a chat (`chat_name_candidate`,
+`src/history.rs:427-437`).
 
 The boolean return value drives the app: duplicates do not refresh the UI or
 trigger the [AI autoreply](../workflows/ai-autoreply.md).
 
 ## Reading
 
-- `recent_messages(chat_jid, limit)` (`src/history.rs:117-135`): the latest
+- `recent_messages(chat_jid, limit)` (`src/history.rs:122-140`): the latest
   `limit` messages, queried newest first (`timestamp DESC, rowid DESC`) and
   reversed to ascending order for display (the open chat view) and for the
   last-message lookup of the startup catch-up.
-- `messages_since(chat_jid, since, limit)` (`src/history.rs:143-161`): the
+- `messages_since(chat_jid, since, limit)` (`src/history.rs:148-166`): the
   messages with `timestamp >= since` (inclusive), capped to the `limit` most
   recent ones (`timestamp DESC, rowid DESC LIMIT ?3`) and reversed to
   ascending order. The [AI autoreply](../workflows/ai-autoreply.md) uses it to
   load the context from local midnight of yesterday, at most
   `CONTEXT_MESSAGES` messages.
-- `list_chats()` (`src/history.rs:168-184`): all chats ordered by
+- `list_chats()` (`src/history.rs:173-189`): all chats ordered by
   `last_timestamp DESC`; the name is the contact name, else the chat name,
   else the user part of the JID (text before `@`).
-- `list_contacts()` (`src/history.rs:210-229`): ordered by name,
+- `list_contacts()` (`src/history.rs:215-234`): ordered by name,
   case-insensitive.
-- `display_name(jid)` (`src/history.rs:235-246`): same contact → chat → JID
+- `display_name(jid)` (`src/history.rs:240-251`): same contact → chat → JID
   user part fallback, used for the open chat title.
 
 ## Contacts
 
-`upsert_contact` (`src/history.rs:190-204`) inserts or renames a contact and
+`upsert_contact` (`src/history.rs:195-209`) inserts or renames a contact and
 ignores blank names, so an empty push name never erases a known name.
 
-## Autoreply flag
+## Autoreply flags
 
-- `set_autoreply(chat_jid, enabled)` (`src/history.rs:252-263`) upserts the
+- `set_autoreply(chat_jid, enabled)` (`src/history.rs:257-268`) upserts the
   `chats` row, creating it with defaults if the chat has no messages yet.
-- `is_autoreply(chat_jid)` (`src/history.rs:269-278`) returns the stored
+- `is_autoreply(chat_jid)` (`src/history.rs:274-283`) returns the stored
   flag or `false` when the chat is unknown.
+- `set_global_autoreply(enabled)` / `is_global_autoreply()`
+  (`src/history.rs:289-315`) upsert and read the `global_autoreply` row of
+  `settings`; the default is `false` when it was never stored. The global
+  flag is independent of the per-chat flags: toggling it never changes a
+  `chats.autoreply` value.
 
-New messages never reset the flag, because the message upsert only touches
-`last_timestamp` and `name`.
+New messages never reset the per-chat flag, because the message upsert only
+touches `last_timestamp` and `name`.
+
+## Deleting a chat
+
+`delete_chat(chat_jid)` (`src/history.rs:322-337`) removes, in one
+transaction, all messages of the chat and its `chats` row (and therefore its
+per-chat autoreply flag). The contact row, other chats and the WhatsApp
+session are untouched; deleting an unknown chat is not an error, and a later
+message simply recreates the chat. The app calls it after a double Delete
+press in the chat list (see [Application state](../architecture/app-state.md)).
 
 ## Tests
 
-`src/history.rs:418-663` covers duplicate ids, limit and ordering, `messages_since` (boundary inclusion, limit keeping the most recent, ascending order), flag and
-sender-name round trips, chat ordering, name fallbacks, chat naming rules,
-autoreply defaults and persistence, contact ordering and blank names, and
-on-disk persistence with parent directory creation. See
-[Unit tests](../testing/unit-tests.md).
+`src/history.rs:481-763` covers duplicate ids, limit and ordering,
+`messages_since` (boundary inclusion, limit keeping the most recent,
+ascending order), flag and sender-name round trips, chat ordering, name
+fallbacks, chat naming rules, per-chat autoreply defaults and persistence,
+the global autoreply default, toggle and independence from chat flags,
+`delete_chat` (messages and row gone, contact and other chats kept), contact
+ordering and blank names, and on-disk persistence with parent directory
+creation. See [Unit tests](../testing/unit-tests.md).

+ 1 - 1
openwiki/concepts/index.md

@@ -1,3 +1,3 @@
 # Files
 
-- [Message history store](history-store.md) - The local SQLite database in src/history.rs that stores messages, chat summaries, contacts and the per-chat AI autoreply flag.
+- [Message history store](history-store.md) - The local SQLite database in src/history.rs that stores messages, chat summaries, contacts, the per-chat AI autoreply flag and the global autoreply setting, and supports local chat deletion.

+ 1 - 1
openwiki/index.md

@@ -4,7 +4,7 @@ okf_version: "0.2"
 
 # Files
 
-- [Quickstart](quickstart.md) - What the wh WhatsApp terminal client with per-chat AI autoreply does, how to build and run it, and where to find each topic in this wiki.
+- [Quickstart](quickstart.md) - What the wh WhatsApp terminal client with per-chat and global AI autoreply and optional web control panel does, how to build and run it, and where to find each topic in this wiki.
 
 # Directories
 

+ 63 - 37
openwiki/integrations/ai-client.md

@@ -1,17 +1,17 @@
 ---
 type: integration
 title: AI chat-completions client
-description: The OpenAI-compatible HTTP client in src/ai.rs — request building, vision image input and validation, timeouts, error handling and reply parsing.
-tags: [ai, openai, reqwest, vision, http]
+description: The OpenAI-compatible HTTP client in src/ai.rs — request building, vision image input and validation, timeouts, retries with linear backoff, error handling, the optional request log and reply parsing.
+tags: [ai, openai, reqwest, vision, http, retry]
 sources:
   - id: openwiki-source-1b05507d7085e8e99a9b64f1
     resource: repo://src/ai.rs
   - id: openwiki-source-2a737474d86fc75cc9d9694f
     resource: repo://src/config.rs
-generated: { by: "claude-code", at: "2026-10-19T19:53:56.480Z" }
+generated: { by: "claude-code", at: "2026-10-20T20:27:06.515Z" }
 verified:
   - by: openwiki/0.7.2
-    at: 2026-10-19T19:53:56.480Z
+    at: 2026-10-20T20:27:06.515Z
 ---
 
 # AI chat-completions client
@@ -23,112 +23,138 @@ turns themselves are built elsewhere.
 
 ## Types
 
-- `Role` (`src/ai.rs:38-58`): `System`, `User`, `Assistant`, serialised as
+- `Role` (`src/ai.rs:42-58`): `System`, `User`, `Assistant`, serialised as
   `"system"`, `"user"`, `"assistant"`.
-- `ChatTurn { role, content }` (`src/ai.rs:60-65`): one message of the
+- `ChatTurn { role, content }` (`src/ai.rs:62-67`): one message of the
   conversation.
-- `ImageInput { mime, bytes }` (`src/ai.rs:95-100`): an image for vision
+- `ImageInput { mime, bytes }` (`src/ai.rs:119-124`): an image for vision
   requests.
-- `AiClient` (`src/ai.rs:67-77`): `reqwest::Client`, endpoint, API key,
-  model, optional vision model and optional log path.
+- `AiClient` (`src/ai.rs:69-81`): `reqwest::Client`, endpoint, API key,
+  model, optional vision model, optional log path and the retry count.
+- `StatusError { status, body }` (`src/ai.rs:83-100`): the typed error of a
+  non-2xx response, kept typed so the retry logic can inspect the status.
 
 Both `AiClient` and `ImageInput` have hand-written `Debug` impls that omit the
-API key and the image bytes (`src/ai.rs:79-114`).
+API key and the image bytes (`src/ai.rs:102-138`).
 
 ## Construction
 
-`AiClient::new(&Config)` (`src/ai.rs:123-146`):
+`AiClient::new(&Config)` (`src/ai.rs:147-171`):
 
 - requires `openai_api_key`; otherwise fails with
   `openai_api_key mancante in config.toml`;
 - builds the HTTP client with a 60 s request timeout
-  (`REQUEST_TIMEOUT_SECONDS`);
+  (`REQUEST_TIMEOUT_SECONDS`), applied to each attempt;
 - builds the endpoint as `openai_base_url` (trailing `/` trimmed) +
   `/chat/completions`;
-- copies `openai_model` and `openai_vision_model`;
+- copies `openai_model`, `openai_vision_model` and `ai_retries`;
 - sets the log path to `Config::ai_log_path()` only when `ai_log` is true.
 
 No network traffic happens until a completion is requested. Defaults and
 config keys are described in
 [Configuration and data](../operations/configuration-and-data.md)
 (default base URL `https://ollama.com/v1`, default model
-`deepseek-v4.1-flash`, `src/config.rs:23-25`).
+`deepseek-v4.1-flash`, default `ai_retries` 2, `src/config.rs:34-45`).
 
 ## Text completions
 
-`complete(&turns)` (`src/ai.rs:155-157`) sends
+`complete(&turns)` (`src/ai.rs:180-182`) sends
 
 ```json
 { "model": "<model>", "messages": [{ "role": "...", "content": "..." }] }
 ```
 
-built by `build_request_body` / `build_messages` (`src/ai.rs:251-265`).
+built by `build_request_body` / `build_messages` (`src/ai.rs:325-346`).
 
 ## Vision completions
 
-`has_vision()` (`src/ai.rs:163-165`) is true when a vision model is
-configured. `complete_with_image(&turns, &image)` (`src/ai.rs:175-188`):
+`has_vision()` (`src/ai.rs:188-190`) is true when a vision model is
+configured. `complete_with_image(&turns, &image)` (`src/ai.rs:200-213`):
 
 1. fails with `modello vision non configurato` without a vision model;
-2. validates the image (`src/ai.rs:311-323`): empty → `immagine vuota`,
-   larger than `MAX_IMAGE_BYTES` (5 MiB) → `immagine troppo grande: N byte`
-   (errors mention the size only, never the data);
+2. validates the image (`validate_image`, `src/ai.rs:385-399`): empty →
+   `immagine vuota`, larger than `MAX_IMAGE_BYTES` (5 MiB) →
+   `immagine troppo grande: N byte` (errors mention the size only, never the
+   data);
 3. sends a body using the vision model where the **last user turn** becomes
    `[{"type":"text",...}, {"type":"image_url",...}]`; with no user turn, a
    user message holding only the image is appended
-   (`src/ai.rs:274-290`).
+   (`build_vision_request_body`, `src/ai.rs:348-369`).
 
 The image is embedded as a `data:<mime>;base64,<...>` URL
-(`src/ai.rs:297-304`).
+(`build_image_part`, `src/ai.rs:371-379`). Validation errors happen before any
+request and are not retried.
+
+## Retries
+
+Both completion methods go through `send_with_retry` (`src/ai.rs:222-236`),
+which performs up to `1 + ai_retries` attempts with the same body:
+
+- after a failed attempt it retries only while attempts remain and
+  `is_retryable` (`src/ai.rs:301-309`) accepts the error: HTTP 429 and 5xx
+  responses and every non-status failure (transport errors, timeouts, body
+  read errors, invalid JSON, missing or empty reply) are retried; any other
+  4xx response (e.g. 401, 400) fails immediately;
+- before retry number *n* it sleeps `retry_delay(n)` = 2 s × n
+  (`src/ai.rs:316-318`), i.e. 2 s, 4 s, … (linear backoff);
+- the returned error is the one of the last attempt.
+
+`ai_retries = 0` disables retrying. Each attempt is a separate HTTP request
+and, when enabled, a separate log record. With vision, the retries apply to
+the vision request; the courtesy fallback in the
+[autoreply workflow](../workflows/ai-autoreply.md) starts only after they are
+exhausted and has its own retries.
 
 ## Sending and error handling
 
-`send_body` (`src/ai.rs:200-209`) delegates to `request_reply`
-(`src/ai.rs:217-243`), which posts JSON with bearer auth and returns
+`send_body` (`src/ai.rs:248-257`) delegates to `request_reply`
+(`src/ai.rs:265-291`), which posts JSON with bearer auth and returns
 Italian `anyhow` errors:
 
 | Failure | Message |
 |---|---|
 | Transport error | `richiesta al servizio AI fallita` |
 | Body read error | `lettura della risposta del servizio AI fallita` |
-| Non-2xx status | `servizio AI ha risposto <status>: <body>` |
+| Non-2xx status | `servizio AI ha risposto <status>: <body>` (a `StatusError`) |
 | Invalid JSON | `risposta del servizio AI non valida: <body>` |
 
 Response bodies in errors are truncated to 300 characters on a character
-boundary (`truncate_chars`, `src/ai.rs:448-450`); the API key is never
+boundary (`truncate_chars`, `src/ai.rs:522-524`); the API key is never
 included.
 
 ## Optional request log
 
 When `ai_log = true` in `config.toml` (default `false`), `send_body` appends
-one record per request to `ai_requests.log` in the data directory
-(`Config::ai_log_path`, `src/config.rs:283-285`) after the request completes
-(`src/ai.rs:200-209`). `format_log_record` (`src/ai.rs:406-421`) writes a
-header `===== <local timestamp> | <endpoint> | modello: <model> =====`, the
+one record per attempt to `ai_requests.log` in the data directory
+(`Config::ai_log_path`, `src/config.rs:318-320`) after the attempt completes.
+`format_log_record` (`src/ai.rs:480-499`) writes a header
+`===== <local timestamp> | <endpoint> | modello: <model> =====`, the
 pretty-printed request body and the outcome labelled `RISPOSTA` or `ERRORE`.
 
 - Image data URLs are replaced by `data:<mime>;base64,<omessi N byte>`
-  (`redact_images` / `redact_data_url`, `src/ai.rs:356-397`); text parts and
+  (`redact_images` / `redact_data_url`, `src/ai.rs:430-472`); text parts and
   remote URLs are kept.
 - The API key is never written (it is only in the bearer header, not in the
   body).
 - The file is opened in append mode and created if missing
-  (`append_to_file`, `src/ai.rs:436-442`); write failures are ignored so
+  (`append_to_file`, `src/ai.rs:510-516`); write failures are ignored so
   logging never makes a reply fail and never writes over the TUI.
 
 ## Reply parsing
 
-`parse_reply` (`src/ai.rs:331-348`) takes `choices[0].message.content`,
+`parse_reply` (`src/ai.rs:405-422`) takes `choices[0].message.content`,
 trims it and fails with `risposta del servizio AI senza choices` when there
 are no choices or `risposta del servizio AI vuota` when the content is
 missing or blank.
 
 ## Tests
 
-`src/ai.rs:452-739` covers vision body shapes, missing vision model,
+`src/ai.rs:526-850` covers vision body shapes, missing vision model,
 oversize/empty images (without leaking data), the exact size limit,
 `has_vision`, `Debug` redaction, request body shape, reply parsing edge
-cases, the missing-key error, endpoint construction, truncation, and the
+cases, the missing-key error, endpoint construction, truncation, the
 request log (image redaction, no key or base64 in records, logging only when
-enabled). See
+enabled, append-only writes) and the retry policy (`is_retryable` refuses
+4xx except 429 and accepts generic errors, the `StatusError` message is
+unchanged, `retry_delay` grows linearly). See
 [Unit tests](../testing/unit-tests.md).

+ 2 - 1
openwiki/integrations/index.md

@@ -1,4 +1,5 @@
 # Files
 
-- [AI chat-completions client](ai-client.md) - The OpenAI-compatible HTTP client in src/ai.rs — request building, vision image input and validation, timeouts, error handling and reply parsing.
+- [AI chat-completions client](ai-client.md) - The OpenAI-compatible HTTP client in src/ai.rs — request building, vision image input and validation, timeouts, retries with linear backoff, error handling, the optional request log and reply parsing.
+- [Web control panel](web-control-panel.md) - The optional HTTP control panel in src/web.rs and src/web_page.html, enabled by web_port — routes, the custom-header guard on POST, the request/reply bridge to App through AppEvent::Web, the quit flow, the browser page and its security caveats.
 - [WhatsApp client integration](whatsapp-client.md) - How src/whatsapp.rs wraps whatsapp-rust — session startup, translation of raw events into WaEvent, JID canonicalization, contact names, history sync, message rendering, image download and sending.

+ 151 - 0
openwiki/integrations/web-control-panel.md

@@ -0,0 +1,151 @@
+---
+type: integration
+title: Web control panel
+description: The optional HTTP control panel in src/web.rs and src/web_page.html, enabled by web_port — routes, the custom-header guard on POST, the request/reply bridge to App through AppEvent::Web, the quit flow, the browser page and its security caveats.
+tags: [web, http, tiny_http, control-panel, security, autoreply]
+verified:
+  - by: openwiki/0.7.2
+    at: 2026-10-20T20:27:06.515Z
+sources:
+  - id: openwiki-source-9fec9d1af408501c9c455161
+    resource: repo://src/app.rs
+  - id: openwiki-source-b55a21a31ede1b56cd31a6a6
+    resource: repo://src/main.rs
+  - id: openwiki-source-ac7a3eaa396cc3aa88c00eef
+    resource: repo://src/web_page.html
+  - id: openwiki-source-7b75006986d8b0337adc6ca0
+    resource: repo://src/web.rs
+generated: { by: "claude-code", at: "2026-10-20T20:27:06.515Z" }
+---
+
+# Web control panel
+
+`src/web.rs` is an optional, minimal HTTP server that lets a browser (for
+example a phone on the same network) see the chats and toggle the global and
+per-chat AI autoreply, or quit the bot. It is off unless `web_port` is set in
+`config.toml` (see [Configuration and data](../operations/configuration-and-data.md)).
+It has **no authentication** and listens on **all interfaces** (`0.0.0.0`,
+`src/web.rs:1-2`, `src/web.rs:16-17`).
+
+## Startup and threading
+
+`main` calls `start_web_panel`, which calls `web::start(port, app_tx)` only
+when `web_port` is set ([Runtime and event loop](../architecture/runtime-and-event-loop.md)).
+`start` (`src/web.rs:165-173`) binds a `tiny_http::Server` on
+`0.0.0.0:<port>` and spawns a detached OS thread named `wh-web`. A bind
+failure aborts startup with `cannot start web server on port <port>`.
+
+`serve` (`src/web.rs:179-186`) handles requests **one at a time** on that
+thread and stops after a request once the application channel is closed.
+`tiny_http` is the only extra dependency (`Cargo.toml:24-25`).
+
+## Routes
+
+`resolve_route` (`src/web.rs:325-334`) matches method and path; the query
+string is ignored (`path_without_query`):
+
+| Method | Path | Effect | Response |
+|---|---|---|---|
+| GET | `/` | Serves the embedded page (`include_str!("web_page.html")`) | 200 HTML |
+| GET | `/api/state` | `WebCommand::State` | 200 JSON state |
+| POST | `/api/global` | `WebCommand::ToggleGlobal` | 200 JSON state |
+| POST | `/api/chat` | `WebCommand::ToggleChat(<body>)`; body = chat JID as plain text | 200 JSON state, 400 on bad body |
+| POST | `/api/quit` | `WebCommand::Quit` | 200 `{"status":"Bot in chiusura"}` |
+| other | other | — | 404 `non trovato` |
+
+The JSON state is `WebState` (`src/web.rs:92-112`):
+
+```json
+{ "global_autoreply": false,
+  "chats": [ { "jid": "...", "name": "...", "autoreply": true } ],
+  "status": "<TUI status line>" }
+```
+
+### Request checks
+
+- **Custom header on POST** — every POST route must carry the header
+  `X-Wh-Request: 1` (name case-insensitive, value exact); otherwise the
+  server answers 403 `richiesta non autorizzata` and forwards nothing
+  (`handle_request`, `has_request_header`, `src/web.rs:193-205`,
+  `src/web.rs:341-345`). A cross-site page cannot add a custom header without
+  a CORS preflight, which the server never approves, so this blocks simple
+  cross-site form/fetch requests (CSRF). It is **not** authentication: anyone
+  who can reach the port can send the header.
+- **Body limit** — `/api/chat` reads at most 1024 bytes
+  (`MAX_BODY_BYTES`); an oversized, empty (after trimming), unreadable or
+  non-UTF-8 body yields 400 `corpo della richiesta non valido`
+  (`read_limited_body`, `src/web.rs:352-364`).
+
+## Bridge to the application
+
+The server never touches application state. `forward` (`src/web.rs:252-261`)
+sends `AppEvent::Web(WebRequest { command, reply })` with a tokio `oneshot`
+sender and blocks the web thread (`blocking_recv`) until `App` answers.
+`App::handle_web_request` runs inside the normal event loop, so web commands
+are serialised with keys and WhatsApp events
+([Application state](../architecture/app-state.md#web-panel-requests)):
+
+- `ToggleGlobal` follows the same rules as F4 (enabling needs a configured
+  AI client; the flag is persisted in the
+  [history store](../concepts/history-store.md));
+- `ToggleChat(jid)` is accepted only for JIDs present in the panel's chat
+  list — otherwise only the status becomes `Chat non trovata` — and then
+  follows the same rules as F3 (AI configured, private chat or group);
+- the reply is always the state after the command; refusals are visible
+  only through `status`.
+
+The panel's chat list (`web_chats`) is the TUI chat list in the same order
+(most recent first), restricted to chats that support autoreply (private
+chats and groups). The `autoreply` field is the **per-chat** flag; the global
+flag is reported separately.
+
+If the event channel is closed or the reply is dropped, the server answers
+503 `bot non disponibile`; a serialisation failure gives 500.
+
+### Quit
+
+`handle_quit` (`src/web.rs:234-246`) first writes the 200 response, then
+sends `WebCommand::Quit` without waiting for a reply, because the process
+exits as soon as `App` sets `should_quit` and the event loop ends. The TUI
+terminal is restored as on F10.
+
+## Browser page
+
+`src/web_page.html` is a self-contained page (inline CSS and JS, Italian
+UI) with a global "AI tutte" switch, one switch per chat, a quit button and
+a status bar:
+
+- it polls `GET /api/state` every 5 s while the tab is visible and refreshes
+  when it becomes visible again (`src/web_page.html:330`,
+  `src/web_page.html:612-635`);
+- every POST carries `X-Wh-Request: 1`; the chat toggle sends the JID as a
+  `text/plain` body (`buildHeaders`, `src/web_page.html:377-382`);
+- while a toggle is in flight its switch is disabled and keeps the user's
+  position; polling is paused, and responses are applied in request order
+  through a ticket counter so a stale poll cannot overwrite a newer toggle
+  result (`src/web_page.html:534-577`);
+- names are inserted with `textContent` only;
+- quit asks for confirmation (`Chiudere il bot?`) and then shows a
+  full-screen "Bot chiuso" overlay and stops polling; a network error after
+  a quit request is treated as the bot having exited
+  (`src/web_page.html:417-439`, `src/web_page.html:597-610`).
+
+## Security and operations
+
+- No authentication and no TLS: anyone who can reach `<host>:<web_port>` can
+  read chat names and JIDs, toggle autoreplies and stop the bot. Enable it
+  only on a trusted network, or restrict the port with the host firewall.
+- The server binds to every interface; there is no setting for the bind
+  address.
+- `web_port = 0` is rejected at startup; values above 65535 or negative are
+  rejected by TOML parsing.
+- Requests are served sequentially and each API request waits for the event
+  loop, so a slow request delays the others.
+
+## Tests
+
+`src/web.rs:366-445` covers query stripping, route resolution (known routes,
+wrong method or path), the custom header check (any case accepted, missing
+or wrong value rejected), which routes require the header, and body reading
+(trimming, empty/invalid UTF-8 rejection, the 1024-byte limit). `web_chats`
+filtering is tested in `src/app.rs`. See [Unit tests](../testing/unit-tests.md).

+ 49 - 39
openwiki/integrations/whatsapp-client.md

@@ -8,10 +8,10 @@ sources:
     resource: repo://Cargo.toml
   - id: openwiki-source-7ea5eac45e5030178a6ea05b
     resource: repo://src/whatsapp.rs
-generated: { by: "claude-code", at: "2026-10-19T19:53:56.480Z" }
+generated: { by: "claude-code", at: "2026-10-20T20:27:06.515Z" }
 verified:
   - by: openwiki/0.7.2
-    at: 2026-10-19T19:53:56.480Z
+    at: 2026-10-20T20:27:06.515Z
 ---
 
 # WhatsApp client integration
@@ -27,7 +27,7 @@ verified:
   send text, download images and query its own JIDs.
 
 `rusqlite` is pinned so that it shares `libsqlite3-sys` with the `diesel`
-backend of `whatsapp-rust-sqlite-storage` (`Cargo.toml:14`).
+backend of `whatsapp-rust-sqlite-storage` (`Cargo.toml:14-15`).
 
 ## Startup
 
@@ -43,7 +43,7 @@ backend of `whatsapp-rust-sqlite-storage` (`Cargo.toml:14`).
 `EventForwarder` (`src/whatsapp.rs:247-268`) only pushes raw events, in
 order, into an internal unbounded channel; its interest set is
 `SUBSCRIBED_KINDS` (`src/whatsapp.rs:51-70`), which includes
-`OfflineSyncCompleted`. All translation happens in
+`OfflineSyncCompleted` and `AppStateSyncFailed`. All translation happens in
 `Translator`, which also owns a name cache.
 
 ```
@@ -52,7 +52,7 @@ whatsapp-rust Bot --raw events--> EventForwarder --chan--> Translator --WaEvent-
 
 ## WaEvent
 
-`WaEvent` (`src/whatsapp.rs:81-110`):
+`WaEvent` (`src/whatsapp.rs:81-111`):
 
 | Variant | Meaning |
 |---|---|
@@ -65,20 +65,28 @@ whatsapp-rust Bot --raw events--> EventForwarder --chan--> Translator --WaEvent-
 | `Contact(Contact)` | A new or better contact name |
 | `Error(String)` | Non-fatal problem shown in the status line |
 
-`Translator::handle_event` (`src/whatsapp.rs:416-456`) maps raw events to
+`Translator::handle_event` (`src/whatsapp.rs:416-462`) maps raw events to
 these variants (`Event::OfflineSyncCompleted` becomes
 `WaEvent::OfflineSyncCompleted`). Failures become `Error` texts such as
 `pairing QR codes exhausted; restart to pair again`, `pairing failed: ...`,
 `WhatsApp rejected this client version (client outdated)`,
-`account temporarily banned: ...`, `connection failure: ...`,
-`stream error: ...` and `could not decrypt a message from ...`;
-`StreamReplaced` becomes `Disconnected("stream replaced by another session")`.
+`account temporarily banned: <code> (expires in Ns)`,
+`connection failure: <reason> (<message>)` (`describe_connect_failure`,
+`src/whatsapp.rs:801-806`), `stream error: ...` and
+`could not decrypt a message from ...`; `StreamReplaced` becomes
+`Disconnected("stream replaced by another session")`.
+
+`AppStateSyncFailed` is reported only when at least one collection failed
+**fatally** (`is_fatal_sync_failure`, `src/whatsapp.rs:813-815`), as
+`app state sync incomplete (fatal: [...], retryable: [...])`; retryable or
+skipped collections are retried by the library on a forced reconnect and are
+not shown.
 
 ## JIDs
 
 - JIDs are stored as `user@server` without device suffix
-  (`jid_to_string` uses `to_non_ad_string`, `src/whatsapp.rs:815-817`).
-- `canonical_jid` (`src/whatsapp.rs:369-384`) prefers the phone-number (PN)
+  (`jid_to_string` uses `to_non_ad_string`, `src/whatsapp.rs:830-832`).
+- `canonical_jid` (`src/whatsapp.rs:369-389`) prefers the phone-number (PN)
   form: non-LID JIDs are kept; a LID uses the alternate PN JID from the
   message, else the store's LID→PN mapping; on lookup failure the LID is
   kept and `LID lookup failed for ...` is reported.
@@ -87,33 +95,33 @@ these variants (`Event::OfflineSyncCompleted` becomes
 
 ## Live messages
 
-`handle_live_message` (`src/whatsapp.rs:472-519`):
+`handle_live_message` (`src/whatsapp.rs:478-525`):
 
 - skips `status@broadcast` and messages with no renderable content;
 - keeps group JIDs as the chat; for 1:1 chats canonicalizes using
   `recipient_alt` for own messages and `sender_alt` otherwise;
 - records the sender push name as a contact name (`push_name_of` drops own
-  and empty names, `src/whatsapp.rs:807-810`);
+  and empty names, `src/whatsapp.rs:822-825`);
 - attaches an `ImageRef` only when the rendered text is an image text;
 - emits `StoredMessage` with the WhatsApp timestamp and `is_ai = false`,
   plus `addressed_to` from `addressed_jids`.
 
-`addressed_jids` (`src/whatsapp.rs:892-919`) reads the `ContextInfo` of the
+`addressed_jids` (`src/whatsapp.rs:927-934`) reads the `ContextInfo` of the
 first content kind that carries one (extended text, image, video, audio,
-document, sticker) and returns the `mentioned_jid` list followed by the
-quoted message's `participant` — the latter only when the message actually
-quotes one (`quoted_message` or `stanza_id` set). The JIDs are raw (not
-canonicalized); the [AI autoreply](../workflows/ai-autoreply.md) compares
-them with the own JIDs to decide whether a group message is addressed to
-the user.
+document, sticker; `context_info_of`, `src/whatsapp.rs:907-919`) and returns
+the `mentioned_jid` list followed by the quoted message's `participant` —
+the latter only when the message actually quotes one (`quoted_message` or
+`stanza_id` set). The JIDs are raw (not canonicalized); the
+[AI autoreply](../workflows/ai-autoreply.md) compares them with the own JIDs
+to decide whether a group message is addressed to the user.
 
 ## Contact names
 
 Names come from two sources ranked `Push < Saved` (`NameSource`,
-`src/whatsapp.rs:270-277`): push names from messages and `PushNameUpdate`,
+`src/whatsapp.rs:270-278`): push names from messages and `PushNameUpdate`,
 saved names from `ContactUpdate` (full name, else first name,
-`src/whatsapp.rs:524-546`) and from history sync. `note_name`
-(`src/whatsapp.rs:332-350`) ignores empty names, never lets a push name
+`src/whatsapp.rs:530-544`) and from history sync. `note_name`
+(`src/whatsapp.rs:332-354`) ignores empty names, never lets a push name
 override a saved one, skips unchanged names and otherwise emits
 `WaEvent::Contact`. The [history store](../concepts/history-store.md)
 persists them.
@@ -121,31 +129,33 @@ persists them.
 ## History sync
 
 Each `HistorySync` chunk is processed in its own task
-(`handle_history`, `src/whatsapp.rs:553-573`). Decoding (inflate + protobuf)
-runs in `spawn_blocking` via `extract_history` (`src/whatsapp.rs:703-744`).
-`emit_history` (`src/whatsapp.rs:579-646`) then emits push names, inline
+(`handle_history`, `src/whatsapp.rs:559-579`). Decoding (inflate + protobuf)
+runs in `spawn_blocking` via `extract_history` (`src/whatsapp.rs:709-750`).
+`emit_history` (`src/whatsapp.rs:585-652`) then emits push names, inline
 address-book contacts, conversation names and every renderable message.
 History messages never carry an image reference (so they cannot trigger
-vision replies) and always have an empty `addressed_to`; the sender is the own PN for own messages, otherwise the
-group participant or the chat. Undecodable conversations are reported as
+vision replies) and always have an empty `addressed_to`; the sender is the
+own PN for own messages, otherwise the group participant or the chat.
+Undecodable conversations are reported as
 `history sync skipped N undecodable conversations`.
 
 ## Message rendering
 
-`describe_message` (`src/whatsapp.rs:826-844`) turns a protobuf message into
+`describe_message` (`src/whatsapp.rs:841-853`) turns a protobuf message into
 text, in this order:
 
 1. plain or extended text;
 2. image → `[immagine]` or `[immagine] <caption>`; audio (including voice
-   notes) → `[messaggio vocale]` (`src/whatsapp.rs:846-858`);
+   notes) → `[messaggio vocale]` (`describe_image_or_audio`,
+   `src/whatsapp.rs:861-873`);
 3. caption of other media;
-4. `describe_other` (`src/whatsapp.rs:866-884`): protocol, reaction,
+4. `describe_other` (`src/whatsapp.rs:881-899`): protocol, reaction,
    encrypted reaction, poll vote and sender-key distribution messages are
    silent (`None`, not stored); video, document, sticker and PTV →
    `[media]`; anything else → `[messaggio non supportato]`.
 
 Helpers used by the [AI autoreply](../workflows/ai-autoreply.md)
-(`src/whatsapp.rs:944-988`): `is_placeholder` (only `[media]` and
+(`src/whatsapp.rs:951-1003`): `is_placeholder` (only `[media]` and
 `[messaggio non supportato]`), `is_voice_note`, `is_image` and
 `image_caption`.
 
@@ -156,13 +166,13 @@ Helpers used by the [AI autoreply](../workflows/ai-autoreply.md)
 - `WaHandle::download_image(&ImageRef)` (`src/whatsapp.rs:173-186`) downloads
   and decrypts the image into memory (`DownloadedImage`), falling back to
   `image/jpeg` when the MIME type is blank. Images are never written to disk,
-  and `ImageRef`'s `Debug` omits key material (`src/whatsapp.rs:112-129`).
+  and `ImageRef`'s `Debug` omits key material (`src/whatsapp.rs:112-131`).
 
 ## Tests
 
-`src/whatsapp.rs:990-1282` covers `addressed_jids` (mentions, quoted
-participant, participant without quote, media context info, plain text),
-text and extended text, captions and
-placeholders, image and voice rendering, image references, placeholder
-helpers, silent reactions/protocol messages, JID device-suffix removal and
-push-name filtering. See [Unit tests](../testing/unit-tests.md).
+`src/whatsapp.rs:1005+` covers `is_fatal_sync_failure`, `addressed_jids`
+(mentions, quoted participant, participant without quote, media context
+info, plain text), text and extended text, captions and placeholders, image
+and voice rendering, image references, placeholder helpers, silent
+reactions/protocol messages, JID device-suffix removal and push-name
+filtering. See [Unit tests](../testing/unit-tests.md).

+ 56 - 33
openwiki/operations/configuration-and-data.md

@@ -1,40 +1,42 @@
 ---
 type: operations
 title: Configuration and data files
-description: How src/config.rs loads config.toml, the supported keys and defaults, where the data directory and databases live, and operational and security caveats.
+description: How src/config.rs loads config.toml, the supported keys and defaults (including ai_retries and web_port), the template file, where the data directory and databases live, and operational and security caveats.
 tags: [configuration, toml, data-directory, sqlite, security]
 sources:
   - id: openwiki-source-ea70eb6c045047448e446296
     resource: repo://.gitignore
+  - id: openwiki-source-0d5b861992295d2e1663735c
+    resource: repo://config.toml.global.template
   - id: openwiki-source-1b05507d7085e8e99a9b64f1
     resource: repo://src/ai.rs
   - id: openwiki-source-2a737474d86fc75cc9d9694f
     resource: repo://src/config.rs
-generated: { by: "claude-code", at: "2026-10-19T19:53:56.480Z" }
+generated: { by: "claude-code", at: "2026-10-20T20:27:06.515Z" }
 verified:
   - by: openwiki/0.7.2
-    at: 2026-10-19T19:53:56.480Z
+    at: 2026-10-20T20:27:06.515Z
 ---
 
 # Configuration and data files
 
 All runtime settings come from one TOML file loaded by `Config::load`
-(`src/config.rs:226-230`). No environment variable is ever read
-(`src/config.rs:1-20`).
+(`src/config.rs:259-263`). No environment variable is ever read
+(`src/config.rs:1-25`).
 
 ## Data directory
 
 The data directory is fixed: `<platform config dir>/wh`, resolved with
-`dirs::config_dir()` (`src/config.rs:144-148`). On Windows this is
+`dirs::config_dir()` (`src/config.rs:166-170`). On Windows this is
 `%APPDATA%\wh`. It cannot be changed from the config file. At startup
-`ensure_data_dir` creates it if missing (`src/config.rs:258-261`).
+`ensure_data_dir` creates it if missing (`src/config.rs:293-296`).
 
 | File | Purpose | Defined by |
 |---|---|---|
-| `config.toml` | Optional settings | `CONFIG_FILE`, `src/config.rs:49` |
-| `session.db` | WhatsApp session store used by `whatsapp-rust` ([WhatsApp client](../integrations/whatsapp-client.md)) | `session_db_path`, `src/config.rs:267-269` |
-| `history.db` | Local messages, chats, contacts and autoreply flags ([History store](../concepts/history-store.md)) | `history_db_path`, `src/config.rs:275-277` |
-| `ai_requests.log` | Optional AI request log, written only when `ai_log = true` ([AI client](../integrations/ai-client.md)) | `ai_log_path`, `src/config.rs:283-285` |
+| `config.toml` | Optional settings | `CONFIG_FILE`, `src/config.rs:56` |
+| `session.db` | WhatsApp session store used by `whatsapp-rust` ([WhatsApp client](../integrations/whatsapp-client.md)) | `session_db_path`, `src/config.rs:302-304` |
+| `history.db` | Local messages, chats, contacts, per-chat autoreply flags and the global autoreply setting ([History store](../concepts/history-store.md)) | `history_db_path`, `src/config.rs:310-312` |
+| `ai_requests.log` | Optional AI request log, written only when `ai_log = true` ([AI client](../integrations/ai-client.md)) | `ai_log_path`, `src/config.rs:318-320` |
 
 The two databases are separate by design: `session.db` belongs to the
 WhatsApp library (linked-device session and keys, schema owned by
@@ -54,29 +56,44 @@ contacts, autoreply flags).
 | `cooldown_seconds` | Minimum seconds between autoreplies in a private chat or to a group message addressed to the user; `0` disables it | `30` |
 | `group_cooldown_seconds` | Same, for group messages not addressed to the user; `0` disables it | `300` |
 | `catch_up_seconds` | After a restart, the last unanswered message of each private chat is answered once if it is at most this old; `0` disables the catch-up | `7200` |
-
-Mapping and defaults: `src/config.rs:236-252`, `src/config.rs:29-38`.
-The three timing keys are unsigned integers in the file (`Option<u32>`,
-`src/config.rs:104-111`), so a negative value is rejected as an invalid
-value type; they are stored as `i64` in `Config`. Their use is described
-in [AI autoreply](../workflows/ai-autoreply.md).
+| `ai_retries` | How many times a failed AI request is retried, waiting 2 s, 4 s, …; HTTP 4xx other than 429 is never retried; `0` disables retrying | `2` |
+| `web_port` | TCP port of the [web control panel](../integrations/web-control-panel.md) on `0.0.0.0`, no authentication; valid range 1–65535 | unset — panel disabled |
+
+Mapping and defaults: `Config::from_file` (`src/config.rs:269-287`) and the
+constants at `src/config.rs:34-45`. The timing keys and `ai_retries` are
+unsigned integers in the file (`Option<u32>`, `src/config.rs:121-128`), so a
+negative value is rejected as an invalid value type; the timing keys are
+stored as `i64` in `Config`. `web_port` is `Option<u16>`
+(`src/config.rs:129-131`), so negatives and values above 65535 fail TOML
+parsing, and `resolve_web_port` (`src/config.rs:243-252`) additionally
+rejects `0`. Their use is described in
+[AI autoreply](../workflows/ai-autoreply.md), [AI client](../integrations/ai-client.md)
+and [Web control panel](../integrations/web-control-panel.md).
 The binary embeds no prompt text: without `system_prompt` the AI is not
 configured and the status line says so (see
 [Application state](../architecture/app-state.md)). Unknown keys are ignored.
 
+`config.toml.global.template` in the repository root lists every key with
+its default and a comment (with `web_port` commented out); copy it to the
+data directory as `config.toml` and fill in `openai_api_key` and
+`system_prompt`.
+
 ## Loading rules
 
-- Missing file → all defaults (`src/config.rs:180-190`).
+- Missing file → all defaults (`read_config_file`, `src/config.rs:202-212`).
 - Unreadable file → `cannot read config file <path>`.
 - Invalid TOML or wrong value type → `invalid config file <path>` with the
   cause `invalid TOML syntax or value type at line L, column C`; the error
-  never echoes file content (`src/config.rs:165-174`). This also covers
-  negative or non-integer timing keys and a non-boolean `ai_log`.
+  never echoes file content (`parse_config_file`, `src/config.rs:187-196`).
+  This also covers negative or non-integer timing keys and `ai_retries`, a
+  non-boolean `ai_log` and an out-of-range `web_port`.
 - A leading UTF-8 BOM (common with Windows editors) and CRLF line endings
   are tolerated.
 - `openai_base_url` consisting only of slashes is rejected with
   `openai_base_url in config.toml must not be empty or only slashes`
-  (`src/config.rs:212-219`).
+  (`src/config.rs:234-241`).
+- `web_port = 0` is rejected with
+  `web_port in config.toml must be between 1 and 65535`.
 
 Startup aborts with `cannot load configuration` on any of these errors
 ([Runtime](../architecture/runtime-and-event-loop.md)).
@@ -84,29 +101,35 @@ Startup aborts with `cannot load configuration` on any of these errors
 ## Security and operational notes
 
 - The API key is never printed: `Config`'s `Debug` shows `***` or `<unset>`
-  (`src/config.rs:114-138`), and the raw `FileConfig` has no `Debug` impl at
-  all (`src/config.rs:90-112`). The AI client redacts it too.
+  (`src/config.rs:134-160`), and the raw `FileConfig` has no `Debug` impl at
+  all (`src/config.rs:105-132`). The AI client redacts it too.
 - `ai_requests.log` never contains the API key and replaces image data
   with a size placeholder, but it does store the full request (system
-  prompt and chat context) and the reply in clear text, and it is only
-  appended to, never rotated. Enable `ai_log` only for diagnosis and delete
-  the file afterwards.
+  prompt and chat context) and the reply in clear text, one record per
+  attempt including retries, and it is only appended to, never rotated.
+  Enable `ai_log` only for diagnosis and delete the file afterwards.
+- `web_port` exposes an unauthenticated HTTP panel on every network
+  interface: anyone who can reach the port can see chat names, toggle
+  autoreplies and quit the bot. Set it only on a trusted network or
+  firewall the port. A port already in use aborts startup with
+  `cannot start web server on port <port>`.
 - Keep `config.toml` in the data directory, outside the repository.
   `.gitignore` ignores a root-level `/config.toml` and `.env`
   (`.gitignore:4-6`), as well as SQLite journal/WAL/SHM files; note that the
   `*.db` pattern is commented out (`.gitignore:7`), so databases copied into
   the repo would not be ignored.
 - `session.db` holds the linked-device credentials: deleting it forces a new
-  QR pairing. `history.db` can be deleted to reset local history and
-  autoreply flags; it is recreated on start.
+  QR pairing. `history.db` can be deleted to reset local history and the
+  per-chat and global autoreply flags; it is recreated on start.
 - Images downloaded for vision replies are kept in memory only and never
   written to the data directory.
 
 ## Tests
 
-`src/config.rs:288+` covers defaults, full files, blank values, multiline
+`src/config.rs:323+` covers defaults, full files, blank values, multiline
 prompts, trimming, base URL normalisation and errors, `Debug` masking,
-vision model, DB and AI log paths, `ai_log`, cooldown and catch-up defaults,
-custom values (including `0`) and rejection of negatives, unknown keys, BOM/CRLF, non-leaking parse errors,
-missing, existing, invalid and unreadable files. See
-[Unit tests](../testing/unit-tests.md).
+vision model, DB and AI log paths, `ai_log`, cooldown, catch-up and
+`ai_retries` defaults, custom values (including `0`) and rejection of
+negatives, `web_port` (unset, valid, `0` and out-of-range rejected), unknown
+keys, BOM/CRLF, non-leaking parse errors, missing, existing, invalid and
+unreadable files. See [Unit tests](../testing/unit-tests.md).

+ 1 - 1
openwiki/operations/index.md

@@ -1,3 +1,3 @@
 # Files
 
-- [Configuration and data files](configuration-and-data.md) - How src/config.rs loads config.toml, the supported keys and defaults, where the data directory and databases live, and operational and security caveats.
+- [Configuration and data files](configuration-and-data.md) - How src/config.rs loads config.toml, the supported keys and defaults (including ai_retries and web_port), the template file, where the data directory and databases live, and operational and security caveats.

+ 32 - 23
openwiki/quickstart.md

@@ -1,8 +1,8 @@
 ---
 type: overview
 title: Quickstart
-description: What the wh WhatsApp terminal client with per-chat AI autoreply does, how to build and run it, and where to find each topic in this wiki.
-tags: [overview, quickstart, rust, whatsapp, tui, ai]
+description: What the wh WhatsApp terminal client with per-chat and global AI autoreply and optional web control panel does, how to build and run it, and where to find each topic in this wiki.
+tags: [overview, quickstart, rust, whatsapp, tui, ai, web-panel]
 sources:
   - id: openwiki-source-651d1fb6c9e49916a916ab51
     resource: repo://Cargo.toml
@@ -14,10 +14,10 @@ sources:
     resource: repo://src/config.rs
   - id: openwiki-source-b55a21a31ede1b56cd31a6a6
     resource: repo://src/main.rs
-generated: { by: "claude-code", at: "2026-10-19T19:53:56.480Z" }
+generated: { by: "claude-code", at: "2026-10-20T20:27:06.515Z" }
 verified:
   - by: openwiki/0.7.2
-    at: 2026-10-19T19:53:56.480Z
+    at: 2026-10-20T20:27:06.515Z
 ---
 
 # Quickstart
@@ -26,8 +26,10 @@ verified:
 companion device by QR code, shows chats and contacts in a
 [ratatui](architecture/terminal-ui.md) interface, stores messages locally in
 SQLite and can answer incoming messages automatically with an
-OpenAI-compatible model, chat by chat — in private chats and in groups
-(broadcasts, status and newsletters excluded).
+OpenAI-compatible model — chat by chat or for all chats at once — in private
+chats and in groups (broadcasts, status and newsletters excluded). An
+optional [web control panel](integrations/web-control-panel.md) lets a
+browser toggle the autoreplies and stop the bot.
 
 ## Build and run
 
@@ -39,46 +41,53 @@ cargo test
 
 The crate uses edition 2024. `whatsapp-rust` is built with
 `default-features = false` because its default `simd` feature needs nightly
-(`Cargo.toml:8-10`). `[lints.rust] linker_messages = "allow"`
-(`Cargo.toml:25-27`) silences the informational MSVC linker line printed
-because the binary exports symbols.
+(`Cargo.toml:8-10`). The release profile strips symbols and uses LTO with a
+single codegen unit (`Cargo.toml:27-33`). `[lints.rust] linker_messages =
+"allow"` (`Cargo.toml:35-37`) silences the informational MSVC linker line
+printed because the binary exports symbols.
 
 On first start the app shows a QR code: scan it from WhatsApp → *Linked
 devices*. The session is kept in `session.db`, so later starts connect
-directly. Main keys: `Tab` switches list/input, `F2` chats/contacts, `Enter`
-opens a chat or sends, `Ctrl+A` toggles autoreply for the open chat,
-`Ctrl+C` quits ([key bindings](architecture/app-state.md)).
+directly. Main keys: `Tab` switches list/input, `Enter` opens a chat or
+sends, typing in the list filters it, `F2` chats/contacts, `F3` toggles
+autoreply for the open chat, `F4` toggles the global autoreply ("AI tutte"),
+`Canc` twice deletes the selected chat locally, `F10` quits
+([key bindings](architecture/app-state.md#key-bindings)).
 
 ## Configuration in short
 
 Settings live in `config.toml` inside the data directory
 `<platform config dir>/wh` (on Windows `%APPDATA%\wh`), next to `session.db`
-and `history.db` (`src/config.rs:40-49`, `src/config.rs:144-148`). The two
+and `history.db` (`src/config.rs:47-56`, `src/config.rs:166-170`); the
+repository's `config.toml.global.template` lists every key. The two
 databases are separate by design: `session.db` is the WhatsApp library's
 session and key store, `history.db` holds the app's chats, messages and
 autoreply flags. Without the file the app still runs as a plain WhatsApp
 client; the autoreply needs `openai_api_key` and `system_prompt`. Optional
 keys tune the cooldowns (`cooldown_seconds`, `group_cooldown_seconds`), the
-startup catch-up (`catch_up_seconds`) and an AI request log (`ai_log`,
-written to `ai_requests.log` in the same folder). Keys and defaults:
-[Configuration and data](operations/configuration-and-data.md).
+startup catch-up (`catch_up_seconds`), AI retries (`ai_retries`), an AI
+request log (`ai_log`, written to `ai_requests.log` in the same folder) and
+the web panel port (`web_port`, unauthenticated, all interfaces). Keys and
+defaults: [Configuration and data](operations/configuration-and-data.md).
 
 ## Startup in one line
 
-`main` loads the config, opens the history, starts the WhatsApp client and
-an input thread, then runs the draw/receive loop until quit
-(`src/main.rs:28-51`, see [Runtime and event loop](architecture/runtime-and-event-loop.md)).
+`main` loads the config, opens the history, starts the WhatsApp client, the
+optional web panel and an input thread, then runs the draw/receive loop until
+quit (`src/main.rs:30-54`, see
+[Runtime and event loop](architecture/runtime-and-event-loop.md)).
 
 ## Where to look
 
 | Task | Page | Source |
 |---|---|---|
 | Startup, channels, threads, main loop | [Runtime and event loop](architecture/runtime-and-event-loop.md) | `src/main.rs` |
-| Events, state, keys, background tasks | [Application state](architecture/app-state.md) | `src/app.rs` |
+| Events, state, keys, filter, chat deletion, background tasks | [Application state](architecture/app-state.md) | `src/app.rs` |
 | Screens, rendering, message formatting | [Terminal UI](architecture/terminal-ui.md) | `src/ui.rs` |
 | WhatsApp session, events, JIDs, media | [WhatsApp client](integrations/whatsapp-client.md) | `src/whatsapp.rs` |
-| AI HTTP requests, vision, errors | [AI client](integrations/ai-client.md) | `src/ai.rs` |
-| When and how the AI replies (groups, cooldowns, deferred replies, catch-up) | [AI auto-reply workflow](workflows/ai-autoreply.md) | `src/autoreply.rs`, `src/app.rs` |
-| Local SQLite messages, chats, contacts | [History store](concepts/history-store.md) | `src/history.rs` |
+| AI HTTP requests, vision, retries, errors | [AI client](integrations/ai-client.md) | `src/ai.rs` |
+| Web control panel: routes, security, browser page | [Web control panel](integrations/web-control-panel.md) | `src/web.rs`, `src/web_page.html` |
+| When and how the AI replies (global/per-chat, groups, cooldowns, deferred replies, catch-up) | [AI auto-reply workflow](workflows/ai-autoreply.md) | `src/autoreply.rs`, `src/app.rs` |
+| Local SQLite messages, chats, contacts, settings | [History store](concepts/history-store.md) | `src/history.rs` |
 | Config keys, data dir, security notes | [Configuration and data](operations/configuration-and-data.md) | `src/config.rs` |
 | Test suites and how to run them | [Unit tests](testing/unit-tests.md) | `src/*.rs` |

+ 1 - 1
openwiki/testing/index.md

@@ -1,3 +1,3 @@
 # Files
 
-- [Unit tests](unit-tests.md) - Overview of the in-module unit test suites in src/*.rs, what each one covers and how to run them.
+- [Unit tests](unit-tests.md) - Overview of the in-module unit test suites in src/*.rs (including the web panel), what each one covers and how to run them.

+ 22 - 16
openwiki/testing/unit-tests.md

@@ -1,7 +1,7 @@
 ---
 type: testing
 title: Unit tests
-description: Overview of the in-module unit test suites in src/*.rs, what each one covers and how to run them.
+description: Overview of the in-module unit test suites in src/*.rs (including the web panel), what each one covers and how to run them.
 tags: [testing, cargo, unit-tests]
 sources:
   - id: openwiki-source-651d1fb6c9e49916a916ab51
@@ -18,12 +18,14 @@ sources:
     resource: repo://src/history.rs
   - id: openwiki-source-6bddea306b9b87f180f1fc02
     resource: repo://src/ui.rs
+  - id: openwiki-source-7b75006986d8b0337adc6ca0
+    resource: repo://src/web.rs
   - id: openwiki-source-7ea5eac45e5030178a6ea05b
     resource: repo://src/whatsapp.rs
-generated: { by: "claude-code", at: "2026-10-19T19:53:56.480Z" }
+generated: { by: "claude-code", at: "2026-10-20T20:27:06.515Z" }
 verified:
   - by: openwiki/0.7.2
-    at: 2026-10-19T19:53:56.480Z
+    at: 2026-10-20T20:27:06.515Z
 ---
 
 # Unit tests
@@ -31,7 +33,7 @@ verified:
 All tests are plain Rust unit tests in `#[cfg(test)] mod tests` blocks at the
 bottom of each source file. There are no integration tests, no
 `[dev-dependencies]` (`Cargo.toml` only has `[dependencies]`) and no test
-touches the network or a real WhatsApp session.
+touches the network, binds a port or uses a real WhatsApp session.
 
 ## Running
 
@@ -46,28 +48,32 @@ cargo test should_reply_false   # by name filter
 | Module | Tests | Location | Covers |
 |---|---|---|---|
 | `autoreply` | 77 | `src/autoreply.rs:528+` | private/group/self-chat detection, `supports_autoreply` (groups yes, broadcasts and newsletters no), every `ReplyGate::should_reply` rule (disabled, own messages, backlog, blank/placeholder text, per-chat cooldown, group cooldown vs. addressed messages via `is_addressed`), `deferral_due` / `take_pending` / `record_reply` pending logic, `catch_up_due` rules, custom and zero cooldowns, `context_start`, `build_turns` roles, role notes, group sender prefixes, context truncation, voice-note and image instructions |
-| `config` | 26 | `src/config.rs:288+` | defaults, trimming, multiline prompt, base URL normalisation, `Debug` masking, DB and AI log paths, `ai_log`, cooldown and catch-up defaults/custom values/negative rejection, BOM/CRLF, non-leaking parse errors, missing/invalid/unreadable files |
-| `ai` | 21 | `src/ai.rs:452+` | request and vision body shapes, image validation limits, `Debug` redaction, reply parsing, endpoint construction, AI log records (no key, images redacted), logging only when enabled, append-only log writes, truncation |
-| `whatsapp` | 26 | `src/whatsapp.rs:990+` | `addressed_jids` (mentions, quoted participant, media context info), message rendering and placeholders, captions, image references, silent messages, JID device suffix, push-name filtering |
-| `history` | 14 | `src/history.rs:418+` | dedupe by message id, ordering and limits, `messages_since` (boundary, limit keeps most recent, ascending order), chat name fallbacks, autoreply flags, contacts, on-disk open |
-| `ui` | 11 | `src/ui.rs:508+` | sender labels, clock time, QR text, wrapping, visible lines, input tail, title, selection clamp |
-| `app` | 11 | `src/app.rs:909+` | autoreply enable rules (private, self-chat LID, groups; broadcasts, newsletters and missing AI refused), vision applicability, error combining, selection and toggle helpers, AltGr handling |
+| `config` | 32 | `src/config.rs:323+` | defaults, trimming, multiline prompt, base URL normalisation, `Debug` masking, DB and AI log paths, `ai_log`, cooldown, catch-up and `ai_retries` defaults/custom values/negative rejection, `web_port` (unset, valid, `0` and out of range rejected), BOM/CRLF, non-leaking parse errors, missing/invalid/unreadable files |
+| `whatsapp` | 27 | `src/whatsapp.rs:1005+` | fatal-only app state sync reporting, `addressed_jids` (mentions, quoted participant, media context info), message rendering and placeholders, captions, image references, silent messages, JID device suffix, push-name filtering |
+| `ai` | 25 | `src/ai.rs:526+` | request and vision body shapes, image validation limits, `Debug` redaction, reply parsing, endpoint construction, AI log records (no key, images redacted), logging only when enabled, append-only log writes, retry policy (`is_retryable`, `StatusError` message, linear `retry_delay`), truncation |
+| `history` | 16 | `src/history.rs:481+` | dedupe by message id, ordering and limits, `messages_since` (boundary, limit keeps most recent, ascending order), chat name fallbacks, per-chat and global autoreply flags, `delete_chat`, contacts, on-disk open |
+| `ui` | 14 | `src/ui.rs:558+` | footer key help (F4 highlight), sender labels, clock time, QR text, wrapping, visible lines, input tail, messages title, list title (global marker, filter), selection clamp |
+| `app` | 14 | `src/app.rs:1167+` | autoreply enable rules (private, self-chat LID, groups; broadcasts, newsletters and missing AI refused), `web_chats` filtering and order, vision applicability, error combining, list filter matching, selection and toggle helpers, AltGr handling, fixed function-key codes |
+| `web` | 9 | `src/web.rs:366+` | query stripping, route resolution, `X-Wh-Request` header check (any case, missing/wrong value), which routes require it, request body trimming, empty/invalid UTF-8 rejection and the 1024-byte limit |
 
-Total: 186 `#[test]` functions.
+Total: 214 `#[test]` functions.
 
 ## Test isolation
 
 - History tests use `History::open_in_memory()` (only compiled for tests,
-  `src/history.rs:74-80`); the on-disk test writes under
-  `std::env::temp_dir()` in a per-process directory (`src/history.rs:648-662`).
-- AI tests check request bodies, parsing and log records without sending
-  requests; the append test writes a per-process file under
-  `std::env::temp_dir()` and removes it (`src/ai.rs:720-731`).
+  `src/history.rs:76-86`); the on-disk test writes under
+  `std::env::temp_dir()` in a per-process directory (`src/history.rs:749-763`).
+- AI tests check request bodies, parsing, log records and the retry policy
+  without sending requests; the append test writes a per-process file under
+  `std::env::temp_dir()` and removes it (`src/ai.rs:802-813`).
+- Web tests exercise the pure routing and parsing helpers; the HTTP server
+  itself is never started.
 
 Pages with module details: [AI autoreply](../workflows/ai-autoreply.md),
 [Configuration](../operations/configuration-and-data.md),
 [AI client](../integrations/ai-client.md),
 [WhatsApp client](../integrations/whatsapp-client.md),
+[Web control panel](../integrations/web-control-panel.md),
 [History store](../concepts/history-store.md),
 [Terminal UI](../architecture/terminal-ui.md),
 [Application state](../architecture/app-state.md).

+ 79 - 50
openwiki/workflows/ai-autoreply.md

@@ -1,36 +1,38 @@
 ---
 type: workflow
 title: AI auto-reply workflow
-description: End-to-end trace of the per-chat AI autoreply — enabling it in private chats and groups, the ReplyGate rules and cooldowns, building the AI context, deferred replies, the startup catch-up, the vision path with its courtesy fallback, and sending the reply.
+description: End-to-end trace of the AI autoreply — enabling it per chat (F3) or globally (F4 / web panel) for private chats and groups, the ReplyGate rules and cooldowns, building the AI context, deferred replies, the startup catch-up, the vision path with its courtesy fallback, retries, and sending the reply.
 tags: [ai, autoreply, workflow, vision, reply-gate, cooldown, catch-up]
 sources:
   - id: openwiki-source-9fec9d1af408501c9c455161
     resource: repo://src/app.rs
   - id: openwiki-source-043e6252badc57a1ee6bfb48
     resource: repo://src/autoreply.rs
-generated: { by: "claude-code", at: "2026-10-19T19:53:56.480Z" }
+  - id: openwiki-source-ce0c4ea8cd3b15431fa691ce
+    resource: repo://src/history.rs
+generated: { by: "claude-code", at: "2026-10-20T20:27:06.515Z" }
 verified:
   - by: openwiki/0.7.2
-    at: 2026-10-19T19:53:56.480Z
+    at: 2026-10-20T20:27:06.515Z
 ---
 
 # AI auto-reply workflow
 
 The autoreply answers incoming WhatsApp messages with an AI-generated text,
-per chat and only when the user turned it on. Decision rules and context
-building are pure functions in `src/autoreply.rs` (no I/O,
-`src/autoreply.rs:1-4`); orchestration lives in
+only for chats where the user turned it on — one by one, or all at once with
+the global switch. Decision rules and context building are pure functions in
+`src/autoreply.rs` (no I/O, `src/autoreply.rs:1-4`); orchestration lives in
 [`App`](../architecture/app-state.md); the HTTP call is done by the
 [AI client](../integrations/ai-client.md).
 
 ```
 WaEvent::Message { message, image, addressed_to }
   -> History::save_message (new only)
-  -> App::maybe_autoreply
+  -> App::maybe_autoreply   (enabled = global OR per-chat flag)
        ReplyGate::should_reply? --yes--> start_reply
        else ReplyGate::deferral_due? --yes--> mark_pending + timer
                                                -> AppEvent::CooldownElapsed
-                                               -> take_pending? -> start_reply (text-only)
+                                               -> take_pending && enabled? -> start_reply (text-only)
        else stop
 WaEvent::OfflineSyncCompleted (first time only)
   -> App::catch_up -> start_reply (text-only) per due chat
@@ -39,22 +41,38 @@ start_reply:
   ReplyGate::record_reply (clears pending)
   History::messages_since(context_start(now), 200) -> build_turns
   image + vision model? -> vision request (fallback: text request)
-                  else -> text request
+                  else -> text request         (each with up to ai_retries retries)
   -> AppEvent::AiReply
-  -> autoreply still enabled? --no--> discard
+  -> still enabled (global OR per-chat)? --no--> discard
   -> WaHandle::send_text -> AppEvent::Sent -> stored with is_ai = true
 ```
 
-## 1. Enabling per chat
-
-`Ctrl+A` toggles the flag of the open chat (`toggle_autoreply`,
-`src/app.rs:605-624`). Turning it off is always allowed; turning it on
-requires a configured AI (API key and `system_prompt`, see
-[Configuration](../operations/configuration-and-data.md)) and a chat that
-supports autoreply (`can_enable_autoreply`, `src/app.rs:885-898`), otherwise
-the status shows `Autoreply disponibile solo nelle chat private e nei gruppi`.
-The flag is persisted in the [history store](../concepts/history-store.md)
-(`set_autoreply`, default off).
+## 1. Enabling
+
+There are two independent switches, and a chat is **enabled** when either is
+on (`autoreply_enabled`, `src/app.rs:366-372`):
+
+- **Per chat** — F3 toggles the flag of the open chat (`toggle_autoreply` /
+  `toggle_chat_autoreply`, `src/app.rs:763-794`); the
+  [web control panel](../integrations/web-control-panel.md) can toggle any
+  listed chat the same way. Turning it off is always allowed; turning it on
+  requires a configured AI (API key and `system_prompt`, see
+  [Configuration](../operations/configuration-and-data.md)) and a chat that
+  supports autoreply (`can_enable_autoreply`, `src/app.rs:1127-1140`),
+  otherwise the status shows
+  `Autoreply disponibile solo nelle chat private e nei gruppi`. The flag is
+  persisted in the `chats` table of the
+  [history store](../concepts/history-store.md) (default off).
+- **Global ("AI tutte")** — F4 or the web panel toggles
+  `global_autoreply` (`toggle_global_autoreply`, `src/app.rs:839-856`);
+  enabling requires an AI client. It is persisted in the `settings` table,
+  loaded at startup and does not change any per-chat flag, so turning it off
+  restores the per-chat selection. The TUI marks it in the list title and
+  footer ([Terminal UI](../architecture/terminal-ui.md)).
+
+The global switch does not bypass the chat-type rule: `should_reply` still
+requires a supported chat, so broadcasts, status and newsletters are never
+answered.
 
 `supports_autoreply` (`src/autoreply.rs:89-97`) accepts private chats and
 groups:
@@ -68,12 +86,13 @@ groups:
 ## 2. Trigger
 
 Only messages that `History::save_message` reports as new reach
-`maybe_autoreply` (`src/app.rs:267-315`), so duplicates and history-sync
+`maybe_autoreply` (`on_message`, `src/app.rs:304-313`;
+`maybe_autoreply`, `src/app.rs:324-346`), so duplicates and history-sync
 re-deliveries of stored messages never trigger. `maybe_autoreply` needs an
-AI client and a system prompt (`ai_setup`, `src/app.rs:389-395`), refreshes
-the own JIDs if unknown and reads the chat flag. It also receives
-`addressed_to`, the raw JIDs the message mentions or quotes (filled by
-`addressed_jids` in the [WhatsApp client](../integrations/whatsapp-client.md)).
+AI client and a system prompt (`ai_setup`, `src/app.rs:430-436`), refreshes
+the own JIDs if unknown and computes `enabled` (global or per chat). It also
+receives `addressed_to`, the raw JIDs the message mentions or quotes (filled
+by `addressed_jids` in the [WhatsApp client](../integrations/whatsapp-client.md)).
 
 ## 3. ReplyGate rules
 
@@ -84,7 +103,7 @@ the own JIDs if unknown and reads the chat flag. It also receives
 
 | Rule | Detail |
 |---|---|
-| Enabled | per-chat flag is on |
+| Enabled | global autoreply on, or the per-chat flag on |
 | Sender allowed | not sent by the user — except human-written messages in the self-chat ("Messaggi a te stesso"); AI messages never trigger (`src/autoreply.rs:166-174`) |
 | Supported chat | private chat or group (`supports_autoreply`) |
 | No backlog | `timestamp >= started_at`, the process start time |
@@ -96,7 +115,7 @@ the own JIDs if unknown and reads the chat flag. It also receives
 `Cooldowns { private, group }` (`src/autoreply.rs:99-107`) comes from the
 configuration keys `cooldown_seconds` (default 30) and
 `group_cooldown_seconds` (default 300), built in `App::new`
-(`src/app.rs:170-173`); `0` disables a cooldown. The last reply instant is
+(`src/app.rs:197-200`); `0` disables a cooldown. The last reply instant is
 tracked per chat. `cooldown_for` (`src/autoreply.rs:334-346`) picks:
 
 - `group` for a group message **not** addressed to the user;
@@ -113,7 +132,7 @@ PN/LID JIDs, ignoring a device suffix (`src/autoreply.rs:145-164`,
 `strip_device_suffix`, `src/autoreply.rs:349-365`).
 
 `record_reply` is called **before** the AI request is spawned (in
-`start_reply`, `src/app.rs:397-416`), so a second message arriving during
+`start_reply`, `src/app.rs:438-457`), so a second message arriving during
 the request cannot cause a double reply; it also clears any pending
 deferral of that chat (`src/autoreply.rs:246-256`).
 
@@ -124,16 +143,16 @@ rule except the cooldown, it is not dropped: `deferral_due`
 (`src/autoreply.rs:196-224`) returns the instant the cooldown ends, provided
 a reply was already recorded for the chat, the cooldown is still running
 and no deferral is pending. `maybe_autoreply` then calls `mark_pending` and
-`spawn_cooldown_timer` (`src/app.rs:744-755`), a tokio task that sleeps
+`spawn_cooldown_timer` (`src/app.rs:986-997`), a tokio task that sleeps
 until that instant and sends `AppEvent::CooldownElapsed { chat_jid }`.
 
-`handle_cooldown_elapsed` (`src/app.rs:317-332`) consumes the deferral with
-`take_pending`; if it was still pending, the chat still has autoreply on and
-the AI is configured, it starts **one** text-only reply (no image is
-downloaded) built from the latest context, so several messages received
-during the cooldown get a single answer. If a live reply happened in the
-meantime, `record_reply` already removed the pending mark and nothing is
-sent.
+`handle_cooldown_elapsed` (`src/app.rs:348-364`) consumes the deferral with
+`take_pending`; if it was still pending, the chat is still enabled (global
+or per chat) and the AI is configured, it starts **one** text-only reply (no
+image is downloaded) built from the latest context, so several messages
+received during the cooldown get a single answer. If a live reply happened
+in the meantime, `record_reply` already removed the pending mark and nothing
+is sent. Deleting the chat locally also drops its pending deferral.
 
 Groups never defer: a group message blocked by the cooldown is simply not
 answered.
@@ -144,11 +163,12 @@ Messages received while the app was off are delivered on connection and are
 older than `started_at`, so the live rules treat them as backlog. When
 WhatsApp signals that the offline queue has been delivered
 (`WaEvent::OfflineSyncCompleted`), `on_offline_sync_completed`
-(`src/app.rs:334-344`) runs `catch_up` once per process (`catch_up_done`;
+(`src/app.rs:374-384`) runs `catch_up` once per process (`catch_up_done`;
 later reconnections skip it).
 
-`catch_up` / `catch_up_chats` (`src/app.rs:346-387`) look at every chat
-with autoreply enabled and take its last stored message.
+`catch_up` / `catch_up_chats` (`src/app.rs:386-428`) look at every enabled
+chat — all chats when the global autoreply is on, otherwise those with the
+per-chat flag — and take its last stored message.
 `ReplyGate::catch_up_due` (`src/autoreply.rs:284-307`) accepts it only when:
 
 - `catch_up_seconds` (default 7200) is positive — `0` disables the catch-up;
@@ -204,39 +224,48 @@ one was started the status shows `Risposte di recupero: N`.
 `TurnContext { self_chat, vision }` (`src/autoreply.rs:379-386`) carries
 the two switches.
 
-## 7. Vision path and fallback
+## 7. Vision path, fallback and retries
 
 The image is kept only for a live triggering image when a vision model is
 configured (`vision_applies`); deferred and catch-up replies never carry
-one. In that case `spawn_reply` (`src/app.rs:418-449`) builds two turn
+one. In that case `spawn_reply` (`src/app.rs:459-490`) builds two turn
 lists — with `vision: true` and, as fallback, `vision: false` — and runs
 one background task that:
 
 1. downloads the image into memory via the
    [WhatsApp client](../integrations/whatsapp-client.md);
 2. calls `complete_with_image` with the vision model;
-3. on any failure, retries with plain `complete` using the fallback turns
+3. on any failure, tries plain `complete` using the fallback turns
    (the courtesy "cannot see it" reply).
 
 If both fail, the error combines both messages
 (`<vision error>; Fallback cortesia fallito: <error>`,
-`src/app.rs:793-822`). Without a usable image, a plain text request is
-spawned. When `ai_log = true`, every request is also appended to
-`ai_requests.log` (see [AI client](../integrations/ai-client.md)).
+`src/app.rs:1035-1064`). Without a usable image, a plain text request is
+spawned.
+
+Every AI call (text, vision and the courtesy fallback) is retried by the
+[AI client](../integrations/ai-client.md) up to `ai_retries` times (default
+2, waiting 2 s, 4 s, …) on transient errors — HTTP 429, 5xx, timeouts,
+transport errors, malformed or empty replies — while other 4xx errors fail
+at once. A reply can therefore arrive noticeably later than the trigger;
+the "still enabled" check in step 8 happens after all retries. When
+`ai_log = true`, every attempt is also appended to `ai_requests.log`.
 
 ## 8. Sending
 
 `AppEvent::AiReply` is handled by `send_ai_reply_if_enabled`
-(`src/app.rs:478-492`), which re-reads the chat flag: if autoreply was
-turned off while the request was running, the reply is discarded with
+(`src/app.rs:519-534`), which re-checks `autoreply_enabled`: if both the
+global and the per-chat switch are off by then, the reply is discarded with
 status `Risposta AI scartata: autoreply disattivato`. Otherwise the text is
 sent with `spawn_send(..., is_ai = true)`; on success `handle_sent`
-(`src/app.rs:494-522`) stores it as an own message with `is_ai = true`,
+(`src/app.rs:536-564`) stores it as an own message with `is_ai = true`,
 which keeps it from re-triggering the self-chat. Errors appear in the status
 line (`Errore AI: ...`, `Invio fallito: ...`).
 
 ## Tests
 
 The rules, cooldowns, deferral, catch-up and turn building are covered by
-77 tests in `src/autoreply.rs:528+`; enable rules and vision applicability
-by `src/app.rs:909+`. See [Unit tests](../testing/unit-tests.md).
+77 tests in `src/autoreply.rs:528+`; enable rules, `web_chats` and vision
+applicability by `src/app.rs:1167+`; the global flag storage by
+`src/history.rs`; the retry policy by `src/ai.rs`. See
+[Unit tests](../testing/unit-tests.md).

+ 1 - 1
openwiki/workflows/index.md

@@ -1,3 +1,3 @@
 # Files
 
-- [AI auto-reply workflow](ai-autoreply.md) - End-to-end trace of the per-chat AI autoreply — enabling it in private chats and groups, the ReplyGate rules and cooldowns, building the AI context, deferred replies, the startup catch-up, the vision path with its courtesy fallback, and sending the reply.
+- [AI auto-reply workflow](ai-autoreply.md) - End-to-end trace of the AI autoreply — enabling it per chat (F3) or globally (F4 / web panel) for private chats and groups, the ReplyGate rules and cooldowns, building the AI context, deferred replies, the startup catch-up, the vision path with its courtesy fallback, retries, and sending the reply.