Browse Source

Enhance documentation for unit tests and AI auto-reply workflow

- Updated descriptions in `index.md` and `unit-tests.md` to clarify the coverage of unit tests, including the version flag and file isolation.
- Added verification metadata to `unit-tests.md` and `ai-autoreply.md` for improved traceability.
- Revised the AI auto-reply workflow documentation to detail the reply delay mechanism, image description handling, and the impact of owner responses on AI replies.
- Adjusted the test coverage details to reflect recent changes in the codebase, ensuring accuracy in the documentation.
softword 1 day ago
parent
commit
d8f255914c
31 changed files with 1451 additions and 1000 deletions
  1. 120 72
      openwiki/.claims/architecture/app-state.json
  2. 47 19
      openwiki/.claims/architecture/runtime-and-event-loop.json
  3. 32 18
      openwiki/.claims/architecture/terminal-ui.json
  4. 42 26
      openwiki/.claims/concepts/history-store.json
  5. 76 56
      openwiki/.claims/integrations/ai-client.json
  6. 4 4
      openwiki/.claims/integrations/web-control-panel.json
  7. 25 35
      openwiki/.claims/integrations/whatsapp-client.json
  8. 71 41
      openwiki/.claims/operations/configuration-and-data.json
  9. 25 11
      openwiki/.claims/quickstart.json
  10. 55 37
      openwiki/.claims/testing/unit-tests.json
  11. 149 75
      openwiki/.claims/workflows/ai-autoreply.json
  12. 2 2
      openwiki/.last-update.json
  13. 44 44
      openwiki/.page-manifest.json
  14. 131 97
      openwiki/architecture/app-state.md
  15. 3 3
      openwiki/architecture/index.md
  16. 45 23
      openwiki/architecture/runtime-and-event-loop.md
  17. 32 28
      openwiki/architecture/terminal-ui.md
  18. 46 26
      openwiki/concepts/history-store.md
  19. 1 1
      openwiki/concepts/index.md
  20. 1 1
      openwiki/index.md
  21. 88 57
      openwiki/integrations/ai-client.md
  22. 1 1
      openwiki/integrations/index.md
  23. 3 3
      openwiki/integrations/web-control-panel.md
  24. 35 43
      openwiki/integrations/whatsapp-client.md
  25. 108 65
      openwiki/operations/configuration-and-data.md
  26. 1 1
      openwiki/operations/index.md
  27. 46 25
      openwiki/quickstart.md
  28. 1 1
      openwiki/testing/index.md
  29. 27 20
      openwiki/testing/unit-tests.md
  30. 189 164
      openwiki/workflows/ai-autoreply.md
  31. 1 1
      openwiki/workflows/index.md

+ 120 - 72
openwiki/.claims/architecture/app-state.json

@@ -1,14 +1,14 @@
 {
   "schemaVersion": 1,
-  "pageVersion": "sha256:34c88f57821c236e84d8cbbd5e3868cc3ff6629a5c6da55afacfbd3989da5c5a",
+  "pageVersion": "sha256:a11d9385ed0fa92efef8cc9b1d8eb7a4c392af8f751c1b660a60d6044eac8893",
   "claims": [
     {
       "id": "claim_daef343b267c45a987738a3daf7e404d",
-      "statement": "AppEvent has variants Key, Redraw, Wa, AiReply, CooldownElapsed, Sent and Web; Web carries a WebRequest from the web control panel.",
+      "statement": "AppEvent has variants Key, Redraw, Wa, AiReply, ImagesDescribed, ReplyDue, Sent and Web; AiReply carries a Reply or an error and Web carries a WebRequest from the web control panel.",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L73-L98",
-          "version": "repo-lines-v1:sha256:5e875e0df93e3b8f591f71b16614b5ad4d266aee0c29ef8e75203b10f4914e9d:eyJzZWxlY3RlZExpbmVDb3VudCI6MjYsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImI4OWMyOWE3NjJmOGZhMmEzZmY0NmY5MGZiZDY3N2QyYTI3MjViMjM3NTYxNjIwODdkOTRmMWI2MDg2Yzk5YzEiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjZmMGExNjA2YWZlMjU3OTU2NWMyMWNkOGU1ZjUyNzljMmY0ZWMwZWMyMjZmZjM5YjljNmY2MjllZTNkYjk0NTEiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjJlYzJiMDUyMWI2NDA2YzM3MGE3MzdmMTZhYTU0NDBkZDcyZDE1YWMxY2E2OGY5MmI1ZDc5NzUxM2I0NDk0NGQifQ"
+          "resource": "repo://src/app.rs#L90-L121",
+          "version": "repo-lines-v1:sha256:f02e9754ecd750b36108a3c72bb9c08065b4ad6856541ca12ce77b0d077a7ed0:eyJzZWxlY3RlZExpbmVDb3VudCI6MzIsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImI4OWMyOWE3NjJmOGZhMmEzZmY0NmY5MGZiZDY3N2QyYTI3MjViMjM3NTYxNjIwODdkOTRmMWI2MDg2Yzk5YzEiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjZmMGExNjA2YWZlMjU3OTU2NWMyMWNkOGU1ZjUyNzljMmY0ZWMwZWMyMjZmZjM5YjljNmY2MjllZTNkYjk0NTEiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjJkNTQzZDNkZjU4ZmJmODdkZjViYjZlOGNmZDQwYzBkNjY5MjY3MWZmMTAyOWRmZDM4OWY0MWIyNDY2OTZiY2EifQ"
         }
       ]
     },
@@ -17,28 +17,28 @@
       "statement": "Screen has Connecting, Pairing and Main; Focus has List and Input; ListMode has Chats and Contacts.",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L100-L120",
+          "resource": "repo://src/app.rs#L131-L151",
           "version": "repo-lines-v1:sha256:92aaf2af1678376e51c1ad2567e9dce9ab29a60eb8e773ba16594a42750ff769:eyJzZWxlY3RlZExpbmVDb3VudCI6MjEsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjY2YTEyNzUwZTQ5YzY3NDE5NGM1Yjg4ODEzOGQ5Y2JjNGI0Y2M2OTM2NjdiYmZjMzIxYjgzM2MxMWZlMTdlY2QiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjY3NmE1NDRjYzAyM2VkNDA1ZTNiOThiOGEzNTQwOTBmYWQxNmU5ZTE4ZDVlZTlmN2JhNmMzZTcwMDUzMDA5ZDEiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjFmY2RhYjQwOGQyMDcwM2JmNTU2ZWJmZjk2NGM3M2I4NGQzYWUzYzdiMzUwZTFiODNmNTA3OTE4MDUxYjA0ZjYifQ"
         }
       ]
     },
     {
       "id": "claim_0a2882d788054b5586b16e6e990f77ee",
-      "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.",
+      "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.",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L191-L229",
-          "version": "repo-lines-v1:sha256:b8e7258ce42c14156856b5548408686e166ef6c02cf336560ffc75ccd7ecf145:eyJzZWxlY3RlZExpbmVDb3VudCI6MzksImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjdmNTYzZjU4ZTBlOWQyYjgxMjgwMTZhOWEyZGZhMTU2YjgzNTM0NWYzOTkzYTVhYjdkNjVhNWU4ODFmMDg0Y2UiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjBhOGJlZTA0NjIzODBmZjdmN2Y0ZDZhYzE2MGViODRmYjk3YjgwZGRjMjM2ZGFmZWY4NTQ4OTYxYjcwZDQ4ZmQiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImU3M2U3ZTFjNDIxZDk5NzQ5ZWVmNzNjNjJiNmE4ODAyODhjNGNhZDM4ZTYwZWQyMTNjNmM1ZDI1ZjAyMDU3OGMifQ"
+          "resource": "repo://src/app.rs#L222-L260",
+          "version": "repo-lines-v1:sha256:40d548eaea0ec6ea72e016a3adeab2e0d8b2c9a0039d4f3bf03310b7e72917d0:eyJzZWxlY3RlZExpbmVDb3VudCI6MzksImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjdmNTYzZjU4ZTBlOWQyYjgxMjgwMTZhOWEyZGZhMTU2YjgzNTM0NWYzOTkzYTVhYjdkNjVhNWU4ODFmMDg0Y2UiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjBhOGJlZTA0NjIzODBmZjdmN2Y0ZDZhYzE2MGViODRmYjk3YjgwZGRjMjM2ZGFmZWY4NTQ4OTYxYjcwZDQ4ZmQiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImU3M2U3ZTFjNDIxZDk5NzQ5ZWVmNzNjNjJiNmE4ODAyODhjNGNhZDM4ZTYwZWQyMTNjNmM1ZDI1ZjAyMDU3OGMifQ"
         }
       ]
     },
     {
       "id": "claim_bf08f07b96ed49c2b688852a35a263f5",
-      "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).",
+      "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 { message, image } events.",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L257-L282",
-          "version": "repo-lines-v1:sha256:9c2eb0130ed8fe6d1f4a7364331089bc43b4a8f4aa42c0a29d8794946dd32f3f:eyJzZWxlY3RlZExpbmVDb3VudCI6MjYsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjZkOGNmMDJkODU2OWY1YTQ3ZjRiNmY0Zjc5MzBmNzkzMTczM2VmNGE4NDMwODNmYmZjMzAzNmNiM2JiMjc5MGQiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImM5NDkyZWM5YTM2ZThkODU0NjE1MmI4MjJkNTdmZDIwMDk4OTE1MDMwYTIwZDliZGExZDVmYWQ0NGY4ZjFiNWYiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImI2ZDczY2I1NTUwODdmZGQwZTc4MzhjZjM4MWZmZmRmMzljMzk2M2VmZWJhMGE0ZjQyNzg4OWE0MGUwODYwZWQifQ"
+          "resource": "repo://src/app.rs#L289-L312",
+          "version": "repo-lines-v1:sha256:5d1dce3b181c3675faeb3b14606340ed867acb60a349b69ef26369d3856c49c6:eyJzZWxlY3RlZExpbmVDb3VudCI6MjQsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjZkOGNmMDJkODU2OWY1YTQ3ZjRiNmY0Zjc5MzBmNzkzMTczM2VmNGE4NDMwODNmYmZjMzAzNmNiM2JiMjc5MGQiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImM5NDkyZWM5YTM2ZThkODU0NjE1MmI4MjJkNTdmZDIwMDk4OTE1MDMwYTIwZDliZGExZDVmYWQ0NGY4ZjFiNWYiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImI2ZDczY2I1NTUwODdmZGQwZTc4MzhjZjM4MWZmZmRmMzljMzk2M2VmZWJhMGE0ZjQyNzg4OWE0MGUwODYwZWQifQ"
         }
       ]
     },
@@ -47,8 +47,8 @@
       "statement": "on_message returns early when History::save_message reports the message was not new.",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L304-L313",
-          "version": "repo-lines-v1:sha256:eb6c3b3607fadb274e63e9fe5f6be1f2f35bb2e82b3beea4cba64c0b5f362f1e:eyJzZWxlY3RlZExpbmVDb3VudCI6MTAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjYyNjcxZjMwOGZhYjk0MTMwNzgwNDU4MzFhNDMxYjJjZTM3MzhhMTEyMzMyNDg4OGY2ZTgzNTY1ODhjYjNmOTciLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImQ1Zjg3MzRjYWZkNWU0MTNiNTUyZDMzZjFmMThlOTEyNWI2YTkwMTFhODU2OTM2YmJjMzRmMzAxMGM4Yzg1ZjkiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjU2YzA0ODg4Njk3MzQ4ODBlMTJiNTM0MmNmZDI3ZjU4YTU0M2Q1NjE4YWY1M2JlZjRkNmVhZDYyYjhiZmQ1N2YifQ"
+          "resource": "repo://src/app.rs#L333-L342",
+          "version": "repo-lines-v1:sha256:39169d7955b19430b175fe5273033b79acede8ec3c2de6b4a8998c3201160caa:eyJzZWxlY3RlZExpbmVDb3VudCI6MTAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6Ijk2ZjNjMTFjYTY5OWY4OTA5MTUwMjAyYzhkZTVlMjdlZjAwNTIyOGRmZmNjMzAzOTlmNDg4OTdmYzdhOTVlMTEiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjdmYzAyNDRhNGJkOTI4YWRmMDE3YjljOWQ0ODIxYjU4NjgwZGRhMmI2YTBhZDEyYWU2MWVhNDBlOWZkMjM3OGMiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjJhNDYyZmZiOTg5MjlkZTUyYmFlMTM0ZWVmMWRkZjYzNTRiODVlNGQ4MzE2MzlkMzZhMDIyMzA5NDcwNDYyZDYifQ"
         }
       ]
     },
@@ -57,11 +57,11 @@
       "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#L22-L29",
-          "version": "repo-lines-v1:sha256:1b77b4b729c472be09a45c5473cf8594fc6554a18cc8f686ece6245ff2aa26ec:eyJzZWxlY3RlZExpbmVDb3VudCI6OCwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiYmQxOTM5NWQwMDUxZDZhMDgzMGQyZDk5ZmE1OThkNTg3YTI5ZmQ1MDQ5OTA0ZDU5Y2I1MjZiZDdjNzg4MGIwZSIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiYWY2MjM3MDg5YjkxZDEwN2I2NjQ1Nzk3M2E4YmEyYTcxZDg3MDQ0ZWE5MWU5MThjZGUyMDVmODZhY2Q0NmM5MyIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiMWVmZGQwMDhhMTYzNjZhM2YxN2I2NjUzYjI3NzIxOGRjNDY1NGIwYWNhMTkwYjViYWJkNzA2YjIzYjEwNDA3NyIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiZjM1MGFjYmZhYzdlNDEzZmFkNWYxMzk0NTQwYWE3MWM3YWY3ZWZmMDA4MDliYmMxYTlhNzQxZGRhNDFiOGYwZCJ9"
+          "resource": "repo://src/app.rs#L23-L30",
+          "version": "repo-lines-v1:sha256:1b77b4b729c472be09a45c5473cf8594fc6554a18cc8f686ece6245ff2aa26ec:eyJzZWxlY3RlZExpbmVDb3VudCI6OCwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiYmQxOTM5NWQwMDUxZDZhMDgzMGQyZDk5ZmE1OThkNTg3YTI5ZmQ1MDQ5OTA0ZDU5Y2I1MjZiZDdjNzg4MGIwZSIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiYWY2MjM3MDg5YjkxZDEwN2I2NjQ1Nzk3M2E4YmEyYTcxZDg3MDQ0ZWE5MWU5MThjZGUyMDVmODZhY2Q0NmM5MyIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiMWVmZGQwMDhhMTYzNjZhM2YxN2I2NjUzYjI3NzIxOGRjNDY1NGIwYWNhMTkwYjViYWJkNzA2YjIzYjEwNDA3NyIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiYjZmYjdiOWM1MTM4MWJmY2U2YjBjNThlNjRmZmEyOGM4MzI2NTk1OTI5ZDUwM2VhNTBiMGUwNGI1OTQ3ZDA1MyJ9"
         },
         {
-          "resource": "repo://src/app.rs#L573-L603",
+          "resource": "repo://src/app.rs#L774-L804",
           "version": "repo-lines-v1:sha256:37edcb8b1691dbaa467f7d59b80cdc3781376ce6ce1fc1783f167c52ae5ce169:eyJzZWxlY3RlZExpbmVDb3VudCI6MzEsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjNmODVjMjY1ZjBmMjNkOTA0NDcwMjYxNWZiM2NkMzQ2ODMxYTFiZWNiOTRlYzNkODYyNDYxYzFkNWEwMGQzNDUiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjE5OTRiMGIxZDNhNDA1MzRjYTFiZTVmNDdiNTRlNWY3ZDg3ZTZlMGMxMWE3ZjU5ZjA0NGU2ZGJmNTkyYzNlZDYiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6Ijk2ZWU5ZDllODg2ODFmN2JmM2M4ZmZiZjEzZDgyODk2ODM4MGY2Y2E0M2Y1ZGE4ODI5MmU1MjY3ZDRjMDhlOWYifQ"
         }
       ]
@@ -71,7 +71,7 @@
       "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#L726-L750",
+          "resource": "repo://src/app.rs#L939-L963",
           "version": "repo-lines-v1:sha256:93f1a991412191cf1e47a90542afc809bc43aaab50594660e57b57012282758d:eyJzZWxlY3RlZExpbmVDb3VudCI6MjUsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6Ijc1ZWM1NzZmZGY5ZTZhYjIyYmM3ZTgyYWM2NzNiY2EyM2MxNTZkZjE0MTRmZGFjZjRlNzcwOTEzYmI3Yzg1OTYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImUxNzg4NWVhNGE5MGJjODU2N2NiZDFmYjM0M2FlNTQ0Y2EwMTU5ZDZkNTA0ZGU3ZjcyNWFiMWIyYjg0YzYwZGMiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjBlYTM2NTI1MWJlYzc3N2JhYTBmYTZjMGEzZTZmOGRiYzliNzhiNmQ5YTNmNmI4ODVmODBmY2RkMmQzYWFhMmEifQ"
         }
       ]
@@ -81,11 +81,11 @@
       "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#L1132-L1140",
+          "resource": "repo://src/app.rs#L1408-L1416",
           "version": "repo-lines-v1:sha256:ba5bea3063970ee3b66b2d54fc0529a98bf3972bf7e9a5cab37c6d0bf4659ff7:eyJzZWxlY3RlZExpbmVDb3VudCI6OSwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiZmFmNzM0NDVmZTU0ZTJjMzIyMDBiMGRiNDhlM2MyOGZlZDgyZDk3NWY0Mzg4MjM0NjA5NjhlZmM4ZmU1ZjJmOSIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDEyY2EzNDVjY2Y3NWJmOWMwODA2YmNlNjk1YmU4ZGU4MDhiNzk5ODQyNTFhN2E1NGQyMDJjZjYxMDFkZDQ1MSIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiOWM5Yjc1NjM5OGI3MTI0NGNmYThkOTk4ZGRlOTQwZWRmODk1MmNkMDEwMTYyMzM1YmEwMzRjYjkxZGZmMjRiZSIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiNDQwM2Q5ZmY0OGI3ZmI2ZDcyM2RlNWM5YjMxODkzMTNlMjQ2NzBhMWE5NjJhYWUxZTYxODRhMjllZmM3MWM1ZCJ9"
         },
         {
-          "resource": "repo://src/app.rs#L781-L794",
+          "resource": "repo://src/app.rs#L994-L1007",
           "version": "repo-lines-v1:sha256:cbcb0d80e707fedd21b3de7081436677468c63c025abb23dab5e5a6ab0ad90cc:eyJzZWxlY3RlZExpbmVDb3VudCI6MTQsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjA0ZTRlYmRmYjA5YmI0NTM3YzBhZDM0MWUxNGY5NTkzYjEwYWE5ZWJiOTM0ZTNiOGJhMGViY2ZiYjlhYjBkZmYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjI1Zjg5YTg3NjBkYWQ3NjUxYmZiZGQ4ZGFkYTM4OGRmZDUwZmU2NDczOGUwMzM4YjIzZTM5NzM4Y2Y1NTRmOWUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImUzZWNmNGIzNzRlN2M0ODFlZjUzODQ1Y2Y1YjlmNDI5MDk3NmQxYTlmNDlkOWY5NWJjNjIxNWEyY2VhMDYyOTAifQ"
         }
       ]
@@ -95,22 +95,12 @@
       "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-L20",
-          "version": "repo-lines-v1:sha256:052992c9f8e9b996b43554cee0a2351140f142d6d9781a2b3217356c40bf6cb1:eyJzZWxlY3RlZExpbmVDb3VudCI6MiwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiODdkOGI3ZDU2NTM2Yjg3MDE0OGY1Y2U5YzQ2YWQxN2FmMzI4ZDQ5NzRjMmNlMTVhODNhOWQ0ODllZTBjNDY5MiIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiMjQwNDg4Y2VlODM4MmUwZTUzMWI5ZjEzZmQwYWE5NDFjNzJkMDM5MGU3ZTg5YzQ0MzA4MTFjYmM3NmFhNjYxMCIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiMGY4MjlhNmQ5ZTNmMmZjNjc2ZTdkYTAzNzA2YjY4ZjQ1OTE5ZjhiMTkyNTIyN2JiYTMwMDkwZGZmYzYwN2Q0MyIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiNDcyMjFlYzQ5YThiY2FiYTFkNzUwNTE0MWZhYzY0MDhkOGZmMmUwMjUyMDgwZTZlMGYwNzQ1MDMzMjY3ZjQwZSJ9"
-        },
-        {
-          "resource": "repo://src/app.rs#L862-L878",
+          "resource": "repo://src/app.rs#L1075-L1091",
           "version": "repo-lines-v1:sha256:0aceaa3d72a442f277babfd7a811f8fa14bea8944c8dee6f9657dcad479fd9d8:eyJzZWxlY3RlZExpbmVDb3VudCI6MTcsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjU0NmM2MGMyMDFmZDM3MThkMzVjZTZhNzRlMTdkMmM2Y2E2Yjg5N2ZmMDc3Y2RkMDcyZGZkZDgzODFhZjVhNWMiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjUzMjlhMjljNjFlZGI3Y2E3NTEzZTQ0NTgxMDJhZTAwNWU1MGM2N2QyYWFhMTdjMjRmMTBhMTRmN2MxOTExNzIiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImNjYzVlMzg4ZThmOTY0ZGY1OTFjZDM1Y2FlZjk5ODM0MmRjNTllNGU5OTMwMmMyYmI0MjMyZmMxMzdhM2Q1ZjkifQ"
-        }
-      ]
-    },
-    {
-      "id": "claim_e3fc47455dc24b508616e9effd70c1d2",
-      "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#L1023-L1064",
-          "version": "repo-lines-v1:sha256:5f2de4b5568e20c68dce45723dee3fac0fdb727c5c0babf4f85e4baaa14a44e9:eyJzZWxlY3RlZExpbmVDb3VudCI6NDIsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6Ijk0MTM1OGQzZmUwMGY3ZDM4YWM4MjNjMDc4OWNlZmU2NTNhMTE2YzkyZmNjNWMyYjYwNWY4ODA0MjA2MzRjYTAiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImQxZTM5MDcyMDkzN2I1OWI2MzAyZjNjZGUwY2Y5NzgyMTUxZjc2OTg0MDM4Yjc4ZjBiYzJmNjkxYjFmZTcwM2EiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImU0MzNjMDFmNDAzMDNjNmE3M2FiOWRjYzAxY2Y3ZTg2ZWIyMGU3ZmEzZjdjNzVhMjQ1MzMyYTRmYTdmYmYwOWMifQ"
+          "resource": "repo://src/app.rs#L20-L21",
+          "version": "repo-lines-v1:sha256:052992c9f8e9b996b43554cee0a2351140f142d6d9781a2b3217356c40bf6cb1:eyJzZWxlY3RlZExpbmVDb3VudCI6MiwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiODdkOGI3ZDU2NTM2Yjg3MDE0OGY1Y2U5YzQ2YWQxN2FmMzI4ZDQ5NzRjMmNlMTVhODNhOWQ0ODllZTBjNDY5MiIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiMjQwNDg4Y2VlODM4MmUwZTUzMWI5ZjEzZmQwYWE5NDFjNzJkMDM5MGU3ZTg5YzQ0MzA4MTFjYmM3NmFhNjYxMCIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiMGY4MjlhNmQ5ZTNmMmZjNjc2ZTdkYTAzNzA2YjY4ZjQ1OTE5ZjhiMTkyNTIyN2JiYTMwMDkwZGZmYzYwN2Q0MyIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiNDcyMjFlYzQ5YThiY2FiYTFkNzUwNTE0MWZhYzY0MDhkOGZmMmUwMjUyMDgwZTZlMGYwNzQ1MDMzMjY3ZjQwZSJ9"
         }
       ]
     },
@@ -119,27 +109,27 @@
       "statement": "spawn_send sends the text via WaHandle::send_text and emits AppEvent::Sent.",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L1101-L1110",
+          "resource": "repo://src/app.rs#L1377-L1386",
           "version": "repo-lines-v1:sha256:946a66552037dc4ae7eff9cf6c602520fd019e2700d8d6cc4dc93c7db36bb837:eyJzZWxlY3RlZExpbmVDb3VudCI6MTAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImFlOWMwM2FlM2RiZTlkZDA3YjYxNTZkZWFkNzBhNmRhYjc4MzI3M2I2Y2E0Y2JlZGI0M2E0MTI4YmJlNTdiZjgiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjMwZDEyZTUxMmY3N2FlNTEwYWQwZTk5M2I3NGNmNjdmY2ExYmRiOTllYmE3OWY3OTdjZDkxMWYyN2Y2MWQ1ODQiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjUyM2IxZmMxYjIzYjFmMWZhZWVhY2E4MTM2Yzc1YmFkOGEwNGEzMGQ5Nzk0MTc5ZDNjZjUxYjJkNGVhMDI1NTIifQ"
         }
       ]
     },
     {
       "id": "claim_3568f04cad88489a974266f0e241c354",
-      "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.",
+      "statement": "An AI text reply is sent only if the autoreply is still enabled for the chat (globally or per chat) and the owner did not answer by hand after the answered context; otherwise it is discarded with a status message.",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L525-L534",
-          "version": "repo-lines-v1:sha256:b744280fd770d3942f92d16928b5cdc09f3df4da0b12e5f3de78cc9d7983c222:eyJzZWxlY3RlZExpbmVDb3VudCI6MTAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImVlODZiMzQ5MjhkN2ZlZWQzNTRiNmY1OGM3MjJlNTk5YzI3ODhhNWE0Yzc2MjUxN2YzYzExYzIzMDM0OGVjOTUiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6Ijc1NjVhM2RiYzM1YWQwYzg0NDFjZThkN2YyNWQyNjQzODZhZTE4NDc3MGYzMDRkODdkYjFiMjk0OGZjM2ExNGEiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImVlYjQ1ODA1ZTI5MzcwMzMyZTY2ZDA3NzAwZjlhMDg3MDgyNWE3ZDM2NWFmYmJmNzYxOWNiYjNlMjNiNTViMWQifQ"
+          "resource": "repo://src/app.rs#L709-L735",
+          "version": "repo-lines-v1:sha256:a05110c5a383be3518256a8f574d7c2b6dfa7ab90b8f3a2d19fbed62b9ec6e14:eyJzZWxlY3RlZExpbmVDb3VudCI6MjcsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImVlODZiMzQ5MjhkN2ZlZWQzNTRiNmY1OGM3MjJlNTk5YzI3ODhhNWE0Yzc2MjUxN2YzYzExYzIzMDM0OGVjOTUiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6Ijc3ODkyYWI4MWM1MjU3OGQ5NjU4YWE4MmNmNDU0NjAyY2VmYTA4MDJiNmMzYzRkYWE4OWM4MjUwZDBlZDBjMjUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImVlYjQ1ODA1ZTI5MzcwMzMyZTY2ZDA3NzAwZjlhMDg3MDgyNWE3ZDM2NWFmYmJmNzYxOWNiYjNlMjNiNTViMWQifQ"
         }
       ]
     },
     {
       "id": "claim_17fa61af9be74c3dbc0b44f6b5d2873b",
-      "statement": "build_ai returns no client when system_prompt is missing.",
+      "statement": "build_ai returns no client when the system prompt is missing.",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L1117-L1125",
+          "resource": "repo://src/app.rs#L1393-L1401",
           "version": "repo-lines-v1:sha256:02a6b90c8547969f5229ba908e64ec2f9fd8d0547f3a7b34404429320b337fe4:eyJzZWxlY3RlZExpbmVDb3VudCI6OSwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiNzcyNTRhNjcwYWJhNGU4ZWFiNDlmM2U1OWYzNjc4NTk0OWM2ZDk1OTFlN2ZkMWY1NjA4NGU3NzBlOWM3NDJlNCIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDEyY2EzNDVjY2Y3NWJmOWMwODA2YmNlNjk1YmU4ZGU4MDhiNzk5ODQyNTFhN2E1NGQyMDJjZjYxMDFkZDQ1MSIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiMGQ3OWM4ZDU3OGFjNjM3NDFmOTgwMTlmMzlmOWRmMDA4M2RkN2YzM2U0ZDNlOWU3ODU1YzZmNmQ2MWQzNjg0MCIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiNTA2ZDU0NTM1OGJmMGQyNTFiNDhiNWZhZTE2YzIyZjkzYzgzNDM5ZTQwNzA0YzRkYzg1MzgyN2ZmNWFiNmYyNiJ9"
         }
       ]
@@ -149,52 +139,52 @@
       "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#L1158-L1165",
-          "version": "repo-lines-v1:sha256:a7e4215d1055bcf132d529dd3c057ef20525fbb22d5a1301c6980d41107b5124:eyJzZWxlY3RlZExpbmVDb3VudCI6OCwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiYzllOWIxZTI5MGI4MzMyZDI5YTMwZjc1N2JjN2E3N2ZlYjZmMjAxYjNjMmM5M2ZiNTQ4MDU1YTFhODVmN2MyZSIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDEyY2EzNDVjY2Y3NWJmOWMwODA2YmNlNjk1YmU4ZGU4MDhiNzk5ODQyNTFhN2E1NGQyMDJjZjYxMDFkZDQ1MSIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiMzVmNmY0NjZlNTljZmMzMDcwM2M0NWY3MmFmNjVkZTU0YWMzNjNkMzU0Y2MxYTMzMzAzNTBhY2Q0NTI0MzRjNCIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiN2JjOThkNmIwZGM0OTJjNTAxNmRmMTA4MDVhM2E1NGViOWMxODRhNWE2MTM3ZTdmNGJlY2ZhMGMyMjA1NTBkMCJ9"
+          "resource": "repo://src/app.rs#L1434-L1441",
+          "version": "repo-lines-v1:sha256:a7e4215d1055bcf132d529dd3c057ef20525fbb22d5a1301c6980d41107b5124:eyJzZWxlY3RlZExpbmVDb3VudCI6OCwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiYzllOWIxZTI5MGI4MzMyZDI5YTMwZjc1N2JjN2E3N2ZlYjZmMjAxYjNjMmM5M2ZiNTQ4MDU1YTFhODVmN2MyZSIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDEyY2EzNDVjY2Y3NWJmOWMwODA2YmNlNjk1YmU4ZGU4MDhiNzk5ODQyNTFhN2E1NGQyMDJjZjYxMDFkZDQ1MSIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiZDg4NTkyMTBlYmNiMTJjZjY4YzYxNjIyMmZjNTYwYzFmNjhlMmZhMzFmNjMwNDkzZGY4MDk1ODg4N2Q1N2FmOSIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiN2JjOThkNmIwZGM0OTJjNTAxNmRmMTA4MDVhM2E1NGViOWMxODRhNWE2MTM3ZTdmNGJlY2ZhMGMyMjA1NTBkMCJ9"
         }
       ]
     },
     {
       "id": "claim_bc67b9414f7d4100abf86082fa0b5681",
-      "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.",
+      "statement": "start_reply records the last context message as answered, marks own mentions, reads the chat name and stored image descriptions, builds the turns and spawns the AI request.",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L444-L457",
-          "version": "repo-lines-v1:sha256:b986ffa888b04f80920f0e7ee6c1fe15e171ea20ddac89ac45271c5446b2a64e:eyJzZWxlY3RlZExpbmVDb3VudCI6MTQsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjRiYmM5MGM3NmFlZGUwNzkzNzVkMjliNGZiMjRlMWMyMzg3ZTc0ODE2NzkyMzM2ODk5ZTE0NTA2OGExMDYyZjIiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjhiZTI4OTVhNzFkYmI4ODUzNDMxYjIwZDI1ZDI4YWQ3ZDk5ZjUxYWEzOWYyOGIyNWIxMzY4ZjdmMTMyNzMxMGYiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjA2ZWViM2RhYjZhYTAxODdiMjY5ZjlmNGY1NjUyODE3ZTljNDE0ZTQzZjQ1Nzc5OWY4YWJlMjliMjhiM2IzZmQifQ"
+          "resource": "repo://src/app.rs#L654-L673",
+          "version": "repo-lines-v1:sha256:0a937f62450210fb1b4a45c2eb392c8533cab8fc5e7f2ca88caec3ba64669dbb:eyJzZWxlY3RlZExpbmVDb3VudCI6MjAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImZkMjA2ZWQ4NGMzNGNhMWE0ZmNmZmQ1NmFlOTBiNDgyZDIyNGM2OWQ0ZmRlMDY4MmI2MTllODNlMDRlNWJhYjYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjEyZTA5MzVlNDEzNWQ5MWEyZGFlMDUxOGE3ZTY5NjQ0MmIxYmM5ZGYxOThiNDA1ZmVmMGQzMjUwY2VmOWU1ZWQiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImQwMjI2ZGQ4YmZkY2JkMDhlNjUxMjUzOGY4MmU5YWNiOTEyNzg2NTgwNDEwMmQ2YjRlMzQ4NmRmMjU4YjMxNmYifQ"
         }
       ]
     },
     {
       "id": "claim_0a16da9dd5e942eb89a106629e24f51d",
-      "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.",
+      "statement": "maybe_autoreply schedules a reply when ReplyGate::should_schedule allows it: the chat is marked pending and a timer task emits ReplyDue after reply_delay_seconds.",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L324-L346",
-          "version": "repo-lines-v1:sha256:b7e3059a255a91b3ca5ae0caad411be8cadeb3cd6d0caace25bc17f59db073f9:eyJzZWxlY3RlZExpbmVDb3VudCI6MjMsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImViZGJkZDZkZGVmMDM5YTViMmI1YTIxZGUxMzliZjllODRmM2JhNDU4YThjZWY0ZTQzOGRiNjQ3M2EzODliODIiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjMwZmQ1Yjc2YTk4ZDRmMjZhODFiMWVjYjkyNjFkMDQzNjJkYTg2NDhhNzMzMGFjNmJjNTVkZmFiOTFmZmY4MmEiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6Ijk0NDE2M2IzN2JlYTdlZjFjNmJiMDZkODAzMTQxOGRmZjZjNTBlNjgzMmUyMWVkZTI2ZjlhN2VjODA5MDlhMTIifQ"
+          "resource": "repo://src/app.rs#L1204-L1209",
+          "version": "repo-lines-v1:sha256:9d9b7ef08292560e1f25590a1f38732ed3fd2b45ffe522b5928335427699b373:eyJzZWxlY3RlZExpbmVDb3VudCI6NiwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiNWM4Mjk4ODI1Mzg5YTU1NjFkNzUyZjZjZThhMjgyNzlkZTI4MDMyOTVlNTRlY2Y3NGQyODc2NWUwM2E3MDBkNSIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDEyY2EzNDVjY2Y3NWJmOWMwODA2YmNlNjk1YmU4ZGU4MDhiNzk5ODQyNTFhN2E1NGQyMDJjZjYxMDFkZDQ1MSIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiZjE5NGJjNjc2ZDg4NmZhM2MyZDM3ZTNlMGFhM2M4OTEwODc1MWZhYzYzZmQwNzViNDY3MzM3NDZlZGIyM2Q2YiIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiMDVkMmViYWNlOGUxMTQ2MDNlNjJkNzViNWY2ZGM4MWMzMmIzODMyYjhiMGIwZTNkNWFkNjQ5MzIwNDhhNmQzOCJ9"
         },
         {
-          "resource": "repo://src/app.rs#L991-L997",
-          "version": "repo-lines-v1:sha256:184085a61638b88a75f3e4c9a0f6423a3e374b61c7950b7a40df4b0897a7c86b:eyJzZWxlY3RlZExpbmVDb3VudCI6NywiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiN2ZjOGY0YmMzMjEzMzJiNTA5NzlhYTdlYWM3MmNjNTIzM2E5Y2U4ZmY0NzlkMThlYTM5MjFkNzUxNzc1NDE3YSIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDEyY2EzNDVjY2Y3NWJmOWMwODA2YmNlNjk1YmU4ZGU4MDhiNzk5ODQyNTFhN2E1NGQyMDJjZjYxMDFkZDQ1MSIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiYTQxZTYwMjlhYjc2NTVlMjZkNzIzZWNlMGM4ZTdhNjQyYTc0NmI4NDcxYTcwYmNlOWEwOTg2MTM0ODUxZDM4ZSIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiMDVkMmViYWNlOGUxMTQ2MDNlNjJkNzViNWY2ZGM4MWMzMmIzODMyYjhiMGIwZTNkNWFkNjQ5MzIwNDhhNmQzOCJ9"
+          "resource": "repo://src/app.rs#L351-L376",
+          "version": "repo-lines-v1:sha256:0f06f0bd1697dfc6eea76bf1331023d0de98cf0face31b358f6defedc38a31d0:eyJzZWxlY3RlZExpbmVDb3VudCI6MjYsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjU5NWFjMWY4MTY1YjkzNmU3MzY3MTRlNTgzM2FmNTc3YWZjMjdkMTUzZGY4NWEwNDRmMzkxNDJiOWI3OTZhODciLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImRiMmY5MGU2YmE0M2NkMDdhNGMzZWRmMWRiZTJiZWI0MmY4NDBjYzAzYjhjNDI3NWQ1ZGEzNDUxYzhkNDlhNDUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjBmZDk1MWYzMzMxMmIwZGIxOTkwNzNjZDIzNDA4MWM3MzMyNDIxZjYwZjRjNmQ4MDA3YmU2YjM1YzMwODYxNGUifQ"
         }
       ]
     },
     {
       "id": "claim_817155a979284282b5a78c3baad994c8",
-      "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.",
+      "statement": "On ReplyDue the kept images of the chat are taken and, if the chat is still pending, prepare_reply either ends the pending state when no reply is wanted, spawns the description of images still lacking one (the reply follows ImagesDescribed), or starts the reply at once.",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L356-L372",
-          "version": "repo-lines-v1:sha256:41cd96a610e59b5cfa51d2283c3c0297bb686cc2990f5a9e13a81349014f265e:eyJzZWxlY3RlZExpbmVDb3VudCI6MTcsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImExY2Q0NTRkZDVjMDM3ZWU2YmI1MDFmNmVhY2Q4MGY2ZjUxNzgzY2NjYmVhNDNmOWRjOWY3OGY5ZTZkOWE3NTQiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjQ4YjNlOGY3MDhlYTQwOGI0OGYyZWU3MTI4NmRiYzMyNjczNTJlZTlkMzhhNzE2ZDg3ZGVhMzAyNmQzMDk2ZWIiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjIyMGUyNjkzMzFhZTU2NTJiYTMxNWViYTdmYzRlNWMxNWFhNDlhMzdiMjE2NGY3NjRlYmNiYTA1ODE5ZGMyNDAifQ"
+          "resource": "repo://src/app.rs#L422-L451",
+          "version": "repo-lines-v1:sha256:7dc3d726de8b72086eda0835321c1115c4fd2cd81cc43f8ed74a3ad6704b3b61:eyJzZWxlY3RlZExpbmVDb3VudCI6MzAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjIxMGU4Y2Q4NGRkNGVlMzUzOTU2ODAxODRjZTNlYTEwZDlhMTI3Mjg1Yzk2MDVkY2RmNDJiZjkwOWJkMDZlNjYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImI5NDI0YzQ0ZDRkYWFkZTBkMTA4ZmM3Nzk5MDVlMGI2Mjk4NDJiMjk3ZjRkMWZhYTc5OTIxNTg5NDJlZGY1MzkiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjU3ODQ1MTBjNTZmNWE5YTNiMmY0ZTdkNmZjMDFjNGM4NTEyOWJjYzUzM2U1YTZjZTA2MTYyY2FiMmIwNGY4YzIifQ"
         }
       ]
     },
     {
       "id": "claim_c8b0ab491aad4ebf91a3b7e090cdbe2b",
-      "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.",
+      "statement": "The startup catch-up runs once per process on OfflineSyncCompleted, marks pending and prepares a reply without delay for 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"
+          "resource": "repo://src/app.rs#L571-L626",
+          "version": "repo-lines-v1:sha256:42baaf60c3fa91366d408e42913e5157b90816c6d76274c43152965dd45df15f:eyJzZWxlY3RlZExpbmVDb3VudCI6NTYsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjJiYWY4NjRiNzJmMjM1YjJhNzAxNzllOWRiZTlkYTg1ZmY0NWM1OTg4YjY1MTUzNmQyYjNmMDM0MzRkMTA3YjgiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjJkOWY0YzAxNDlmZTYwZWNjMDY0NDU2Y2VhNTgwNWIyMmI4YjgzZTljYzY0YzVmZTc3YzhjZDNjMzQ5ODg1OWMiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6Ijc4OTU5NGE5NzIzN2Y2NjVkMmU0Y2RhMTIyZmY1NTIyNzJjMjVmZTFhMTI0NmZjMGNkNjllMDQ4MjE2ZTNlYWMifQ"
         }
       ]
     },
@@ -203,8 +193,8 @@
       "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"
+          "resource": "repo://src/app.rs#L521-L527",
+          "version": "repo-lines-v1:sha256:9190cc2d950eb45e132eae01c1ae01b5fb4af82470bb42fae8b4e0227a315710:eyJzZWxlY3RlZExpbmVDb3VudCI6NywiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiYmMzZGZhMjZjNzdiNTNkZDcyZWZmZGI1MmEyNWFlNGY5YzMwNTdiZTk2NWI0MzRlMTMyOWIwMTM2MGZmNGViNyIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDg5M2Y0MGUzNzgxZWI4OTZjODJkMDQ2YTA4MDY3YTExZTM1Y2RmMmYxZGZhZjI2ZmVhOTBiOGRiNjY3NjQxMyIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiMTYyZWU1N2U0MWE4YzIwYWUwMTY0ZGYyNjE0MzlkY2M0OTlkMzUyMDgzNDE1MTUxNmQwNDdlOGRlYWU4YTQ2NiIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiOTEyYzE5ODI4NjExZDJhZjdmZWFmNjQyN2ZmNzE0MjFiN2JhMTU4MzM5OWY0YWU5ODBiMzJkNThlNDUzYTJiYSJ9"
         }
       ]
     },
@@ -213,26 +203,26 @@
       "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#L1148-L1150",
+          "version": "repo-lines-v1:sha256:fa265165a7dbaf620c22011a0c3c22d3351ef4c563f2f54f8fcb163c57bd1734:eyJzZWxlY3RlZExpbmVDb3VudCI6MywiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiOTBhZGVkYjQzODU5N2ZkNWM0ZTVlZTAxMmE5Y2RiYzY1Yjk4Y2VkYTcxMDc2YWU5YjE4MmFjMDNmMmQ5YWRlNyIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDEyY2EzNDVjY2Y3NWJmOWMwODA2YmNlNjk1YmU4ZGU4MDhiNzk5ODQyNTFhN2E1NGQyMDJjZjYxMDFkZDQ1MSIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiOWI5ODAxZjkzZGZiYTU3OWNmZTAzOWRiMGFlYTMwNzc1YWRhYTU2ZDM2NGUxZjE3NWJmNjYxYzhhZjg4MDRkYSIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiMmFiZDA1MjI3NzQyOTJhOTE3MDdjZDg0ZjRmMWExYTExNmQ3MWQzNDA3Yjg1Y2NkNDY4OWU1ZDM1MDBiYjAyNiJ9"
         },
         {
-          "resource": "repo://src/app.rs#L706-L720",
-          "version": "repo-lines-v1:sha256:a74b903231dad2e8c6a1b043953ed2693e6235ed8d22e62536eb65c3acb7c714:eyJzZWxlY3RlZExpbmVDb3VudCI6MTUsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjBjYTdlYjAxNjVkZmQ4YjM0Zjk0YjRlZjFjMWFlYmUxM2EwNjlhMDE2NjcyNTIxMTQ3ZTAwNTk3MTliYmY1YjAiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImEzMTk2M2YzNjVhMWM2NGM3NDk2YzRiZjEwOTZkMzUwMjZiZWJhNTI1OGEwODc0ZWFkNWNiMzZkM2Y3MmViMjgiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImQzMzk2MDA5NzYxYzQ4YWY3MDRkZDRmZDQzNWM1NTNhOTg2ZGQ5ZWIzMmZiNTYyZmY0MGFkODI0NGIwMWU4ZDQifQ"
+          "resource": "repo://src/app.rs#L812-L848",
+          "version": "repo-lines-v1:sha256:ebbdd1522a95030cb283c058e7c767edf7b3297cdfe336683fa94151dd1114b4:eyJzZWxlY3RlZExpbmVDb3VudCI6MzcsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjExNTZkYzNmM2MyODVjOTI5NmU1NWNlY2FiOGQzMzczNzg1YTgyOTVkYTZkZjU4Nzk1ZDUyNTZlNDc5MjRiMzMiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjU3NmVkYmFmNjIyMDAzYjdkMGYzOWE0YjE1ZGNkZjJmYTQ0NGNhY2Q5ZWRlMWRmNjc2OGI0ZWZiMzQ3OTVjNGIiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjdkZWE3NDZmZTI4YTEwOGI2ZDUyNmVkOWJlNzE1NjU3MmI2YjIwYTdmODgxYTI0NGU0OTViN2YzMzg4MWFiMDUifQ"
         },
         {
-          "resource": "repo://src/app.rs#L935-L937",
-          "version": "repo-lines-v1:sha256:fa265165a7dbaf620c22011a0c3c22d3351ef4c563f2f54f8fcb163c57bd1734:eyJzZWxlY3RlZExpbmVDb3VudCI6MywiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiOTBhZGVkYjQzODU5N2ZkNWM0ZTVlZTAxMmE5Y2RiYzY1Yjk4Y2VkYTcxMDc2YWU5YjE4MmFjMDNmMmQ5YWRlNyIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDEyY2EzNDVjY2Y3NWJmOWMwODA2YmNlNjk1YmU4ZGU4MDhiNzk5ODQyNTFhN2E1NGQyMDJjZjYxMDFkZDQ1MSIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiOWI5ODAxZjkzZGZiYTU3OWNmZTAzOWRiMGFlYTMwNzc1YWRhYTU2ZDM2NGUxZjE3NWJmNjYxYzhhZjg4MDRkYSIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiMmFiZDA1MjI3NzQyOTJhOTE3MDdjZDg0ZjRmMWExYTExNmQ3MWQzNDA3Yjg1Y2NkNDY4OWU1ZDM1MDBiYjAyNiJ9"
+          "resource": "repo://src/app.rs#L923-L933",
+          "version": "repo-lines-v1:sha256:3f82b1885fd37c0a7f8cddf8a1e57280241400b28724250c318d147c0b73a56a:eyJzZWxlY3RlZExpbmVDb3VudCI6MTEsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImM4NDJlYTc1OTQ1NzNkNTFkY2MyY2YzMTUyNDRkODE2Nzc4NWQwYjA1ODUzZDc3NzAzMWQ4ODU5NmVmNzRlZTIiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImY5OTI3ZWJiYWIzOWI4Zjc4ZGM3NzcyYTY3Y2YxNzhjZmY4Y2I5OTBiYWJlZjVjMTc1ZDQwNDRlZTllOWZmMWQiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImQzMzk2MDA5NzYxYzQ4YWY3MDRkZDRmZDQzNWM1NTNhOTg2ZGQ5ZWIzMmZiNTYyZmY0MGFkODI0NGIwMWU4ZDQifQ"
         }
       ]
     },
     {
       "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.",
+      "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 pending reply, kept images and answered snapshot, and closes it if open; any other key cancels the pending deletion.",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L640-L693",
-          "version": "repo-lines-v1:sha256:4e43012c3fa4132665986e82f140c4b11bd24adebb940cb03888ac8f096a5660:eyJzZWxlY3RlZExpbmVDb3VudCI6NTQsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6Ijc2NjA4ZjMwMzQ4ZWYyZThkYTg5OWZkNTJlY2RhOWI4OTQ0ZWI1MTUyODUyZTM0MzI2OGZkNDY3NGIyMDVhZmEiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjkzNzNhMTgxNzI2YzQ3ZjdjNzlmOTI3ZDI2NzhhYzQ3NjYzY2VhMzI1OGYxOGEwNTM0ZTg2NDVlYmFlOTlkMGUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjgyNTEwNTY0NjA2ZWRmNDUyODU1YTA4OTFlYmRlN2ZhOGZjYjIyMTBlODQyZjU4NDNjNzlhMmRmZDcyZjU5OTAifQ"
+          "resource": "repo://src/app.rs#L854-L906",
+          "version": "repo-lines-v1:sha256:170c2b5d404120c0eeed964795203ed9134af4677755a22c29cc61b36babe747:eyJzZWxlY3RlZExpbmVDb3VudCI6NTMsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjU1NWJlYTQ4MGM0OWY0N2ZiZDg2MGY3ZThiMDIwMzNmYmZlNGU4ZDJjYmZmMWU4Y2Q3Mjk3NjY2YTc3NDI2ZjEiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImI0MzQxZWJmNGViOTE5ZGQwMmUwYWE3YTVmNjM0MjdhNDI1OGQ4YzZkOTAwNDc4MWY3Zjc5YmI2ZTViNWUwZjkiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjgyNTEwNTY0NjA2ZWRmNDUyODU1YTA4OTFlYmRlN2ZhOGZjYjIyMTBlODQyZjU4NDNjNzlhMmRmZDcyZjU5OTAifQ"
         }
       ]
     },
@@ -241,7 +231,7 @@
       "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",
+          "resource": "repo://src/app.rs#L1052-L1069",
           "version": "repo-lines-v1:sha256:05cde78eab7927a8b5c33ce971f85c0e210ce60936eada88f8c9481585c1519e:eyJzZWxlY3RlZExpbmVDb3VudCI6MTgsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjlhMjY2MjVlMDNiMmE0MDg1NDg0YjQ2NjUzOTllOWRmOWFhZDQzYmVkNGQxNWFkODEwODMzMGIyMDhjNmM5ZTYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6Ijk5OWJkZWZlZWFjYTYzNDBlMDI5MGRjODZlODg5Yzg5ZDVlMTg5YWE1YmE0NGI1ODViM2I3MDczNGY0YTYyZDgiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImU2ZWQ1MmZlYjhjZTliMjNiYmNiYTkwMGFkYjk0MzE3MjA4ODNhZTk0ODY4NmQyNjhlNmRkMjFlNjljYTU3YjkifQ"
         }
       ]
@@ -251,7 +241,7 @@
       "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#L796-L837",
+          "resource": "repo://src/app.rs#L1009-L1050",
           "version": "repo-lines-v1:sha256:7e50e3ca57a62654a61947e33ba08f34d79beff1c2e3df006a2210c439244f6a:eyJzZWxlY3RlZExpbmVDb3VudCI6NDIsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjA3YTEzOTMxODdkZWNmZmM0NzMzYzYwY2FiYjA2MTkwYjdkYjAxMjJjYWEyYWM3YjRjNGUwNzBiNzIxNTk1NWQiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjM2MGUyOWExOGZkNDE5NDVjMjM3NTgxMzM4ZGI3NWFjOTIzYzYxYjVmYmFiOTNlOTM3ODdlNWJmMGMyMjk4OTciLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImJlODM0YjUzNzA2ODgzYmQzMmMxMmViNmQ0OGEyMmFjZjlkNTIyYzIzOWJlNTE5NDZmNzA5YjIxZDNkNjIwMzIifQ"
         }
       ]
@@ -261,18 +251,76 @@
       "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",
+          "resource": "repo://src/app.rs#L1418-L1432",
           "version": "repo-lines-v1:sha256:fef040ce667500067e70dc91e838a73c35752821bb041d4463b4361b4445b2c0:eyJzZWxlY3RlZExpbmVDb3VudCI6MTUsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImYxMTFhMDk4Y2E0M2I2Zjg1OTYwYTUxMjFiOGVmY2Y5MDVlZDZmNjdmZjE5MjY0NjcxZDhkYTFiYzhjZTEwNWQiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjM1ZjZmNDY2ZTU5Y2ZjMzA3MDNjNDVmNzJhZjY1ZGU1NGFjMzYzZDM1NGNjMWEzMzMwMzUwYWNkNDUyNDM0YzQiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6Ijg5NGUwY2FkODFjNTBiOWY4MWE5ODI2ZjA1YzUyZjY0MGE0ZGM3NzdiYWQyNGM2MGEwMjQ5NzhlZWJmMWRjMGMifQ"
+        }
+      ]
+    },
+    {
+      "id": "claim_779f06313b39432cbb9c3cb304427524",
+      "statement": "When ImagesDescribed arrives, each description whose message still exists is stored in the history, failures set one status with their count and the first error, and the reply is started only when the target asks for it and the chat is still pending.",
+      "evidence": [
+        {
+          "resource": "repo://src/app.rs#L1321-L1324",
+          "version": "repo-lines-v1:sha256:22bd4378ebfd0c259aac4089a03d61912cbb17abca836659a5d9c8fc9cde7b15:eyJzZWxlY3RlZExpbmVDb3VudCI6NCwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiYjA0MjdkNzcxY2Q5ZTlmY2YxZTMyODc5YWNlMDkzZWY5ODk3NjRiZjE4ZjE3OWY2YzFkZGFjM2M3Nzc1MDMyMiIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDEyY2EzNDVjY2Y3NWJmOWMwODA2YmNlNjk1YmU4ZGU4MDhiNzk5ODQyNTFhN2E1NGQyMDJjZjYxMDFkZDQ1MSIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiYTQzMjhiMGQzNWY1YmVlOTEzODE3NWRlZTE2ZDRmNmZhM2FmMzc1NDM0ZmIwOTMwYWU4Nzk5MDg2OTk2MTgxYyIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiOTViMzliZDc5NmMyYjVjYThiYjhjMWEzNjUzODk5NzY5MGE0OTE5ZDY5NzVlNGMxMGZiMWE1ZGE3MGU1Mjc3OSJ9"
+        },
+        {
+          "resource": "repo://src/app.rs#L474-L497",
+          "version": "repo-lines-v1:sha256:5736af4a9574d77087b2de7f56f7b77e2d96d8861d19b9c42159d91fa560f0f1:eyJzZWxlY3RlZExpbmVDb3VudCI6MjQsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImJmNTc4ODJlNGRmYjJmODViYWQyOGRhYWU0Njc1ZjIwODYxYzc2N2E1ZjIxNzg3OTIyYzYyZDgxOGQ5YjJiYjUiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjdjZWI1ZWFiOTZmZDM0MzEzNTI3ZjczMDk2ZjYwMWYzZDZiNWUxYmY1MzE3YTRmMzg0YWM0MTY4OWE0YWI1NTIiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjBhN2YyNDA4YTExOTJiY2VmZTI0ODJhNTg2MGM1MTVhMmQ4NGIxMzQzZTE1ZDdmMGVmNGRhMTA0YzY1ZTJiM2EifQ"
+        }
+      ]
+    },
+    {
+      "id": "claim_831a5e88df6448b9a0545ab015ba1580",
+      "statement": "finish_reply clears the pending state and, if a newer message that passes should_schedule arrived after the answered one, schedules a new reply; otherwise it drops the kept images of the chat.",
+      "evidence": [
+        {
+          "resource": "repo://src/app.rs#L1312-L1314",
+          "version": "repo-lines-v1:sha256:f837e4630ef0ee6fe89e07bf810f5851838ae48e227e0659b648d4ea480392fa:eyJzZWxlY3RlZExpbmVDb3VudCI6MywiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiNTk5ZjgxMThmMmE5ODIzYzNmZDQwNDcyYjlhMmUwNDE1ZTRhZGQzMWQ5NTkwNjE0NzY3MzlhYjk4YWIyNTUxMCIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDEyY2EzNDVjY2Y3NWJmOWMwODA2YmNlNjk1YmU4ZGU4MDhiNzk5ODQyNTFhN2E1NGQyMDJjZjYxMDFkZDQ1MSIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiNGQ4NDkzOWY4ODU5NjI0YmY1NGNiOTdjYWJjMDE2ZjRhYmY3OTU0YWUwNGU0MTVmODUzMjQ3ZTRmYWY2Zjc3ZCIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiNDlhNTk5ZGY5ZDUwOWVhMzBiNDMzZjkzYjY4ZjE4ZGQ1ODY2ZDVmMGJjMGMzMGEyMTU1OWFkN2U4MWM1ZmMwMCJ9"
+        },
+        {
+          "resource": "repo://src/app.rs#L505-L519",
+          "version": "repo-lines-v1:sha256:084f8b11c8537716d5b5aa7dd03d18d8c925c2c8808ed3bd55605fac82c22a47:eyJzZWxlY3RlZExpbmVDb3VudCI6MTUsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjNmN2EzMzI2Mzk4MGZiOTQ4OGI5NDcyZTI4ZGU0NjE4ZWVjMzc3MTE2ZmFmZDViODZlNmUzYTZhZGVjYjM2YTEiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjBiZTFkYWFmY2ViYWI1NjdjMWQwYjE3NmVjZmY3N2I5NGNmOTllYmRmYzI0MGZiMjFhYTRjNDIxYzBkYTRiNzMiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImNmMGY4ZWU3NGMyYmNhMjZlNTlmY2NmYjUzZmQyYWI2OGJjNzIwZTY2MGZiZjM3M2IwZjFhNTY4ZmIyNjBjOTUifQ"
+        }
+      ]
+    },
+    {
+      "id": "claim_388616d41b3945b386a74e91229b089d",
+      "statement": "spawn_ai_request awaits the request task in a second task so that AiReply is always emitted, even when the request task fails unexpectedly, and the chat never stays pending.",
+      "evidence": [
+        {
+          "resource": "repo://src/app.rs#L1218-L1229",
+          "version": "repo-lines-v1:sha256:5c8be8846fd13dfd0c92193083af62f733a451585be051143592b7b033237591:eyJzZWxlY3RlZExpbmVDb3VudCI6MTIsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjY2OWRlZjlmNmNmMDJlMjJjNGFhMmU4NTllMWY1MWZjYjdhOWUyYzc1NzRmZmFhY2IxNDNmOWIzZmUxMWY1ZmMiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6Ijc0ZmM5YzZmMDQxZWE4YTkyNjlhMGIzYmYzMTJjNjYwYjk4OTEyNWI5MDhmN2I1NjE2Y2IwZmZmMjM0NTI2YjUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjE5NTA2Nzc1NGYwNDQ1NDFmZmVlZDA3N2Y3OGMyZjg1ZTY3NjNiM2YzOTZkYjBlYTM5OTJmZDgxMmZjYWRmNDAifQ"
+        }
+      ]
+    },
+    {
+      "id": "claim_6bb34eb965544529b117ebd74cf5568c",
+      "statement": "When the model calls the no_reply tool, nothing is sent and the status shows 'AI: nessuna risposta necessaria'.",
+      "evidence": [
+        {
+          "resource": "repo://src/app.rs#L695-L701",
+          "version": "repo-lines-v1:sha256:f27e2fa9423ba6f3c9fb6381dd2ec60346c3c15ed204b861c320fac53ba2a473:eyJzZWxlY3RlZExpbmVDb3VudCI6NywiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiMWY1MGM3ZGZhNzg3MTk0NDViMjRiNDI2NjAyNjQxZDk0MmI1NjFmN2U1MmQ3Y2M2MGNkN2M5YzNiYjI2NWQzNCIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDg5M2Y0MGUzNzgxZWI4OTZjODJkMDQ2YTA4MDY3YTExZTM1Y2RmMmYxZGZhZjI2ZmVhOTBiOGRiNjY3NjQxMyIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiNzk0ZmJmY2NiNjQ2MWJhYjM5ZWE3YTVmZTJiZDJjMzU3YTQyY2I1NTUxYjE3NzJjNTc2YThjOTVjODkyMTY2ZiIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiZGVkOTlkNjVkMzhkZjFiODYxZTExYTc1MzdlZDk4YWI1NjA5NzEyYTVhZjgzNjdlNWY1Y2FkNjM4MGYyNWZiMSJ9"
+        }
+      ]
+    },
+    {
+      "id": "claim_dc12a441f287421887863d2001c4d63e",
+      "statement": "In list focus PageUp/PageDown move the selection by LIST_PAGE_ROWS (10) rows and Home/End jump to the first/last row, clamped to the filtered list.",
+      "evidence": [
+        {
+          "resource": "repo://src/app.rs#L31-L32",
+          "version": "repo-lines-v1:sha256:7825efb1211acd5d025f243b3bb470aeb24c370f07856a47d71779b8de576ab3:eyJzZWxlY3RlZExpbmVDb3VudCI6MiwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiMTVlZWU2ODI2YzIzMDYxMTIxNjY4N2UzNjYyM2E1YjA0NThhMGRjNGFjMGIxOTk3ZDZjYmVlNDNhZGJkMDgyYiIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNGEwYTBlYmUxMGIzNDk4NjVhMzlmY2Y3ZjJlMzY5MDM2MTA2NjdiNWQwYmU5MGU2NWZhYzUwNGJiNmZkNTM5MyIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiZmZiMmY1ZDFiZmZhMTlmNWYxODFhNjBkYTg2NWU5YmY0ODk4MDBkYjUzNDgwMjZjNjE4MGJmNjBkZDk4MjhmZiIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiZjM1MGFjYmZhYzdlNDEzZmFkNWYxMzk0NTQwYWE3MWM3YWY3ZWZmMDA4MDliYmMxYTlhNzQxZGRhNDFiOGYwZCJ9"
         },
         {
-          "resource": "repo://src/app.rs#L1201-L1215",
-          "version": "repo-lines-v1:sha256:8a598e5c5940c056f43610aa06cf7645c95a4e8f8fafac263afe969929ec6e1e:eyJzZWxlY3RlZExpbmVDb3VudCI6MTUsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjkyYjVlZGY1OTQ5Y2ZlYTY3ODIyY2Y2OTExMzIzNjNkNjhkNmYxZTE2OWY1ZTBkNGY3YzE0ZTVmYTFlNmQ0ZTUiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImYyZDgyOGEwMzdhYzFjODE1MzViMWQ1NTAwODMyMzIzYmZiYjg2NmEzYTNmODdlN2E2OGMxM2EyYmFlNTZmZmYiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImI2YjBlZjExZjE0Nzg5Y2Q3ZGY3MTBjYjExOWRhNTZmZjAyYjA3MThiNWQwODQzMTJjOTUwOTFkNzQzZTFhNTUifQ"
+          "resource": "repo://src/app.rs#L812-L839",
+          "version": "repo-lines-v1:sha256:acf06960921a82cc9b2dfbc4cd8be73f5ce9d8945326f358e29f727a80dd79f0:eyJzZWxlY3RlZExpbmVDb3VudCI6MjgsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjExNTZkYzNmM2MyODVjOTI5NmU1NWNlY2FiOGQzMzczNzg1YTgyOTVkYTZkZjU4Nzk1ZDUyNTZlNDc5MjRiMzMiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjU3NmVkYmFmNjIyMDAzYjdkMGYzOWE0YjE1ZGNkZjJmYTQ0NGNhY2Q5ZWRlMWRmNjc2OGI0ZWZiMzQ3OTVjNGIiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjFlYjhjMzVjMzllMzMxMTJhMGUzZTVlMDg1NjlmOWVkYWJkNDM5ZmNkM2FkZmVkNjQ5MzAwNjcyZWRiYjYwNjEifQ"
         }
       ]
     }
   ],
   "verification": {
     "by": "openwiki/0.7.2",
-    "at": "2026-10-20T20:27:06.515Z"
+    "at": "2026-10-21T21:49:12.291Z"
   }
 }

+ 47 - 19
openwiki/.claims/architecture/runtime-and-event-loop.json

@@ -1,14 +1,14 @@
 {
   "schemaVersion": 1,
-  "pageVersion": "sha256:7cbc08bb6eae59bb32a760bcc976c115e6702ecca67c1710b68b7a232fb16f49",
+  "pageVersion": "sha256:3abe82512bfb153e658472ff7afa50967adbaa76f4efa6bb2af3215c6b83795a",
   "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, starts the optional web panel, builds App, spawns the input thread and runs the TUI.",
+      "statement": "Unless a version flag is given, 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#L30-L54",
-          "version": "repo-lines-v1:sha256:78c36720946a6da37fed1551aa53c38d6a246801eca122beca85edd71edb50f1:eyJzZWxlY3RlZExpbmVDb3VudCI6MjUsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImExZDA2ZTA0OGNkMjcyNzM4ZDgzN2IwYzBlYzZhYWIwYjAzMTQ1N2U0NzVmZDQ5YWJiY2JhZmM1ZjczYzAyMGQiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImZhNDUwZGE5MjlmMjA2MmRjNmMxMzBhZTc4MDU2NWE2MGUyYTkyZmI4YjYzNTA5MTZkNTdiM2EwYzA1MDM4MTkiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjcyOGNjMGI0ZGIzNjFhYzMxMDc4YmMxOTZiZDY1MjkxMzVhNmFmYzQ1ZWNhZTkyMWY2MTcxMjI1ZTJmNGNkOTgifQ"
+          "resource": "repo://src/main.rs#L36-L64",
+          "version": "repo-lines-v1:sha256:2f5e5e305de372b540e8f2ab32af7faeb163b55a1cea3b4f31696097c921a6a7:eyJzZWxlY3RlZExpbmVDb3VudCI6MjksImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImExZDA2ZTA0OGNkMjcyNzM4ZDgzN2IwYzBlYzZhYWIwYjAzMTQ1N2U0NzVmZDQ5YWJiY2JhZmM1ZjczYzAyMGQiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6Ijk3YjIxYjgxYWE2YTcxZmY5NGYxNmE2NGVhMThkMjlkODhiYTE5YzkyYjM2MjUzMDhlNmU4NDM2MTkyNzM0MmQiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjAxZmI2ZDM1MThiYTRjMzUwOWRiMWM5MTAyMTk2NjQwMzI4ZjgzM2M3MDJmMzgwNmNmZjFlZGI5MzU4Y2Q5YTgifQ"
         }
       ]
     },
@@ -17,8 +17,8 @@
       "statement": "The WhatsApp forwarder wraps each WaEvent in AppEvent::Wa and stops when a channel side closes.",
       "evidence": [
         {
-          "resource": "repo://src/main.rs#L60-L68",
-          "version": "repo-lines-v1:sha256:6be0244c848e1db9107831f7228801caee72758797c9fa238420d9b550b1aa04:eyJzZWxlY3RlZExpbmVDb3VudCI6OSwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiMDFmYWUxNzcwNWQ2OTQ2NTViZGFlMjdlZTE4ZDA4MDg1ZjdjMzlmZjgyYWEzYjdiZGI0Yjc3ZGUxOTY3MjJjNiIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDEyY2EzNDVjY2Y3NWJmOWMwODA2YmNlNjk1YmU4ZGU4MDhiNzk5ODQyNTFhN2E1NGQyMDJjZjYxMDFkZDQ1MSIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiYzRiMzgyZTY4YzBlZTk1MGMwODdiOTA2YmZmMGJiMTQzNmI1MTFmMGFhZmVkNmFmNmE1YmM0ZTdhMTc5YmJlMCIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiYjU5MDFlMDA0ZGRhODVmZTA2ODQ4ZWRiOGQxYjMzZDU1M2ZhYzhmOTIzNmVlMWUyOWRlM2Y0ZTMxZGY1Mzg1OSJ9"
+          "resource": "repo://src/main.rs#L74-L86",
+          "version": "repo-lines-v1:sha256:376729d914bfe5462e810010c4e827fb7fc331182e173bf42c14ec815be9c5eb:eyJzZWxlY3RlZExpbmVDb3VudCI6MTMsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImRhNjQ2NjQ1ZDg1OTU4N2VjNWRjNmMyZWVjMzJhNjFkODI1ZDVmZjM0MWQzMmVlMmRhNGNhMWQwNzYzNmQ3ZjkiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImFhOGNhMzU1ZmQ1NzA2YTk1OTI3N2EwNDFlM2IxNTIzM2I4YzI0M2M0Njc4MGM4M2Y1ZjkyY2M2ZTU0MjhjN2QiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjNiMWI0ZGU5MmNiYmMwMmUxN2UyYWJmYjc3OGQyZDA0MjJmNjUzODM5YWZiNWE2MTdmNDhjNmE4MTBiZGU3YzIifQ"
         }
       ]
     },
@@ -28,11 +28,11 @@
       "evidence": [
         {
           "resource": "repo://src/main.rs#L23-L23",
-          "version": "repo-lines-v1:sha256:1d14980ebaa2f31e4144fe6b04613dfc3285892428a59dc526be0808a55c1eff:eyJzZWxlY3RlZExpbmVDb3VudCI6MSwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiMWQxNDk4MGViYWEyZjMxZTQxNDRmZTZiMDQ2MTNkZmMzMjg1ODkyNDI4YTU5ZGM1MjZiZTA4MDhhNTVjMWVmZiIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiMWQxNDk4MGViYWEyZjMxZTQxNDRmZTZiMDQ2MTNkZmMzMjg1ODkyNDI4YTU5ZGM1MjZiZTA4MDhhNTVjMWVmZiIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiNDZmZmE0OWU5ZTQyNmZkYjkwM2I2ZmIyNjYxZDdjZDEwZmM1OTljMjFjZjg2ZDMzMWFlNjY4NDBhMzUwZGNkMyIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiMzRkZmU0NTZlN2M0YTU3MTBiMzQzZDg2MWFjYTE5YWRmM2I1MDA4MzQ3OGYxMjdiM2QxYTZlNWM0MjNkMDMxNyJ9"
+          "version": "repo-lines-v1:sha256:1d14980ebaa2f31e4144fe6b04613dfc3285892428a59dc526be0808a55c1eff:eyJzZWxlY3RlZExpbmVDb3VudCI6MSwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiMWQxNDk4MGViYWEyZjMxZTQxNDRmZTZiMDQ2MTNkZmMzMjg1ODkyNDI4YTU5ZGM1MjZiZTA4MDhhNTVjMWVmZiIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiMWQxNDk4MGViYWEyZjMxZTQxNDRmZTZiMDQ2MTNkZmMzMjg1ODkyNDI4YTU5ZGM1MjZiZTA4MDhhNTVjMWVmZiIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiNDZmZmE0OWU5ZTQyNmZkYjkwM2I2ZmIyNjYxZDdjZDEwZmM1OTljMjFjZjg2ZDMzMWFlNjY4NDBhMzUwZGNkMyIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiODMyYWE3YjFiZGM3MTY1MmFjMmUwODIxNWJkOTcxYmQ3MGJjMjRjNzQyYWVlYzA5NDA4YjYzMTE3ZDkzMmQyMSJ9"
         },
         {
-          "resource": "repo://src/main.rs#L85-L116",
-          "version": "repo-lines-v1:sha256:cf6579340493800af2e3db3ff443a6e706051052b19e599dfa304084d854b29f:eyJzZWxlY3RlZExpbmVDb3VudCI6MzIsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImRhNzdjMDlkNzU5NTU0NWM4Y2M5NGU5YjhlZDg4YzQzYWQ4ZDAyNDAwM2FmYjE5NmFjOWNlOGZlMTMzMjkzNjYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjhmNWY0NWU2MTAxMTkyODQ5ZGNjNzA5NjBlMDA0M2Q2ZmM2ZTg5ZWIwNDkzYTc0MTMzYjAxMTQ2OWFmYzg0NWIiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImViNTc2ZTc2ZWM5NDRlZjNmY2IzMGZmZDlkYTJiOTU5NzNiNDUyYTdkYTQ4NjM5ODczNjM1MDExOGQ2MDU3ZDAifQ"
+          "resource": "repo://src/main.rs#L99-L134",
+          "version": "repo-lines-v1:sha256:58d18a88c972035530c97c24224afd69709cd2c5de07f4236969a3cc38d1fa8e:eyJzZWxlY3RlZExpbmVDb3VudCI6MzYsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjliZmVlNTkyZTcyMzMwNTAzZGVmODM4ZWFjZTE0ODNjMTFmNGVkOTk2OTM3YzlhODA3OGM1OGE4MTljYWVmNzUiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6Ijg0NTAxNDAzZmViNmI2ODk4YjM1MDcxOGQ4MmY3ZmU0YzM4YzBhNTljMjZkY2UzMTRjMzUyMDQ3OTcyYTk4NDQiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImViNTc2ZTc2ZWM5NDRlZjNmY2IzMGZmZDlkYTJiOTU5NzNiNDUyYTdkYTQ4NjM5ODczNjM1MDExOGQ2MDU3ZDAifQ"
         }
       ]
     },
@@ -41,8 +41,8 @@
       "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#L123-L129",
-          "version": "repo-lines-v1:sha256:85bac23651d9b95ec5f5593453b077b12706d21eed247ce921d00504110bfaad:eyJzZWxlY3RlZExpbmVDb3VudCI6NywiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiYzcxMDI5NWZiM2ZiYzY4ODZmZmNkZmJiMzEzMGYwMjgzYWM2MDY0MmQ5MGQ2OTllY2QwM2U1ZTI2MmM1NjVjMiIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDEyY2EzNDVjY2Y3NWJmOWMwODA2YmNlNjk1YmU4ZGU4MDhiNzk5ODQyNTFhN2E1NGQyMDJjZjYxMDFkZDQ1MSIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiZTkzZDY1YjVkODczOTc3NmUxMzMzZmZmZGQ0MzlhOGQ3YzQwYTA0MTAyNzUzMjY3NTQ4MDAwNmQyMGVmMGE2NCIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiNjQxY2IxZDYxYmJhN2RkZDlmYmI4YTI5MWY4MDdmMDVjNjY3NjkyMzY2M2RjM2IzYTcyOWQ3MzI4NDVkNDhjZCJ9"
+          "resource": "repo://src/main.rs#L136-L147",
+          "version": "repo-lines-v1:sha256:d8d9638a5e048622a2f5afb4eb87071516bfc768606487e27c53c03eb11ffa4f:eyJzZWxlY3RlZExpbmVDb3VudCI6MTIsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjJjNjVkZTMxODE3ZDgzNDIyNDg1NTNiNmZkZTAzN2M1ODI3YTRjOWI1NzA4ODAxMzc4NzYxNWM1NWMwNmU4ZDMiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjE5ZTc2MzU1Y2YxMTk5NDgwYzNmOTk4MzZiMTQ4NmM4NDFkODkxZmFhZDkwNGM2MDhlMDkzMDIyZDM3NTdhZWUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjY0MWNiMWQ2MWJiYTdkZGQ5ZmJiOGEyOTFmODA3ZjA1YzY2NzY5MjM2NjNkYzNiM2E3MjlkNzMyODQ1ZDQ4Y2QifQ"
         }
       ]
     },
@@ -51,8 +51,8 @@
       "statement": "run_tui always restores the terminal after the event loop returns.",
       "evidence": [
         {
-          "resource": "repo://src/main.rs#L135-L140",
-          "version": "repo-lines-v1:sha256:0f8fe295d420386182d43d18d5735ed806080dd2bfac8adb559c5685bef212f9:eyJzZWxlY3RlZExpbmVDb3VudCI6NiwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiYzI5YzU4MTYzNmNkOTM0NmZjNDQyMjUwOTZkZTU0NDQ3MDAwNGU0MmM1Yzg3Nzg1Y2Q0Yzc0Zjk1Y2I1NzJmNSIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDEyY2EzNDVjY2Y3NWJmOWMwODA2YmNlNjk1YmU4ZGU4MDhiNzk5ODQyNTFhN2E1NGQyMDJjZjYxMDFkZDQ1MSIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiOWQ4OGMwZjlkMTllYjQ0M2JlOGRhN2ZmNzc4MWM5MGEwNTBhZWVlMDJmOWI4ZDFhOTRmYTY3ZmZjMDc0YzY5YSIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiZWQyNGY5Y2EzODEzYjAyZDVkY2E4MmY0ZTc3NTM0ODc5ZGFhM2E0MjhmMmMzMzliMjU4NTgyNTk0MTg1NjI4ZiJ9"
+          "resource": "repo://src/main.rs#L149-L158",
+          "version": "repo-lines-v1:sha256:8bb99c5e51b2b1b83667961939978ee4834cb3bce0ba00fe956af3a2d48a09d5:eyJzZWxlY3RlZExpbmVDb3VudCI6MTAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjU4NjMxYjQ1YTE1ODQ4ZjQ5YTJhYWQyYTMzNDdiZWRkOWZkZGNmNDBhNTM5MTkwMDkzNDk1OTdhNzYwOWYzMmIiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjE5ZTc2MzU1Y2YxMTk5NDgwYzNmOTk4MzZiMTQ4NmM4NDFkODkxZmFhZDkwNGM2MDhlMDkzMDIyZDM3NTdhZWUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImVkMjRmOWNhMzgxM2IwMmQ1ZGNhODJmNGU3NzUzNDg3OWRhYTNhNDI4ZjJjMzM5YjI1ODU4MjU5NDE4NTYyOGYifQ"
         }
       ]
     },
@@ -61,8 +61,8 @@
       "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#L147-L166",
-          "version": "repo-lines-v1:sha256:644802a473993f7032f4dbd83c1464e2a0c1554ff7d51b3cc5503bc08fcf5548:eyJzZWxlY3RlZExpbmVDb3VudCI6MjAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjkwNTMwZjA2M2RhYzc0YzIwZGZmMGE4YmI5MjA5MTFmYThjYTA2YWRhMTMyMDU4ZGI2NTRiYjBjNDg5MTE0YjAiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjFmYzBjMDAyZTcyMTE3NTg2ODEyN2Y4ZmZiYzNmYTVlMWNhNDBjNmYxYjNlM2JiNTUwMDU3YmU0NDkyYWI2MTIiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjowLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImUzYjBjNDQyOThmYzFjMTQ5YWZiZjRjODk5NmZiOTI0MjdhZTQxZTQ2NDliOTM0Y2E0OTU5OTFiNzg1MmI4NTUifQ"
+          "resource": "repo://src/main.rs#L160-L184",
+          "version": "repo-lines-v1:sha256:c813b17a84a3084eefaa34b8e9284507be8cfac301dd0b8abab485d88d66204a:eyJzZWxlY3RlZExpbmVDb3VudCI6MjUsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjNlMTkzYjAzYzI2ZWIzM2EwMjk4MWViY2MyNmM2YzIxNmRlZGQzYTkxMDQ5OTA0ODI5ZjJmNDQ0ZTVmYWYyODUiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjAzYTEwMzdmMDcxOTYxZmIzYjEzYTdkYWZmNjlhY2MyYmFiODEwZmU4YmRhMTZhMmIyMjcxNTU0MzcxNGNjNzUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjdiYzk4ZDZiMGRjNDkyYzUwMTZkZjEwODA1YTNhNTRlYjljMTg0YTVhNjEzN2U3ZjRiZWNmYTBjMjIwNTUwZDAifQ"
         }
       ]
     },
@@ -71,8 +71,8 @@
       "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#L142-L166",
-          "version": "repo-lines-v1:sha256:c813b17a84a3084eefaa34b8e9284507be8cfac301dd0b8abab485d88d66204a:eyJzZWxlY3RlZExpbmVDb3VudCI6MjUsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjNlMTkzYjAzYzI2ZWIzM2EwMjk4MWViY2MyNmM2YzIxNmRlZGQzYTkxMDQ5OTA0ODI5ZjJmNDQ0ZTVmYWYyODUiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjAzYTEwMzdmMDcxOTYxZmIzYjEzYTdkYWZmNjlhY2MyYmFiODEwZmU4YmRhMTZhMmIyMjcxNTU0MzcxNGNjNzUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjowLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImUzYjBjNDQyOThmYzFjMTQ5YWZiZjRjODk5NmZiOTI0MjdhZTQxZTQ2NDliOTM0Y2E0OTU5OTFiNzg1MmI4NTUifQ"
+          "resource": "repo://src/main.rs#L160-L184",
+          "version": "repo-lines-v1:sha256:c813b17a84a3084eefaa34b8e9284507be8cfac301dd0b8abab485d88d66204a:eyJzZWxlY3RlZExpbmVDb3VudCI6MjUsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjNlMTkzYjAzYzI2ZWIzM2EwMjk4MWViY2MyNmM2YzIxNmRlZGQzYTkxMDQ5OTA0ODI5ZjJmNDQ0ZTVmYWYyODUiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjAzYTEwMzdmMDcxOTYxZmIzYjEzYTdkYWZmNjlhY2MyYmFiODEwZmU4YmRhMTZhMmIyMjcxNTU0MzcxNGNjNzUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjdiYzk4ZDZiMGRjNDkyYzUwMTZkZjEwODA1YTNhNTRlYjljMTg0YTVhNjEzN2U3ZjRiZWNmYTBjMjIwNTUwZDAifQ"
         }
       ]
     },
@@ -81,14 +81,42 @@
       "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",
+          "resource": "repo://src/main.rs#L88-L97",
           "version": "repo-lines-v1:sha256:54c6448bd45241fd4b90479b1a91e2fd528e7a38f4ac5d543eda728fbf3756d5:eyJzZWxlY3RlZExpbmVDb3VudCI6MTAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImYwZjA3MmY1NzA4NTRiMGM0ODg3YmRlZDU4YzU2NWRiZGExOTNlYmY3NGIwODZmMjYyNTFmZjZhOTZiMmJiYWMiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImRmYWI3MTBkZGY3ODU5ZTU3NTc2NGE5MTNhMDAyYzc0NTg1ZmZhNzAwMTUxMGNiODgxMWRhODNkMmNmNzRiMzQiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImI1OTAxZTAwNGRkYTg1ZmUwNjg0OGVkYjhkMWIzM2Q1NTNmYWM4ZjkyMzZlZTFlMjlkZTNmNGUzMWRmNTM4NTkifQ"
         }
       ]
+    },
+    {
+      "id": "claim_f1118b0552ab43838f216917a238b0e2",
+      "statement": "With a --version or -V argument main only prints 'wh <BUILD_VERSION>' and exits before loading the configuration.",
+      "evidence": [
+        {
+          "resource": "repo://src/main.rs#L24-L41",
+          "version": "repo-lines-v1:sha256:ce72f2efa3e3637cbeac509d5672e3b6228a19b2d405cc1402d7f536c98d46dd:eyJzZWxlY3RlZExpbmVDb3VudCI6MTgsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImUzYTMwMjZjMmE4MzdhNTVhMTBlMmU2OTI4NmY3MTVjZDljNGMwNTBiOTJjZmE0MTg0M2YzNTk0OTA4MDgyNGYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImE4MjYzNjdkZjY0MGI1YzFkZTYyYjFlNDdkZTc4ZGMxMmY2Y2YxNDAyMTI5OTllOTg2YWI1MDVmYmMyYjZkYjIiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjEzNGZhODM0YjVmMWVkZTIwMmNmNGQ0OWVjNDc0NDI0ZTcxNjMxYTJmNzFkOTUzYjQzMjRiODY4NWEzMGQzYTcifQ"
+        },
+        {
+          "resource": "repo://src/main.rs#L66-L72",
+          "version": "repo-lines-v1:sha256:c7d36e35db0f27324cf4631b197cf7332f8de7e7002d977fead70ef0f6213c51:eyJzZWxlY3RlZExpbmVDb3VudCI6NywiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiMjMwMTlkMzNiYmQ2ZmY5MGU3MWNhNWQyYTA0NTI3ZDNhMzg3MzVlM2FhYjA5NjQzMDk5MWVhNDNhZDNjOTVmMyIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDEyY2EzNDVjY2Y3NWJmOWMwODA2YmNlNjk1YmU4ZGU4MDhiNzk5ODQyNTFhN2E1NGQyMDJjZjYxMDFkZDQ1MSIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiNGI3MGY3Nzk4YzAxYzhmOTQwNTBiZTU0MTQwMWI4MzQ3YWY2MDM2MjdkZDExODBiYjIzZjAxMWZmNjQ1NDhjZiIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiNzI4Y2MwYjRkYjM2MWFjMzEwNzhiYzE5NmJkNjUyOTEzNWE2YWZjNDVlY2FlOTIxZjYxNzEyMjVlMmY0Y2Q5OCJ9"
+        }
+      ]
+    },
+    {
+      "id": "claim_4de286148c754addaeb2e9d98014190e",
+      "statement": "build.rs embeds the short git commit hash as WH_GIT_HASH (falling back to 'unknown') and BUILD_VERSION combines the package version with that hash as '<version> (<hash>)'.",
+      "evidence": [
+        {
+          "resource": "repo://build.rs#L1-L33",
+          "version": "repo-lines-v1:sha256:862b7d4a061487e5c9a432ace9cadefe3bc590f6eb970126321e7e932fdd4b65:eyJzZWxlY3RlZExpbmVDb3VudCI6MzMsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjAxNjZlNzY2Mzg4OTM3MmMxNDVhMDYwZDY2Njg0ZTliMGJmYTU5ZjBmN2MzMmRiOGUyMGZiYWI5MWY2MmFlZTIiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjowLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImUzYjBjNDQyOThmYzFjMTQ5YWZiZjRjODk5NmZiOTI0MjdhZTQxZTQ2NDliOTM0Y2E0OTU5OTFiNzg1MmI4NTUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjowLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImUzYjBjNDQyOThmYzFjMTQ5YWZiZjRjODk5NmZiOTI0MjdhZTQxZTQ2NDliOTM0Y2E0OTU5OTFiNzg1MmI4NTUifQ"
+        },
+        {
+          "resource": "repo://src/main.rs#L24-L25",
+          "version": "repo-lines-v1:sha256:0cf641f44d6658b7b5f5b7157c9d533ac928fa26293fd9f090fcaf80de1ad016:eyJzZWxlY3RlZExpbmVDb3VudCI6MiwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiZTNhMzAyNmMyYTgzN2E1NWExMGUyZTY5Mjg2ZjcxNWNkOWM0YzA1MGI5MmNmYTQxODQzZjM1OTQ5MDgwODI0ZiIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNmQ0OWQyMmQ3NGM5NWQzZDFmMjJjNGNkN2JmNDA1ZGYxNWU2NTcxNDBjMGMwMWFjNDUzZmEwNjQxOTAzYWRhMyIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiYTgyNjM2N2RmNjQwYjVjMWRlNjJiMWU0N2RlNzhkYzEyZjZjZjE0MDIxMjk5OWU5ODZhYjUwNWZiYzJiNmRiMiIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiNWEyMmU1NDJiM2M2MWYzMjAzNTI1OTYwZGFhYTNmMjA4NGYxNTkxNDI4OTNhZDhhMjdhZDE3M2QzZWQ0OTRkNSJ9"
+        }
+      ]
     }
   ],
   "verification": {
     "by": "openwiki/0.7.2",
-    "at": "2026-10-20T20:27:06.515Z"
+    "at": "2026-10-21T21:49:12.291Z"
   }
 }

+ 32 - 18
openwiki/.claims/architecture/terminal-ui.json

@@ -1,13 +1,13 @@
 {
   "schemaVersion": 1,
-  "pageVersion": "sha256:52543285487729f07ef002bcc4cb827b749e6ec744b94cd40269ba91da70160f",
+  "pageVersion": "sha256:d47df0cc7d426e68c11679d15aeb6042a2f2f7ca975321194f44268297b7bc05",
   "claims": [
     {
       "id": "claim_7b6015e24275488a85ba88dd065f2ff8",
       "statement": "render dispatches on app.screen to the connecting, pairing or main screen renderer.",
       "evidence": [
         {
-          "resource": "repo://src/ui.rs#L105-L112",
+          "resource": "repo://src/ui.rs#L107-L114",
           "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#L190-L199",
+          "resource": "repo://src/ui.rs#L192-L201",
           "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#L169-L183",
+          "resource": "repo://src/ui.rs#L171-L185",
           "version": "repo-lines-v1:sha256:f70009d551400b4459543b770a01bcb4ca2cfec3c41a6bd11efe47faa57a3e59:eyJzZWxlY3RlZExpbmVDb3VudCI6MTUsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjlkZDQyYTM5MDMyYmQ1ODhiZmNiMTM5NjgyMGY3NmI0ZmIyMmNhMDY3YWU2OGNkZDdjYzhmNjZiZjgzMjMzNGQiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjkzMzJjMDRlNjVmOTMyMGRkNzNjY2VlM2IxOTBiZDZkNThmYmE3ZjhmZTEyNzg4MDA3MmRiNmM5YTMwZjg0ZjMiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImNlYmI2MGRjYjRjZTI4NzBjZWM3ZGE2YTk5YmI4MDk5NGIyYWIzYWQzZWI5NTc3ZTE2MDFmYjU0NzI3NjZlOWIifQ"
         },
         {
-          "resource": "repo://src/ui.rs#L73-L79",
+          "resource": "repo://src/ui.rs#L75-L81",
           "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#L205-L220",
+          "resource": "repo://src/ui.rs#L207-L222",
           "version": "repo-lines-v1:sha256:c7b573159910f69890eba199ba66af477c525be3a8732bcbcd2f9ba7be836466:eyJzZWxlY3RlZExpbmVDb3VudCI6MTYsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImM1NWUwYzhkYjY0N2Q0ZDUwZmM0MWY5Mzk1NjgxMmM5OTk4ODQ3NzZmYzVmMjZiOTM1Zjc1MDA1YjQ3M2U1YmUiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjUxODU3ZjE2ZjQ5MDJjOWNhZWM0MDU1NTgxYWI4ZWJiZjFhYzU0NGU3OTMxMjM2ZWY1NDAxOWVkYTkyMDlhNjMiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6Ijk2YmI0MWI1YThhOTczMmVhYjg5MTA4MWE4MDRmYjU2ZjEwNGRiYzAzNDVhYzAwOWQ4MzdhYzhiZWVhN2FjMzgifQ"
         },
         {
@@ -55,11 +55,11 @@
       "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#L240-L259",
+          "resource": "repo://src/ui.rs#L242-L261",
           "version": "repo-lines-v1:sha256:732f44ba82916dfa1b3ddde98a73ecbac245a78006147401460b3c1aa17b9b15:eyJzZWxlY3RlZExpbmVDb3VudCI6MjAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjVhYWI1NDY2NTFmMTk2NDNiOTg2MGQzNjBkMmYyZGE2YmIxNTMyYzAzZGRkMzIxNGI3YTg5ZWFiOTFmNjgyZDQiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjliZDEwMzhkZWZmODFiYWFlZjU0N2JjMGI2NTk5ODc3YTc5NjhhOWRiZDBkMWM5YzZkODFjOTBhMTJkM2UzM2YiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImJkYjk3MWM3OTZlNTNmZTE5ZDE3OTc4OWIwMDRlZDEzZTdhOWEzN2VhOGNkNGYwNmQ3NzgxNzQ0NmUxNDA5NzYifQ"
         },
         {
-          "resource": "repo://src/ui.rs#L281-L296",
+          "resource": "repo://src/ui.rs#L283-L298",
           "version": "repo-lines-v1:sha256:81e36f0dbffed3b264bf79086b294804a0e2423387d63ef243ec387746d2b84c:eyJzZWxlY3RlZExpbmVDb3VudCI6MTYsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjBlN2Q5YjRmZmQ1M2VhYjVmMjdhMmFiNGJlZGI5NWNlYzEyZTRlMTRlNTBjMWRhNTdhZWNlZjdlNTBlZDM1NTkiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjBjMmZiOGQ3YjJjMGMwNTI5MmVjNGM4ZWQ0OTYxMzk2ODgzZjYwZjdkYTQ3Yzk5NzdmZTVmZTU1NTMyMDM1ZDIiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImZkZTk5NjAxMDljZTU4NjlmNjRmNmFmNGMwNDYzMWU2MmU2ZDFlODU5MGYwMGNlMWNhZGYyY2Q0N2JkYmU5ZTEifQ"
         }
       ]
@@ -69,7 +69,7 @@
       "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#L310-L346",
+          "resource": "repo://src/ui.rs#L312-L348",
           "version": "repo-lines-v1:sha256:c291029f490ad470710b3ac4482737631d1c35ee2eff8d99cde384408b75b573:eyJzZWxlY3RlZExpbmVDb3VudCI6MzcsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjMwM2M5MTRlZDU2ZmZiODJmYzlhZDJjZjQ0NDQxMzRmZDRhZTkyZTkzZmYxMTQ2NzZlOWI3NjI2NTUwZDVjMzciLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjBiOWJkNjEyZDA0MDhiOWE2NjFiNGQ4YzJkYThlNzk4OWRmYWIxNmVlOGEyNjBmN2NhMjI2NjBhODRjOWZiNDciLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImJlNWU3NzM0ZjZhNWM1NWJiOTExY2U5ZTNhYjgwYWExZmZmZjljN2FmODgzMTUwMzg1ZDg4ZTVmM2M5ZTJiOWYifQ"
         }
       ]
@@ -79,7 +79,7 @@
       "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#L492-L522",
+          "resource": "repo://src/ui.rs#L494-L524",
           "version": "repo-lines-v1:sha256:4af9cab79ebcb71cd601b44fe7a4b8fcceb1fe16ecac50a20ec643453b72d21d:eyJzZWxlY3RlZExpbmVDb3VudCI6MzEsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImM5YzFjMzliNjRjZmUzMjlkNGFjZmM2NTNlYTY1M2RmOTBiMGI5MDQ0NDJlMWU4OTZhMjFhMGUxYmY3ZDRkNjAiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjBjOTcwNjYzMjgxOGU3MzliNDJhYWZlZTA2OThlZGQ1N2RkZjc5MmM1ODcxY2I3ZGNiZTliOTBhYWU2YjJhYzEiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImEzYWY3MDc2MmFmY2NkYTcwOTEyZWVkMDUwYzlmZTlmNzVjNzg5YjMwMjVjM2NkOWRhYmMwYTBiYjhkODI1MjQifQ"
         }
       ]
@@ -89,12 +89,12 @@
       "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#L524-L556",
-          "version": "repo-lines-v1:sha256:d0e8646c9a73f738bb5de0be1ad81c7138c3994f200493bc090e5e1e3845fff0:eyJzZWxlY3RlZExpbmVDb3VudCI6MzMsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImJiNzk4OWQ5NWQzZGZlM2YyODliYzI4Njg5OWNjYzAxZWI2YzVkNGQyMzZkNDU3Mjg4YjJiYTA4YjZkMWRkNGIiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjNhMzZhNmJkYjRhZWM3MTA3YmVjYTFmM2I1OWZhMDBlOWVkYzdlYWZmNmI2YjUwMDlhMzQ4OGM2MjZhMDdlYTUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjdiYzk4ZDZiMGRjNDkyYzUwMTZkZjEwODA1YTNhNTRlYjljMTg0YTVhNjEzN2U3ZjRiZWNmYTBjMjIwNTUwZDAifQ"
+          "resource": "repo://src/ui.rs#L526-L566",
+          "version": "repo-lines-v1:sha256:5cc3c702fe6df48a350aa6157256a193b7dd8cef520a91d2deeef73b07e1b798:eyJzZWxlY3RlZExpbmVDb3VudCI6NDEsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImJiNzk4OWQ5NWQzZGZlM2YyODliYzI4Njg5OWNjYzAxZWI2YzVkNGQyMzZkNDU3Mjg4YjJiYTA4YjZkMWRkNGIiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjAxYmE0NzE5YzgwYjZmZTkxMWIwOTFhN2MwNTEyNGI2NGVlZWNlOTY0ZTA5YzA1OGVmOGY5ODA1ZGFjYTU0NmIiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjNhMzZhNmJkYjRhZWM3MTA3YmVjYTFmM2I1OWZhMDBlOWVkYzdlYWZmNmI2YjUwMDlhMzQ4OGM2MjZhMDdlYTUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjQ1MTY0YWM3NTJmMmY2YWE3N2JkMTM0OGI0ZmQ2MTgxZGU4NmQ4OWU0MzY2MzliMDc1ZWIzMmY4YTc1ZWM4NTgifQ"
         },
         {
           "resource": "repo://src/ui.rs#L53-L59",
-          "version": "repo-lines-v1:sha256:6375d8760354f1a87914855b5565f443be78757b62647d467bf23d52b5b1b491:eyJzZWxlY3RlZExpbmVDb3VudCI6NywiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiNWJiMDYzYTdlZmM4YTczMDVhM2NhNjliYjFkYjEyYTliMjg3NTNhNWM5MzY0ZmE1NTUzYWYyOGI1NDY0MTI1MiIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiMzljMmIzMjhmMGU0MDc5ZDhiN2ZhZjQxOWUyM2Y3YjkzN2QxNGQ2YzZmZjU2NmUwYmRmMjcyZGNkOTVkZDM0YiIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiMjU3MWY4OTdkYmU1OGFhZDg5YTBkNzc1MWU0YTIyMTAwZGI5Y2UzYmI4MDllMTRmMjBlNWY4ZDJlYWQ3MGJhNiIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiNzEwYmQ3N2I3NWExZjgxMjI2MjkwNTY1NWU0MTk1YjA0ZjIzZjQ0OGRkNDVkZjE4ZmJiZGY3NGMyMzAxNmIxYSJ9"
+          "version": "repo-lines-v1:sha256:6375d8760354f1a87914855b5565f443be78757b62647d467bf23d52b5b1b491:eyJzZWxlY3RlZExpbmVDb3VudCI6NywiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiNWJiMDYzYTdlZmM4YTczMDVhM2NhNjliYjFkYjEyYTliMjg3NTNhNWM5MzY0ZmE1NTUzYWYyOGI1NDY0MTI1MiIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiMzljMmIzMjhmMGU0MDc5ZDhiN2ZhZjQxOWUyM2Y3YjkzN2QxNGQ2YzZmZjU2NmUwYmRmMjcyZGNkOTVkZDM0YiIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiMjU3MWY4OTdkYmU1OGFhZDg5YTBkNzc1MWU0YTIyMTAwZGI5Y2UzYmI4MDllMTRmMjBlNWY4ZDJlYWQ3MGJhNiIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiNWIxODI3ODdlZjdmNzdmNGRhODk4NWY5ZmQ3NGZlMjI0ZTcwNmVmOGY4OTkwZDk3YTVmNDBkZjI3YmEwNDRhZCJ9"
         }
       ]
     },
@@ -103,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#L377-L435",
+          "resource": "repo://src/ui.rs#L379-L437",
           "version": "repo-lines-v1:sha256:c499132db2ac5eead3eb62f5ad7bab3b7f8d914d35adb23bfc5ca621870465f2:eyJzZWxlY3RlZExpbmVDb3VudCI6NTksImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImM3ZWNmMTZhOWUxM2QxZTA4ZmRhYmUzOGM5ZjU3MmMyZjRmNmUxYTkwNGNiODAxYWY3M2RlNzllOTczN2NhZjYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImMyZDgzZjgxYTNiOWQwZjYzMWZmYzlmMTg5ZGQyYTY4NWNhYzJmOGViODA3NTYwYWQxNzlkZTk2Y2NiNjgzMzQiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjQyOGI1ZWVjNDkzNDMxZWNmZGY2ZTIyZGVlMjc5OThhNDcxOTczN2Q1Mjg1NGI3MGEwZmM3MzUwZjViMjg4YTQifQ"
         }
       ]
@@ -113,7 +113,7 @@
       "statement": "AI messages are cyan, own messages green and incoming messages use the default style.",
       "evidence": [
         {
-          "resource": "repo://src/ui.rs#L365-L371",
+          "resource": "repo://src/ui.rs#L367-L373",
           "version": "repo-lines-v1:sha256:9a0c35114d98982d540867094c84291c906d6d832ab746b9c4f2dec88b905a7c:eyJzZWxlY3RlZExpbmVDb3VudCI6NywiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiMDg0NDE5ZmQ0YjVmM2E4YzE4MTM4M2Q4NmQ5ZDc0ZDhiMWUyOWVlZjk5ZGMzNTM3ZGNhNjYzYzI1YjM2Mjk4YSIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDEyY2EzNDVjY2Y3NWJmOWMwODA2YmNlNjk1YmU4ZGU4MDhiNzk5ODQyNTFhN2E1NGQyMDJjZjYxMDFkZDQ1MSIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiZmVmNTliMjU3ZDI4Yjk0NTA2YTg2YTU0MmQyZThhOWNkODlkNDNjNmIwMmY1YTNhYmZjNWIzYmY2ZmJkZGE3YyIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiOWVlODhlMWI2M2U2ZWU3M2I1NDcyNTVjN2EyYzQzMzE1NjJkNWE2OGE1NDc3ZGRkMzY0OTZiMzc5ZmYxNWYxYiJ9"
         }
       ]
@@ -123,7 +123,7 @@
       "statement": "Text wrapping splits on newlines, wraps by display-width characters and drops control characters.",
       "evidence": [
         {
-          "resource": "repo://src/ui.rs#L441-L486",
+          "resource": "repo://src/ui.rs#L443-L488",
           "version": "repo-lines-v1:sha256:e966afd08433fba747794711a95f264804c09ac9f81a7c818a6f80be9a2893ad:eyJzZWxlY3RlZExpbmVDb3VudCI6NDYsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImYwYTJhNzczNmUxMWZkOGUyYjYyYmViMWM5NjNjZjRjNWRmYmNiNTIzNDZkNDdlOTgzNTgwZmE1ZmY1ZjY4OTQiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImNkNWU4MTdmODljYTM3MjE2NTg3NGM0OGRmNzBjMjRiZTc5YTkxZmQxODQyZWE3OTI0YTllZDQzMzg2ZDdlZDgiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImM1MjY1OTg3OTliMDNjYmQ4ODczY2RjNjRhOWJmOGE2NGQyOGMxNjhmNDkwYTMwM2ExZTkwY2U5ODkzMDc4ZmQifQ"
         }
       ]
@@ -133,14 +133,28 @@
       "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",
+          "resource": "repo://src/ui.rs#L242-L277",
           "version": "repo-lines-v1:sha256:fc4a9a5029ba905ce7fee869463b44f5f84ba4ff03c0566e28b38b850f9145bb:eyJzZWxlY3RlZExpbmVDb3VudCI6MzYsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjVhYWI1NDY2NTFmMTk2NDNiOTg2MGQzNjBkMmYyZGE2YmIxNTMyYzAzZGRkMzIxNGI3YTg5ZWFiOTFmNjgyZDQiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjliZDEwMzhkZWZmODFiYWFlZjU0N2JjMGI2NTk5ODc3YTc5NjhhOWRiZDBkMWM5YzZkODFjOTBhMTJkM2UzM2YiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjQyZjY4ZGVhNTk5MTczNjlkNmMwZGJhNTg5NTZhNDI3OWYzMWJhNDMwM2YxNjY0Y2QxY2M1ZGNhNzVmODIxNDQifQ"
         }
       ]
+    },
+    {
+      "id": "claim_b1dea223b7104bf29b27931738b2d194",
+      "statement": "The footer reserves a right-aligned area exactly as wide as the version label and draws the build version there, dimmed, as 'v' + BUILD_VERSION + a trailing space.",
+      "evidence": [
+        {
+          "resource": "repo://src/ui.rs#L545-L573",
+          "version": "repo-lines-v1:sha256:d8e5581d97934ea27752d49d8ae47361b347d25a53b08c164b4f7a0f40518099:eyJzZWxlY3RlZExpbmVDb3VudCI6MjksImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImQ3NTc1MjJiMDY1NmVmYzZjNjcyNTk0MTJkZWFlYWRkNjY3ODAxZmFiZjEyMTFjMmIyZWM1NGY4NTJmZDBjMjkiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6Ijk2YTgwOWQ2ZjliZWQyNjAyNzhlYjI2ZGVjOGRmZDVjOWUwMDVhMzAxNzI4MmU1YjNlMmZhYjZkOTBiMGZhYWIiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjdiYzk4ZDZiMGRjNDkyYzUwMTZkZjEwODA1YTNhNTRlYjljMTg0YTVhNjEzN2U3ZjRiZWNmYTBjMjIwNTUwZDAifQ"
+        },
+        {
+          "resource": "repo://src/ui.rs#L60-L65",
+          "version": "repo-lines-v1:sha256:dc21562a3695c6a131645edbb4347b667e125c8e42450e3629835edc7ef58958:eyJzZWxlY3RlZExpbmVDb3VudCI6NiwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiOWEwNmNhOTM5ZTU4YmY5ODk3ZjcxMTQzMWU4MGQ0Mzc3NWYxMWRjZjAxOGVjYjA1Yjg2NTlhZDRkMjhmZDE0ZCIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiMDBjYjlkMmVkNTFiZDg1ZjA4M2Q0ZWFjYTBjZmViNjJkZDQ0ZTNhZTdmNWQxYjZiOTYxYzQ5NTgxNWEwMDQwMiIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiMDJiNGUzZDFhN2IxOTVlYzQyYzg5Mzg5OWI4MDA5NDU1ZmM2YzIxMzQ0MzYxMTMyZTViOTIxYWIwMmYzYjQ5YyIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiMjdlMGVmMGY5NTkwNDBmMzI2YzcxYjJmMjdjMzI3NDQxNDVjZTJiNjBmZTcyNTgzMjAxMDI5MTM1YTIxMDBhZCJ9"
+        }
+      ]
     }
   ],
   "verification": {
     "by": "openwiki/0.7.2",
-    "at": "2026-10-20T20:27:06.515Z"
+    "at": "2026-10-21T21:49:12.291Z"
   }
 }

+ 42 - 26
openwiki/.claims/concepts/history-store.json

@@ -1,24 +1,24 @@
 {
   "schemaVersion": 1,
-  "pageVersion": "sha256:af82818f0b14ff0161574d8625a43bed0807de3f4b8dc152a3283e1a40bccf24",
+  "pageVersion": "sha256:fb61743878eebfa7a936223b03e0c9a3d4247468218b42035889a35e1283bc0a",
   "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#L65-L98",
+          "resource": "repo://src/history.rs#L67-L100",
           "version": "repo-lines-v1:sha256:43f65e608bb68aead3f44bea548e6e7b8169cd5274874c39a748cec236b2373e:eyJzZWxlY3RlZExpbmVDb3VudCI6MzQsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImVmODI1MzY0MWU3OWEzYzQyOTk5MDgzODFhZGQxMDhhYTE4OWJkMTI1YTVhYWE0MGMyMTk3NDgxZjNlOTRhNTciLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjBmNzY3M2M5ZTc2ODA2MmJjOTM3MmNlZGQwM2JiYjJiYmQ5ODcxMjEzZWVjNTc2MDA0MjcyM2U1M2E1NTMzZTkiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjlmZTMxZjY0OWZmMjg3ZDhhMzU3ODI3MzJmNDQ3MjgyOWVjZTdkZmUyZGIyMDc1MmY3M2M0Y2JkMDBjNjczZDIifQ"
         }
       ]
     },
     {
       "id": "claim_f1b5347b6c074646bf967856b976ca8c",
-      "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.",
+      "statement": "The schema defines messages, chats, contacts, settings and image_descriptions tables plus an index on messages(chat_jid, timestamp) using CREATE IF NOT EXISTS, with no migrations.",
       "evidence": [
         {
-          "resource": "repo://src/history.rs#L340-L374",
-          "version": "repo-lines-v1:sha256:4cff43e18fae30b0a29423e81000ae1701552bd4bcf0333d49a79580fee2a62b:eyJzZWxlY3RlZExpbmVDb3VudCI6MzUsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6Ijg2MTQ5Yjg1MjZhMTc4ZjhmMzMzNTViZjU1MmMxNjJlYzdiZDRhNTQyNjk2MzhmNzlhOTkwODIzNzg1YjM5NWYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjE5ZTc2MzU1Y2YxMTk5NDgwYzNmOTk4MzZiMTQ4NmM4NDFkODkxZmFhZDkwNGM2MDhlMDkzMDIyZDM3NTdhZWUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImEzNWM0OWE4ZjYxOTQzMjJkOTYwYTAwZjQwNzA2ZDJkODAzMTE2NDRkNmZmZDhmZWJlNmZjZWIyN2EyZDhkZjYifQ"
+          "resource": "repo://src/history.rs#L397-L436",
+          "version": "repo-lines-v1:sha256:3c733d522843b98c851557c73e574046d7229aecf27eb5d391e341f2d18e015c:eyJzZWxlY3RlZExpbmVDb3VudCI6NDAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6Ijg2MTQ5Yjg1MjZhMTc4ZjhmMzMzNTViZjU1MmMxNjJlYzdiZDRhNTQyNjk2MzhmNzlhOTkwODIzNzg1YjM5NWYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjE5ZTc2MzU1Y2YxMTk5NDgwYzNmOTk4MzZiMTQ4NmM4NDFkODkxZmFhZDkwNGM2MDhlMDkzMDIyZDM3NTdhZWUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImEzNWM0OWE4ZjYxOTQzMjJkOTYwYTAwZjQwNzA2ZDJkODAzMTE2NDRkNmZmZDhmZWJlNmZjZWIyN2EyZDhkZjYifQ"
         }
       ]
     },
@@ -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#L105-L116",
+          "resource": "repo://src/history.rs#L107-L118",
           "version": "repo-lines-v1:sha256:598d40867f709e3d9ec09cccd1e752c4ba2ae078a93b814058400d12451c022f:eyJzZWxlY3RlZExpbmVDb3VudCI6MTIsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjJjY2YyOTI3M2ZhMDRlMTFmMTY4ODliZTBiYWY2ZDg1MjdjN2Y1ZGNmY2VjMGVhMmZmNmU3NmViNDA1MGVhZTkiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImYzMWE1ZDk2MDM0YzIyMWYyMWVkZTQyYTlkOWM3ODgwMjQxZTI2YzJjMzQxOWQ4Y2M4MmNiM2JiMjdjNWJhY2MiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImQxZmRlNjg0N2FkMzhlOWUyYjA1ZjdmYTRiNDUzOTY2YzE2NjRkNmJmYTExMDJmY2YyYzJiOWNjNDM5MGQ1OTUifQ"
         },
         {
-          "resource": "repo://src/history.rs#L380-L401",
+          "resource": "repo://src/history.rs#L442-L463",
           "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#L408-L421",
+          "resource": "repo://src/history.rs#L470-L483",
           "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#L427-L437",
+          "resource": "repo://src/history.rs#L489-L499",
           "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#L122-L140",
+          "resource": "repo://src/history.rs#L124-L142",
           "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#L173-L189",
+          "resource": "repo://src/history.rs#L175-L191",
           "version": "repo-lines-v1:sha256:21c241cfd2393adfe0d73994422db78368373279b5a142e4604f76c30b8092c6:eyJzZWxlY3RlZExpbmVDb3VudCI6MTcsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImViODI2ZTYxYWIwODc5YTZkZGEzYTVhZWYyNTc4NTEyZGMzNzYyMjA0ZjcyNDc5NzI3MWVhMDI1YzFlMWI2NjEiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImFlNjMxYmYzODIyNWIyNzIzNWJjOTE2NTg0YjEzNWMwZjZkNWY3NjQzYzYzZDVkMTU3NzYyODM1NjdjMTRiNTIiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjdhYWZlOThkYmNlMTM2NmMxYmU4OTdkZjk0MmE3NzFmZDI0YzQzODE1Nzc3ZDUzZTAzNjgzYmEyYTk4Zjk3N2UifQ"
         },
         {
-          "resource": "repo://src/history.rs#L468-L479",
+          "resource": "repo://src/history.rs#L530-L541",
           "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#L195-L234",
+          "resource": "repo://src/history.rs#L197-L236",
           "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#L240-L251",
+          "resource": "repo://src/history.rs#L242-L253",
           "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#L257-L283",
+          "resource": "repo://src/history.rs#L259-L285",
           "version": "repo-lines-v1:sha256:956f230dd6020d91e8b2d7d137d9fa4fa04d0a316c598d90f38729025889cd02:eyJzZWxlY3RlZExpbmVDb3VudCI6MjcsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjNkMDNkMmIyZWM0M2RjMGIxYjg3NDg1MjQyNDRmZDc3ODVlNzcwZWJkNGJmZjBhZTUxODc5MjM1ZTFjZjM4OGEiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjQ3NzVhZDM0NjMyYTRjYjNmZjQ4NjA5Zjk2NWI4OTY3NDFmNTIzYjNkODY1Y2FiODQyZTgzZTI0YTZhOGQ3M2IiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImYyZWNiYWUyM2ZlZTE0ZTViOWIzNWQ0NDcwNmFmOGQ1OWY1NWQyOWIwNzYzNjU4ZjI4YjIzNjZkNWUzNWM5Y2UifQ"
         }
       ]
@@ -115,7 +115,7 @@
       "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#L142-L166",
+          "resource": "repo://src/history.rs#L144-L168",
           "version": "repo-lines-v1:sha256:6daa209756bdfb87ba10954b88d7b52b35ee2212dc1328db974b019c47aab510:eyJzZWxlY3RlZExpbmVDb3VudCI6MjUsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImRiNWUzOWMyMzYyNTg2NWE5NGNiY2ZlMTVhMDNkNThiYTZkZDY3YTkxNWE4OGJjNzdiMjBkOTc1NTlmMjU2NGUiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjAzODg4YWU3MTJlMTVmZTE3NTYzNDgzMWQ4ZTcwYWZkYWEwMTMxZGI2ZDAxZWZkNzExZjQ1NmFkNThjZmExNjUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjhlZWMyZTE2YzJiYmJhOWFmMmQyMGY4MTcyNTJhZmQ3ZDdiYWRhZjgwYTM0YTcwYWE3N2Y0YzM5NzQxNzFhNzYifQ"
         }
       ]
@@ -125,36 +125,52 @@
       "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",
+          "resource": "repo://src/history.rs#L20-L23",
           "version": "repo-lines-v1:sha256:fb1e0c7f933e63c16b33cec6c6d4ababced3866d36d400b459bc168ff203aab4:eyJzZWxlY3RlZExpbmVDb3VudCI6NCwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiYzgyOGY3ZjM1MjgwY2ZjMDUzOGY5NmMwNmMxNzczZGIyODVhNTdjOWNhMWM2MDYyZjljYmU5MjEzMzA1Yzg3MCIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiODliZTkwZWUzOWU5M2U0N2MzOWRkMTg5NTY0NzNlYTkzZjkxY2MyZjVlNjdlM2Y1MTZlYTZlZTc2NjBjYWE3ZCIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiYjhjNjIyOGY4NDg1ZWFkNTEwZjNjYTEwZGM1YzRhOGE5Zjg5ZTU5MzY4YTJlNjEwZTUyMDY0ODlmMjMzY2MxYiIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiZWRhNzYxMjkwMjY1MmNkNzBlZmVjZWE4MWJhN2NhMjM5Y2NmZjViMjAzN2M2NjQyZDUzZjM5YjVlZDVmMTViNSJ9"
         },
         {
-          "resource": "repo://src/history.rs#L285-L315",
+          "resource": "repo://src/history.rs#L287-L317",
           "version": "repo-lines-v1:sha256:7446cd3d43ecedda818c27075602eb8b9f1b3ef669ea39edf3a6c5520cc4bec3:eyJzZWxlY3RlZExpbmVDb3VudCI6MzEsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImE2OTlhMTQ5OTg2YzgxZjQ4OGYwMjQxMmU4Zjk0YjE1NjdhNWQ2OTEzZTNiYTg0NjJlMmFmYzY5NDJkYWYxMTMiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjRjZTNjZDVlMGE4OTI3NjIwMWQ2ZWU2OWE2ZGM4ZTEyMzgxNDVmMzEyYmE2ODQxZjUzMGYzNDQ0MmU0Njc2NjIiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6Ijk4NTAwNmMxYjY0YjM5MzI5ZWZhMmUxZmU2YTEwMWRiYzg0MTIzYjkxNjkyYjM1MjhjOTg4YzhhOGVmNGRjY2QifQ"
         },
         {
-          "resource": "repo://src/history.rs#L670-L681",
+          "resource": "repo://src/history.rs#L732-L743",
           "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.",
+      "statement": "delete_chat atomically removes a chat's messages, their image descriptions 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#L369-L394",
+          "version": "repo-lines-v1:sha256:c4f4b4352468ac2099f49717f06a98ea40bab4d5c62a4158e04234c744610ee6:eyJzZWxlY3RlZExpbmVDb3VudCI6MjYsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImNkMTFhNTg0ZDdhNWU5N2U1ZmY4NmU3MzY5ZDBkMzdhNGYxZjlmZjBlYzRlMzk5OWEyZWRkYTc2MDMyNTQwMGYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjNlMjRjZDNlMGNmY2Y4OGRmMGJmNGU1YjRjYzY0MzdiODBhMjAyYjEwNzk5Y2JjNTMyNzBjY2ZlMGRjNDk5MjYiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImYyZWNiYWUyM2ZlZTE0ZTViOWIzNWQ0NDcwNmFmOGQ1OWY1NWQyOWIwNzYzNjU4ZjI4YjIzNjZkNWUzNWM5Y2UifQ"
+        }
+      ]
+    },
+    {
+      "id": "claim_ad763485a3d74d7a82a1b9d59673d77e",
+      "statement": "save_image_description upserts one description per message id (a new one replaces the old) and image_descriptions returns the descriptions of one chat keyed by message id.",
+      "evidence": [
+        {
+          "resource": "repo://src/history.rs#L334-L367",
+          "version": "repo-lines-v1:sha256:63f7062cc95232bf44796ae0a8c94de3eb0412d9afad7f881a13da1e20f1c794:eyJzZWxlY3RlZExpbmVDb3VudCI6MzQsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjMwMjkwMTRkZWU5ZjY3MjRkMDM1MGYyZGRkYWU4YWE2MjYwODI3ZjdkNTc0NTc1OTBlMzQzOTFiYTRlOTMyNWMiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjJhYWFmOTZhMzJlYWM3NDA3ZjM4NGI3MzBkM2ExYzU0NDhjMjhiMTEzNmU3NDc1NWE4YWY2ZWNiMGQwNDE5MDciLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6Ijk4NTAwNmMxYjY0YjM5MzI5ZWZhMmUxZmU2YTEwMWRiYzg0MTIzYjkxNjkyYjM1MjhjOTg4YzhhOGVmNGRjY2QifQ"
+        }
+      ]
+    },
+    {
+      "id": "claim_6f226f295465444baf3b62864b0eb9dd",
+      "statement": "message_exists tells whether a message id is stored in the messages table.",
+      "evidence": [
         {
-          "resource": "repo://src/history.rs#L695-L717",
-          "version": "repo-lines-v1:sha256:167f97ca14887b3abaed94df2233c76fafe73a7a65f58396500a10a95260ff41:eyJzZWxlY3RlZExpbmVDb3VudCI6MjMsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjhkZWE5MGY2NjhiMTNkZjZiMjNmMmYzNjhiYzYyYzY5ZWU5ZTI0YmU2MzJjMGI4MTg4MTljOGJiYTJiNTRiMWYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjAxYmE0NzE5YzgwYjZmZTkxMWIwOTFhN2MwNTEyNGI2NGVlZWNlOTY0ZTA5YzA1OGVmOGY5ODA1ZGFjYTU0NmIiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImEwMGRiMDQwZjI5MDliZjk3OGZmMGE5NTZhNTVlODNjNjUzNDE4ZGU2YmI2OGY4MDQxZGE5NzZmOGQzZDIyNzAiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6Ijk1ODAwYjU1YWE2ZWY1OGE3ZjFhZDVhZmEwNWE2MDNhZWE3YmE2ZTBmZjEwY2M5Yjc0NDMwY2I1Yzk4ZTMyZDkifQ"
+          "resource": "repo://src/history.rs#L319-L332",
+          "version": "repo-lines-v1:sha256:630d929422a0e6baf9ec9733842246510670f4cbd13a1a58e80d87fb8815c590:eyJzZWxlY3RlZExpbmVDb3VudCI6MTQsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImRmYWFhMjFhYzg3YThjYTczYzk4NDc5YjUzNTQ1MDc5M2U5MmU3MmE3ODg3ZmU5YjBmNmYwNWFiNzFmNDBlMGEiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImY5OTM0OWRjNmNiNTFlOWIwMzQxMTdiZjQxMzkzZDE5YzQzMGM3NTNmZDcxZWZmZmZjZmQzYmIzOGFhNjU0ZmYiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjlhNjc2ZmQ2MzliOTMyOTliYTFhMzBlY2QyM2NiMDQ1Yzg3OTBlMDc1OTAyNDRkNjY0ZjU0NTQ2MWIxMTc0ZDAifQ"
         }
       ]
     }
   ],
   "verification": {
     "by": "openwiki/0.7.2",
-    "at": "2026-10-20T20:27:06.515Z"
+    "at": "2026-10-21T21:49:12.291Z"
   }
 }

+ 76 - 56
openwiki/.claims/integrations/ai-client.json

@@ -1,14 +1,14 @@
 {
   "schemaVersion": 1,
-  "pageVersion": "sha256:e30d17eaaf9e0d8e88f0bc51ae0a0c586c23d7bea67344aa86f5188912c80441",
+  "pageVersion": "sha256:e71947f4ffd79153e75af279f7eade4504e8b9a729d276a1f1938a45a094f7a0",
   "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#L42-L67",
-          "version": "repo-lines-v1:sha256:2985d21aab6a595c3f5ede27b794f4b4793b24bfb2676f79788e10d0f5c1638c:eyJzZWxlY3RlZExpbmVDb3VudCI6MjYsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjA5YzYwYmU2NjI3ZGFmNGM2MzEzZThlMTdmNzExNzFjY2RlYWU5MjZiNTBlZmU1ZWIyNTljN2FhN2Q0MGUzZDUiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjJmNzFiMjQ4YTgyZjdiZjQ0YzUyZmY4ODk2NDNiZWJmNGI1YWNjZmQ4N2YzOWIzNWIxNGZiNTU2MmRkMTRlODgiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6Ijk0ODgzMTY4NjYzMDEyM2RlNmQ0ZmJlNmJiYjBiZmM5Njg1MzgzOTI3MmQ1YTNmYzFiZjViMjdiZWY3NjlkNTUifQ"
+          "resource": "repo://src/ai.rs#L60-L87",
+          "version": "repo-lines-v1:sha256:a3ba5081f556babe8c0b542175d88321a54c6a0d016283721c5f79da510b0435:eyJzZWxlY3RlZExpbmVDb3VudCI6MjgsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImRjYzU5YWNjMmRmZWM1OWI2NjE3ZDdmNmE5MTljOTNhZGU4NjU2MzU5ZGNhYTA5MjUxZDM0YzRlZmI4NTIzMjEiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjZmNDU4MTFlNWRlMTgyNWY2ZTllYTM0ZmM2NzAwNTg2YWQxNGViZDVhOTFiNmE3ODZkNTU5YTBhMmI4ZWU5NDQiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6Ijk0ODgzMTY4NjYzMDEyM2RlNmQ0ZmJlNmJiYjBiZmM5Njg1MzgzOTI3MmQ1YTNmYzFiZjViMjdiZWY3NjlkNTUifQ"
         }
       ]
     },
@@ -17,7 +17,7 @@
       "statement": "AiClient and ImageInput Debug implementations omit the API key and image bytes.",
       "evidence": [
         {
-          "resource": "repo://src/ai.rs#L102-L138",
+          "resource": "repo://src/ai.rs#L124-L160",
           "version": "repo-lines-v1:sha256:252913842faf254329b50d5fd74240a84fdd0456ec9e8e68bcc77e1d8883daa4:eyJzZWxlY3RlZExpbmVDb3VudCI6MzcsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjE2NTYxZDE4YzQxMTY4MjBjZGFiOTJmMjQwNDVhNzlmMTVlNzZmZTdiY2FiODY4Y2JkYTMyZWI1YzM0NWQ1YmMiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjM3ZDYwZjQxMWJhNTUxYTEzOGUwOWJkYjdiMDg5YjRjMGE3NDUyNTU2MzUzYmE3YTAxNGVhODA5ODEyY2M1Y2QiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjIyZmQyY2VkZGZlMDVkZDM4ZTYwZmRjYzkwNGU1OWZkZTM3Y2IxODY4Yjk3MzhlODJkNWI2ZTIxZmE3Nzg0OWYifQ"
         }
       ]
@@ -27,12 +27,12 @@
       "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#L147-L171",
-          "version": "repo-lines-v1:sha256:44a0131bcff163d3b23847cb3c2a056e5eadd8bc591340fa6c19aa263ff24757:eyJzZWxlY3RlZExpbmVDb3VudCI6MjUsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQyMDRmMzBiYmM1OTNhOGQxOGFiYTY3OWYyMWQ0ZGY1M2JiN2YxYjdlYWIwYTNkNDRmOWJhMjgyZmVhYjFhYjUiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjA2YWQyYTE2MzQ0NTcwYjlkY2QwODA0NjQ4Y2NhNzgxYTA0YmRiNWVmMTkwYTQxZGRlOWQ1N2NjYzE0ZjBhY2QiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImY0OGYzMmRjZTI0MWMwNmFjYzBlZjdjZTc4NWM4MGZjM2E5NTk3MTZlODYwNTcwYjdkZmM0MWNiYjg0MWZjNDkifQ"
+          "resource": "repo://src/ai.rs#L169-L197",
+          "version": "repo-lines-v1:sha256:f967f19779a732371d88712967219c8c951d71246e7691599693c2d86eaa50b2:eyJzZWxlY3RlZExpbmVDb3VudCI6MjksImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQyMDRmMzBiYmM1OTNhOGQxOGFiYTY3OWYyMWQ0ZGY1M2JiN2YxYjdlYWIwYTNkNDRmOWJhMjgyZmVhYjFhYjUiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjA2YWQyYTE2MzQ0NTcwYjlkY2QwODA0NjQ4Y2NhNzgxYTA0YmRiNWVmMTkwYTQxZGRlOWQ1N2NjYzE0ZjBhY2QiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImY0OGYzMmRjZTI0MWMwNmFjYzBlZjdjZTc4NWM4MGZjM2E5NTk3MTZlODYwNTcwYjdkZmM0MWNiYjg0MWZjNDkifQ"
         },
         {
-          "resource": "repo://src/ai.rs#L23-L26",
-          "version": "repo-lines-v1:sha256:387091425f02a452aec12422a8131f0728f26389419cee2af4500edf72eba0d1:eyJzZWxlY3RlZExpbmVDb3VudCI6NCwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiZTdjZDhjOGU3Yzc4MWIxNTNhYzUzOGI4MGU4YmUwYjI5OTkzZjNlMTI0ZThmYjRhMzcwOGQ5Yjk5YzkwNmRlZCIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiYzgxOGM0OTA5ZDY1ZmUwYmI0NWFmYjQyYmNlNTE4YjA1MzlkOTFhMzY2NzUyMWMwMTYxYjYxMzkwZGY4MTI2YyIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiZjAxZGVmODgwNmEwNDc3ZGQyNTdmOGM5NDlkZDViOWE3ZjA2MzZjOWQzZjkwY2VlZWQ3OTRkZjE0NjZlOWIwNyIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiYzY0NGQwYjM2ZWY3MmUxMTU1YTY5N2MwNGU4ZmZhNjNmZDU1MjQ2NTkyZWI2YTM3MTNkNjA2M2Y3Y2JjYjBmOCJ9"
+          "resource": "repo://src/ai.rs#L26-L29",
+          "version": "repo-lines-v1:sha256:387091425f02a452aec12422a8131f0728f26389419cee2af4500edf72eba0d1:eyJzZWxlY3RlZExpbmVDb3VudCI6NCwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiZTdjZDhjOGU3Yzc4MWIxNTNhYzUzOGI4MGU4YmUwYjI5OTkzZjNlMTI0ZThmYjRhMzcwOGQ5Yjk5YzkwNmRlZCIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiYzgxOGM0OTA5ZDY1ZmUwYmI0NWFmYjQyYmNlNTE4YjA1MzlkOTFhMzY2NzUyMWMwMTYxYjYxMzkwZGY4MTI2YyIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiMDg1MDNiYzliMGYzYjMwYjZmMzI3Y2ZlMzIwYTNhNDA0MzI4N2MzNDJhMmZhOTI1ZjYyM2M2ODAyYThkYTY3OSIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiYzY0NGQwYjM2ZWY3MmUxMTU1YTY5N2MwNGU4ZmZhNjNmZDU1MjQ2NTkyZWI2YTM3MTNkNjA2M2Y3Y2JjYjBmOCJ9"
         }
       ]
     },
@@ -48,43 +48,43 @@
     },
     {
       "id": "claim_f1bcd507d7bc494488698809a5e4d5af",
-      "statement": "complete sends a body with the model and role/content messages.",
+      "statement": "complete sends a body with the model, role/content messages and a tools list containing only the parameterless no_reply function.",
       "evidence": [
         {
-          "resource": "repo://src/ai.rs#L180-L182",
-          "version": "repo-lines-v1:sha256:6c710c12e7719cdb505b12f9af500f78a0569c21c8098a12c1b53b5c19d43207:eyJzZWxlY3RlZExpbmVDb3VudCI6MywiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiMGE5MGZlNDNkOTJhMDA1YTM3YjEyYmM5Nzk3MTIxNGVjNDc1NDk2MTc0MmUyNzQ0OTgwODQ0ZTVkYWFlMTNmNSIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDg5M2Y0MGUzNzgxZWI4OTZjODJkMDQ2YTA4MDY3YTExZTM1Y2RmMmYxZGZhZjI2ZmVhOTBiOGRiNjY3NjQxMyIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiMTNiOWZjMjM4M2MwMzAwYjUwMzI2NzhkMjUxNDZjYWE3ZDJmODZlOTQwMzZmZTg3ZGI2NTU2ZjZlZTk2MzVjZSIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiM2JlODA2YWQyMDg1ZmYxMDNjZGJjODM1ODRkYjM4MGRmNDU5N2YwNmQ0YmY5NDk0Nzc0ZDVmZGVlODliZjBiYyJ9"
+          "resource": "repo://src/ai.rs#L199-L208",
+          "version": "repo-lines-v1:sha256:84db9a28e9fbaa3500a8750fd4e0562b0ca94d27cb5b716c579ef27b6d1ab945:eyJzZWxlY3RlZExpbmVDb3VudCI6MTAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjRjOTFlMDQ2M2RhZDJlNGQ5NTk1YmYzNDkzMTdiZjFkNDRkY2ExMzI4NGNkZTEwMDIwNmJjYTFiNDFlYjBhNmEiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImViOWEwOGJjMjlmNmViZWJlOWE1ZjAxOTAzZDk4NDVkYmNiNTg0MmE0NTVmMDdkNWY0MDAyZGFjMzJhNWM2MDUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjNiZTgwNmFkMjA4NWZmMTAzY2RiYzgzNTg0ZGIzODBkZjQ1OTdmMDZkNGJmOTQ5NDc3NGQ1ZmRlZTg5YmYwYmMifQ"
         },
         {
-          "resource": "repo://src/ai.rs#L325-L346",
-          "version": "repo-lines-v1:sha256:fc6890863f2d103b4db6f7460506060b7e629e95f79e42e6bca5d92ecf262f51:eyJzZWxlY3RlZExpbmVDb3VudCI6MjIsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjdiZjlhNjk0Mjc3MDY1MDI5ZThmZDIwMjYwYzBmMzZjZDNjMDYzYzQ5Yjg3NTU2YzI1NzdjZDZiZmZjODM3NDEiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6ImQ4MGYwYWFiOGM0NDViYTQ5ZGYxNmVjMDY4ZDk0ODgwYjJjZGJhYzc1MDE3MWU3YzgyMzg4MzMzNTExM2M3YTYiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImMwNDdhZDE2MzZiMWQ2OTc3ODM5NDBjNDMxZmIzN2IwZjFhMWRjNTNlMzc2YzI1YWZkZGUxMGJlYjI3ZGE1MjAiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjczNTE1ZjNiMTM5NmQwNmQxMDZlZmM0ZWM5ZDdjYjU0MjVlMWJjMzU3Y2I2OGJhM2U4NTBkMDVmOWY2OTRjNzEifQ"
+          "resource": "repo://src/ai.rs#L343-L379",
+          "version": "repo-lines-v1:sha256:dda6d7007908d587c35f4714ff74812e0a09bc602f912758f08b2a1b5d39175f:eyJzZWxlY3RlZExpbmVDb3VudCI6MzcsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjM5OGNjOWNlYzY5ZDJiZmQzNThlZTEyMzYxYWNmNWI4Nzc5NjI1NjUzMDRlNDcwOWJjMmUxMDE3ODU5YTJjZDQiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjAxYmE0NzE5YzgwYjZmZTkxMWIwOTFhN2MwNTEyNGI2NGVlZWNlOTY0ZTA5YzA1OGVmOGY5ODA1ZGFjYTU0NmIiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImU0OWVhZTIzMTIxNDdhOTMxMjNjMGE4ZjJlZTUyYmVjNTgzYTRhMzc5NjQ5MGQ4NmYxMWE3OWIzNWE2ZWViY2EiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6Ijc1NDJjMzEwZjQwN2NjOGUxZjNiNzg4MDQzOTM4YTBmNzdkYWVkNDJmZDI4NTI3YTU2ZjRmMzUxNDY5OWE0ZjYifQ"
         }
       ]
     },
     {
       "id": "claim_eca836c7fc554ba9b5a1e0bbfe970f9d",
-      "statement": "complete_with_image requires a vision model and validates that the image is non-empty and at most MAX_IMAGE_BYTES (5 MiB).",
+      "statement": "describe_image requires a vision model and validates that the image is non-empty and at most MAX_IMAGE_BYTES (5 MiB) before any request.",
       "evidence": [
         {
           "resource": "repo://src/ai.rs#L18-L20",
-          "version": "repo-lines-v1:sha256:cddeca6fba6fde7769f6cdec95f08d62b34b93c4f1a6822927f4b106a7db0a07:eyJzZWxlY3RlZExpbmVDb3VudCI6MywiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiNTkyZDVjNzYwZTNiYmRlNWQ3MDRlNjE4YWY1ZmVmZTY5YWMzODEyMjg0MGU3NTUwYjk1N2RlYTZkMjFiY2YxZiIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiMGJmZTUyOGM4OWM4NTc2YTQxMjYyZDU4NTdkMjE4ZDFjYWIzYTFhYzY4MzVlMGVjMDI4ODI2ZDk5YWVkZWVhNiIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiNDdiNTMxMDc0OTNkYTIyZTIzZmQyYTlmZTVjY2EwYjFhOGQzYjM2MmI2YzQ1YjkyMjZiZDM1OWE5MzgxODFiMyIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiMDM4NDJhNGNmOTA1YmJmOWY2NmFkNTY5ODljYjZiYmFhNjNhMzQ0YmIwMjUwNjY4YmU2ODVhNjZlMGRjYjgwZiJ9"
+          "version": "repo-lines-v1:sha256:cddeca6fba6fde7769f6cdec95f08d62b34b93c4f1a6822927f4b106a7db0a07:eyJzZWxlY3RlZExpbmVDb3VudCI6MywiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiNTkyZDVjNzYwZTNiYmRlNWQ3MDRlNjE4YWY1ZmVmZTY5YWMzODEyMjg0MGU3NTUwYjk1N2RlYTZkMjFiY2YxZiIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiMGJmZTUyOGM4OWM4NTc2YTQxMjYyZDU4NTdkMjE4ZDFjYWIzYTFhYzY4MzVlMGVjMDI4ODI2ZDk5YWVkZWVhNiIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiNDdiNTMxMDc0OTNkYTIyZTIzZmQyYTlmZTVjY2EwYjFhOGQzYjM2MmI2YzQ1YjkyMjZiZDM1OWE5MzgxODFiMyIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiYWMzYWNhOGE0NDIxMDNkMDMwNGMzMjYzZmIzMzQxNzBkMjVmN2M3MmZjZjg2ZWZkNGIwZTE2MDY2NDBiYjk2NyJ9"
         },
         {
-          "resource": "repo://src/ai.rs#L200-L213",
-          "version": "repo-lines-v1:sha256:09ee9e9276c0c6cfe14563db8840a913e8c6c327a0394f7b9be32282dfd32da6:eyJzZWxlY3RlZExpbmVDb3VudCI6MTQsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjFmNmNhMjIzNTg1NmI2MmM3NmZjMDk5ZDk5ZGFiOWFhMzRiY2I2MjM3ZTY0NjkxOTE2OTM4ODVhYTA2MzMxZjciLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjAwNTU5ZGE4YzBkNGQ0Y2ZkMjFkMzZhZGFlMWFmMjNlYWM2Mjk5OGU1Mjg5NTJiMDViNTcxY2JkNTZiOTVhODQiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjA1ZWY0YzRmMGNjMDcxMzBjOTc1ZWVhM2E3MzdkMGJmOGNjZjY3MThiODUwMDY5NWY0NTU2Y2E2OTk5NmQ0YTcifQ"
+          "resource": "repo://src/ai.rs#L218-L236",
+          "version": "repo-lines-v1:sha256:3b94ba30b7e5cb2d330d42ad007c48b0603bfef4b59fed9f017691dc5588825f:eyJzZWxlY3RlZExpbmVDb3VudCI6MTksImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjUzNTRlMzRjMWYwMTgyN2YyMzk2ZTdlZTc3MWZkOTQ4MDhkOGFiNjlkMzIyOGU3NzQyNWI4MDIyMzM1NWRiYjUiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjRiZTViMmQ5MTI2YWViZGRjY2YxOWJlYmFiZTFmNjRkODYzZmQzODA0MWMyY2FkYjk3MzM3ZWFiYmZiNmUzOGYiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjA1ZWY0YzRmMGNjMDcxMzBjOTc1ZWVhM2E3MzdkMGJmOGNjZjY3MThiODUwMDY5NWY0NTU2Y2E2OTk5NmQ0YTcifQ"
         },
         {
-          "resource": "repo://src/ai.rs#L385-L399",
-          "version": "repo-lines-v1:sha256:68a089645a7a69cf4f7f652087e4d1f3a7c37fbcdaa506a831122eef48a12189:eyJzZWxlY3RlZExpbmVDb3VudCI6MTUsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImY5YzNhMGViNjE4M2YxNzFmMmE4ZTQxY2JlNWFmMjc0YjdhNjk1ZmM5Zjk3YTAwZDUzZGQ4MmVmYmJiMDE2ZWEiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjRiNGU1YzcxMTNkZTFlMWIwOTIyYjEyYTlhM2ZhMGJjYTVmNzI5YzQ3OGM5NzA5ZDczZWYyMmUyYWM0YTEyZDQiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImY1NjQyY2ZlYzYwMmNlYzJhOWUyZDk4ODc3OWUzNjIwZjkzZDU2NWRjZDA1MjM1NWFjZmFlOTkyMjAwODRmYjEiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjhjYzlkYTNhZDRjZjZmY2RiMDkwNTE4MmY3ODgyMzIxZTE0MGU5ZWZiYTJmNjBhMjJjODdiY2YwYzk0ZWVkNTAifQ"
+          "resource": "repo://src/ai.rs#L408-L427",
+          "version": "repo-lines-v1:sha256:092f50c0711f3d5bc0d27aec19da222f84f626a7d3a736ad917fcc84a58b6d9f:eyJzZWxlY3RlZExpbmVDb3VudCI6MjAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjRmNjM1OTllNGU1ZWMwYTJmMzlmYWEzN2RlYTJlODQwZWIzYjQ2NmJmMGJiNDBlODEzYjdlMTMzYTI0ZGVjZmUiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6ImUyYjlmYWRmNDM1MTMxNWMwYmMwMTE2YTQzYjNkYmQ5OGE0NWE5YjI0NjExYTg4Y2Q2ODRjZTMyNzlmOGE0N2EiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjYwNjNjNGQxYWY4Y2Q4OWJiMGJkZDc2NTdmZmNiZjY4MDZhNjEwZDc5ZDZjOGQ0YzMwN2MyNDFkZjM5MDA1MjIiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImJiZTExYTZmMDQxMTdlZjQ2MDZhNTZkNmMxNWZjNjE5N2Q4ODg5ODY1MzcxNTc1ZTk0MDg0ODg4YjBhZTEzOWYifQ"
         }
       ]
     },
     {
       "id": "claim_23440e7b2c704bdd90c1385f5bcb4ecd",
-      "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.",
+      "statement": "The image description body is a single user message holding the description request text and the image as a base64 data URL, sent to the vision model without tools.",
       "evidence": [
         {
-          "resource": "repo://src/ai.rs#L348-L378",
-          "version": "repo-lines-v1:sha256:0421b4eafacfa0da02301fb95fc4b94d4cd4b0a9d61791112860348f252e7675:eyJzZWxlY3RlZExpbmVDb3VudCI6MzEsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjdlZTMyOWRjZTI2MzIyY2FkMmI0N2E0NGU0MjY1YmEwNzM3YjM1NDJjMjM1MzUwZmQyMWZlOGI0MTNjM2M4ODUiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImRjNWUzZjQ5YTQ0MjE1MjlmZmI4NTMyNzkzY2ZlZTllNjlkMzFhNWZhMWY4YzA1ZGJlMTcwZGI0MTRlMGQxYjMiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjJjMWU2Yzg2MTE1NTc0YWZlM2FhMjAwNDYxYzRkMmUzZmRiNzI5ZjFmYjMyMjQzN2VmNGJhNWZmYzJmMDkxZGIifQ"
+          "resource": "repo://src/ai.rs#L381-L406",
+          "version": "repo-lines-v1:sha256:504cd9a8e9a12796644c5e64bf2dfaf3e052ae4cdc48d88acb91a8674cd4d994:eyJzZWxlY3RlZExpbmVDb3VudCI6MjYsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjkxMWU4YjYyZTMzYmM0ODQxYzg2YTUyYmJmZDQyNGFlNDFlMGJiZWE4MjQyMmQxMGUwZDIwYjlmMzA1ZmI0MGMiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImY4ZTUxYTI1NDgwNDkyN2MwNDc3OTFjYjkyNmUxY2MxNTUwMzY3ZjhlYTA3NWFjNzk0MDc5NGIzOTAwZmUxNGMiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjJjMWU2Yzg2MTE1NTc0YWZlM2FhMjAwNDYxYzRkMmUzZmRiNzI5ZjFmYjMyMjQzN2VmNGJhNWZmYzJmMDkxZGIifQ"
         }
       ]
     },
@@ -93,26 +93,26 @@
       "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#L265-L291",
-          "version": "repo-lines-v1:sha256:b80ccebb98e52d79a2155c3e75a8de42dc9ab06286c6639216c4094f37a90562:eyJzZWxlY3RlZExpbmVDb3VudCI6MjcsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjFmYWQyYmNjYzU1ZWJiNGE5OTJlNjQ1YmE4ZDBiNDc3MWFjNDk0YTcwMDhlMDUxOTk5MmMxOWQ4ZjQwYTFmYjEiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjUzNGRhMTA2NDE1N2QyNzIyZWNjYzIzNTUzNTlhZDIyN2IwYjQwNDU5YWRkMGM0YWRhODZkMTNhZDZlODJjZTUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImI2YzllMjYwYjQyZThmYmJiMDRiOWZiM2UwOWEzNmMyZjI1OWI1MDBhOTM1OWE0OTYwMTA2NDYxNWIxY2Q5YjMifQ"
+          "resource": "repo://src/ai.rs#L105-L122",
+          "version": "repo-lines-v1:sha256:1e8751e4147e04846b7aafee348637638f2fc85f2f11b79537cc0246feaccc44:eyJzZWxlY3RlZExpbmVDb3VudCI6MTgsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImVmOTlmZDhkOTIwYjk2MTFiZmI3MDkxZTJjZTU3ODM3ZTY5MzY2NmVkNjRjMzFmNmIzMDY5MjJhYjVjOTBkMjUiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjM0ZGQyM2ZhMzVjMjczYjgwYzEwZGViNzUwYjNmM2M5NDFhNmJlMDg1NDBmNWFlNGQ2ODdkNjQyZmQ0NTQxNjQiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImJjMDk2M2MxZWY1YzRiMjZkZDNmNGY2MDhlZGRhZmNhMDRkMDVhNjI1ODk1YTcxZTYzMmY2N2Y4OTQxYjc2M2IiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjY3Y2VkMGUzMjc4NjE2MjcwZjFiMDY1NjRlYjlhMDUyYTczNTc0ZWJlYjgxODg0ZDM0YjIyZTFlMjBiMDM0ZTYifQ"
         },
         {
-          "resource": "repo://src/ai.rs#L522-L524",
-          "version": "repo-lines-v1:sha256:81326ce3f8703f9c90b0068b202df13efb9689c0fe8ee236b5f77cbaa6edc4d4:eyJzZWxlY3RlZExpbmVDb3VudCI6MywiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiZGYzN2E0ZTlhMDhhNjc2ZjRjMTg3MTA1ZTk0OTQ2YWM4MjhiZWM4ZTZhMjlmYWMxYTk5NDk2ZDBiNjYwYWIyZCIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDEyY2EzNDVjY2Y3NWJmOWMwODA2YmNlNjk1YmU4ZGU4MDhiNzk5ODQyNTFhN2E1NGQyMDJjZjYxMDFkZDQ1MSIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiN2UyNTA4ZjI2MjVhMWRhY2EwMjIyZWMzYTdmYzNkMWQ4MzZjODdlZjQyODVhYTlmYzZiZmQ0ZTAwN2MzOGY4NCIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiN2JjOThkNmIwZGM0OTJjNTAxNmRmMTA4MDVhM2E1NGViOWMxODRhNWE2MTM3ZTdmNGJlY2ZhMGMyMjA1NTBkMCJ9"
+          "resource": "repo://src/ai.rs#L282-L314",
+          "version": "repo-lines-v1:sha256:580ac5298eb243d788d0a8c2ab3c5d4b293ef2288d101b6f46bb06680a3bdbab:eyJzZWxlY3RlZExpbmVDb3VudCI6MzMsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6Ijc1MTkyMjFkZDdiYWFiZGNmMTIyYmJmNTU5ZGM5MGNiOWJmY2E2NTcxOGU4NTk1MDIyMGEzOGE1MTk0ODgwZTYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImEyYWMyZWMxODc1NDdmYTFiNzk2MjZiMGE0NjUyN2IwZDc2N2U0YzE0MzAyMTQyYjQ0NjdiMzY4ZDA2YjcyMTgiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImI2YzllMjYwYjQyZThmYmJiMDRiOWZiM2UwOWEzNmMyZjI1OWI1MDBhOTM1OWE0OTYwMTA2NDYxNWIxY2Q5YjMifQ"
         },
         {
-          "resource": "repo://src/ai.rs#L83-L98",
-          "version": "repo-lines-v1:sha256:7772287888f472e0806adf8646ac05d7d32a9518c29e32a7359cf3194b2230c7:eyJzZWxlY3RlZExpbmVDb3VudCI6MTYsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImVmOTlmZDhkOTIwYjk2MTFiZmI3MDkxZTJjZTU3ODM3ZTY5MzY2NmVkNjRjMzFmNmIzMDY5MjJhYjVjOTBkMjUiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImJjMDk2M2MxZWY1YzRiMjZkZDNmNGY2MDhlZGRhZmNhMDRkMDVhNjI1ODk1YTcxZTYzMmY2N2Y4OTQxYjc2M2IiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjM3ZDYwZjQxMWJhNTUxYTEzOGUwOWJkYjdiMDg5YjRjMGE3NDUyNTU2MzUzYmE3YTAxNGVhODA5ODEyY2M1Y2QifQ"
+          "resource": "repo://src/ai.rs#L568-L574",
+          "version": "repo-lines-v1:sha256:366599ee31cc7c09c20fa0ddcff49b42b55831d6b97ede5ec5da7376b9aaa450:eyJzZWxlY3RlZExpbmVDb3VudCI6NywiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiOTgxODJmYmI3OGRjMzUzZjJkNjM2ZTc2MjU0YzYzNjIxZmQ2N2IyZjFlNzVmNTM0MzE0ZjFmYmI2N2IwYjJkZSIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDEyY2EzNDVjY2Y3NWJmOWMwODA2YmNlNjk1YmU4ZGU4MDhiNzk5ODQyNTFhN2E1NGQyMDJjZjYxMDFkZDQ1MSIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiODI0MTY5N2IzMjdjNjYxODJlYTJhODdjOGJmZDI1Nzk5Y2I5MTk3OGY0MTBhMmI0NjA4ZTM2YjZiZDc4YjUzYiIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiN2JjOThkNmIwZGM0OTJjNTAxNmRmMTA4MDVhM2E1NGViOWMxODRhNWE2MTM3ZTdmNGJlY2ZhMGMyMjA1NTBkMCJ9"
         }
       ]
     },
     {
       "id": "claim_1acd5c55f8234d20bf935285166fe3b1",
-      "statement": "parse_reply returns the trimmed first choice content and fails on missing choices or empty content.",
+      "statement": "parse_reply returns Reply::NoReply when the first choice calls the no_reply tool (even with text), otherwise Reply::Text with the trimmed first choice content, and fails on missing choices or empty content.",
       "evidence": [
         {
-          "resource": "repo://src/ai.rs#L405-L422",
-          "version": "repo-lines-v1:sha256:b4141c4af9d4b72b638ca3901c24c019a87de5cc0149465761d63c1c2a7b30c0:eyJzZWxlY3RlZExpbmVDb3VudCI6MTgsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjlhNGJhYzcxZjEwYTkwZWNmMzZkMzM5ZjJjZGZkZmY3OThmNTk4NDE4ODc0NWM3M2IzYTkzMDVmMTE0YjI4ZDAiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImIxNDJlMWMwZDkyM2E5YWY0YzczOGRhZThhMjkwMjg1YzRmYmNkMmY5YmQ1OGY1NGMzMTk1M2NhMTliZDE4ZTQiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjJjZTgwZDQ1MzU3ZWY2NWZiNjUzODgzZWEzOTU1NDMyMDMzN2Q0YjA3MzcxNzQ1ZWE0MGFkYzRmMGFjNTU0YTcifQ"
+          "resource": "repo://src/ai.rs#L429-L477",
+          "version": "repo-lines-v1:sha256:b50cbfe36f47a1bcbd306810d81baa272a3fb57a711164de89a741f5818b12ca:eyJzZWxlY3RlZExpbmVDb3VudCI6NDksImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImRjZjNhM2E5Zjk2MTJjZGJmMDZjMjU0YTU2NGY4NTBlOGFiZTEyYTNjMWNmZWRmNWYxMjJiN2IzMTlmZDdmYjUiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6Ijk2MjY0OGRjYWQyYTIyOTY3Y2YzMGUxZDRhMGE2NDA0OGQyZmE4NGY0ODNmNTEyMjU4NjZmODIyMmJmMTVlZWIiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImU0MDRmMmRiY2YyZGQyYjZhMThkYWRkYjg3MTg4Y2E2NGQyMjZhOWZmMTJkMGEzNTNjZGIzMzM3YTcyY2NkODAiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImIyMWNhYWEyZTk5ZTU2MTA0ZWU2MjkxM2JhMTY3ZTc0MmJkYzBmZDg0YmI3ZDhiODg5NzQyMWI2ZTExNzUzYWUifQ"
         }
       ]
     },
@@ -121,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#L238-L257",
-          "version": "repo-lines-v1:sha256:ba98f0d5c85a04762a0b5b0f52b0312a6fe7010f6894234828e6040933fd49bd:eyJzZWxlY3RlZExpbmVDb3VudCI6MjAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjUxNzY0ZmUzOGM2MTVjODM3MzM2M2ZhM2U2OTVhNTQ3Y2M0MGM5MjhmZjIwYjE0ZTI3NGRjZjQxNDI5NjI5MjQiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjZiMjBlNzhiOGRlMmI1ZDBlYzRlNDgwN2I0ZTNhODFmYzIyYjczMTQ4Mzg2ZWVmNzM1ZjZjYzFlMTIyYjRkZWMiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjJmNjljNjZiNTk4NDRmMjg4YzY3Yzg4NWY2NzQwNmEwNmExNjg4OWI4ZTc4NDMzNDZiMWUzNmRkNjRlYWIzNTUifQ"
+          "resource": "repo://src/ai.rs#L261-L280",
+          "version": "repo-lines-v1:sha256:80e196adb6578d005aa9692b9e78d664b43c74a606ded3b99a348545c412b065:eyJzZWxlY3RlZExpbmVDb3VudCI6MjAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjUxNzY0ZmUzOGM2MTVjODM3MzM2M2ZhM2U2OTVhNTQ3Y2M0MGM5MjhmZjIwYjE0ZTI3NGRjZjQxNDI5NjI5MjQiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6Ijk5OWJkZWZlZWFjYTYzNDBlMDI5MGRjODZlODg5Yzg5ZDVlMTg5YWE1YmE0NGI1ODViM2I3MDczNGY0YTYyZDgiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjEyYTMyMmRjMWM3OTk2OTEzNTU1MGI0YzRkMzYyMThiNTQ4ZDY2NjE4OWRmNDFkMWM1ODIyYjU1MmE3OTljNjMifQ"
         },
         {
-          "resource": "repo://src/ai.rs#L473-L516",
-          "version": "repo-lines-v1:sha256:708d606edee28a507ed556f78b94a9c7da74e9c069e02f390412464b7b2ea30e:eyJzZWxlY3RlZExpbmVDb3VudCI6NDQsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjkxYjRlNWM3NzBmZmRlY2QyZDNlNDczNWUxNjFlOGZiMTVmYmM5NTFkZWMyNmI4YmY2ZWQ3MzdjMmVhZmE4MDciLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImZkMjAxZWU4NzY1ZDNhMzAxMjY5NDkzNmVkZjFmNWQ2NzlhMzAyN2ExYTgwMzg0MjJmYTkzZTZmYzllN2RkZWMiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjJjZTgwZDQ1MzU3ZWY2NWZiNjUzODgzZWEzOTU1NDMyMDMzN2Q0YjA3MzcxNzQ1ZWE0MGFkYzRmMGFjNTU0YTcifQ"
+          "resource": "repo://src/ai.rs#L521-L566",
+          "version": "repo-lines-v1:sha256:f5678d32b8e747a88110624d96ba85e6f0b191d2ff311ce41bd841dd4cd0a2a9:eyJzZWxlY3RlZExpbmVDb3VudCI6NDYsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjAxYmE0NzE5YzgwYjZmZTkxMWIwOTFhN2MwNTEyNGI2NGVlZWNlOTY0ZTA5YzA1OGVmOGY5ODA1ZGFjYTU0NmIiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjVhMDJhNmJkMDVjZjBmNWY4NmNlMjk3MTg5ZDk1MjJlYzU4ZWZhMzY1ZjI4NGNjNGYyOWE2NTk2NmQxODhiZWQiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjJjZTgwZDQ1MzU3ZWY2NWZiNjUzODgzZWEzOTU1NDMyMDMzN2Q0YjA3MzcxNzQ1ZWE0MGFkYzRmMGFjNTU0YTcifQ"
         },
         {
-          "resource": "repo://src/config.rs#L314-L320",
+          "resource": "repo://src/config.rs#L333-L339",
           "version": "repo-lines-v1:sha256:77a53fa11fa04d405caba6426205b2c57c25edd1b59ce057d5429c3cb0af854d:eyJzZWxlY3RlZExpbmVDb3VudCI6NywiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiNzUyM2JkYmRjYjY1ZGIyYThmNWU0OGI0ZmUyMDdjOTUxYjdjNzE3NDYxYzgwNjUzODMxNjExZGI0YjMxZjdhZCIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDg5M2Y0MGUzNzgxZWI4OTZjODJkMDQ2YTA4MDY3YTExZTM1Y2RmMmYxZGZhZjI2ZmVhOTBiOGRiNjY3NjQxMyIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiNDRhMWY2MWFmZTk3ZWM0YWZjODI1OWM4ZGI4MDljZjlmMjkxNWQ3ZWUwYWRlZWJhNzdkZDMxZDBjZmJkM2JhZSIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiYTUyMTYwNGIzNWZjNDBhZjAwY2JhNGVlYTJmZDVlMmMzYWYxODYzYjM5MTE4NWQ3YmY0YmIwOWU3MmQwYzhiMyJ9"
         }
       ]
@@ -139,22 +139,18 @@
       "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#L424-L471",
-          "version": "repo-lines-v1:sha256:e830f325d8c43a24ff3d984cdf42ad06554b5a92cf56bc4bf6c77a580822e60c:eyJzZWxlY3RlZExpbmVDb3VudCI6NDgsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjNhZmRkOGE1NjBlNzQ4ZmFmNGRlZDhhYjJmOWU4NmUyMTU5NDA1YTAxMGUwYzE4YjI2M2NhNDE4MzAxMzFjZTciLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjcwZmRhNDBjNDBhZTM2Yzk5ODVhZWRjMDE5MDM3NDJmMTViYmY5ZDY4N2IxZjk1MTVmZDkwOWU2ZGUzY2E2ZTkiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjFmYjc4NDZkODI2ZmYyZmU0ZGExODEzZjRmODQxM2ViOWEyMWQwZjZlNWUwOTFkMjFhMDI1OWI3NzhiOGIyNWMifQ"
-        },
-        {
-          "resource": "repo://src/ai.rs#L473-L495",
-          "version": "repo-lines-v1:sha256:e7220f3e055e3aba45c898158efa96ce9e802fc18cbcac7911028d984018c8ad:eyJzZWxlY3RlZExpbmVDb3VudCI6MjMsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjkxYjRlNWM3NzBmZmRlY2QyZDNlNDczNWUxNjFlOGZiMTVmYmM5NTFkZWMyNmI4YmY2ZWQ3MzdjMmVhZmE4MDciLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImZkMjAxZWU4NzY1ZDNhMzAxMjY5NDkzNmVkZjFmNWQ2NzlhMzAyN2ExYTgwMzg0MjJmYTkzZTZmYzllN2RkZWMiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjNhNTdiMWEyNDNlMjM2MjAyZjY2OGUyYmMwY2EyMTMwNDJmOTgwYzM4NGY4ZmMwZWRlNWZlN2ZiYzNkOTEyY2UifQ"
+          "resource": "repo://src/ai.rs#L473-L547",
+          "version": "repo-lines-v1:sha256:abb31f22842d8a444e09f5ec22b68f5ff8465a79ef1489ef4b124d21b18bcd0e:eyJzZWxlY3RlZExpbmVDb3VudCI6NzUsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjNhZmRkOGE1NjBlNzQ4ZmFmNGRlZDhhYjJmOWU4NmUyMTU5NDA1YTAxMGUwYzE4YjI2M2NhNDE4MzAxMzFjZTciLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjBhNjlhODZmMTdiMzAyZWQ5MDgwZTZhMGNjNzY1ZTgxMmFiZmJmYzBmYmZjODNmY2Q5MTA5ZDQ0MWQ3MTExZTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImRkNmI1NWM2ODQyOGJkOWU4ZDZiNGU0Y2YwYmJmYmI4Mjc4MWEzMjU4NjEwYWU5ZTAxMjI3NzA0NjM1MGQwN2IiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImVkMjc4N2RhNGI5YWFiNTIyZDRmZWQwODQ1MjY2YzFmMzk5ZWEwM2E4NTdjOTE5Njk2Y2MzNjk2Y2I1NjVmNGQifQ"
         }
       ]
     },
     {
       "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.",
+      "statement": "Requests 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"
+          "resource": "repo://src/ai.rs#L238-L259",
+          "version": "repo-lines-v1:sha256:c006e232dbf853be3e7d3072eeee16260cafa0266b3e5044835b674eb20f8601:eyJzZWxlY3RlZExpbmVDb3VudCI6MjIsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjYwNzBmYTg1ZjNiN2RjOGYwM2QwYjBmMjA2NWNhYzQ5NWYxNWQyNzQwMWExN2I4YzE2Y2Q4ZmQ5YWIwMWQ0MWQiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6Ijk5OWJkZWZlZWFjYTYzNDBlMDI5MGRjODZlODg5Yzg5ZDVlMTg5YWE1YmE0NGI1ODViM2I3MDczNGY0YTYyZDgiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjExYWIwMTYzYTk2YjkzZWI0ODA1NmUxM2Y4ODUzMTJmOGVhNDQ5NjcwYmY3MWU2ZTRiZTAxOTU0ODg1OTk2YjEifQ"
         }
       ]
     },
@@ -163,12 +159,8 @@
       "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",
+          "resource": "repo://src/ai.rs#L317-L332",
           "version": "repo-lines-v1:sha256:29bbd06d8acee1a3e8788f3a5d669981da5c3ff1da3d4ad7b5a66e0cfcd3a69a:eyJzZWxlY3RlZExpbmVDb3VudCI6MTYsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImQxOGVkNTcxN2U3MjlmODcxM2RkZjJiYWE4ZGI1ZGZlZDgzNWYyY2JhZDI3ZGEwNTUwMzgwMWNiNmZlNTAwNGYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjE5ZTc2MzU1Y2YxMTk5NDgwYzNmOTk4MzZiMTQ4NmM4NDFkODkxZmFhZDkwNGM2MDhlMDkzMDIyZDM3NTdhZWUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjVjZGFjZmMzNDYzZjY1NDljYjAzNTQ0MzdiNjg3N2YzYmNlZjVhNWQ4NDk2YmMwYjFhYzc0YTE5OWUyY2Q0YTEifQ"
-        },
-        {
-          "resource": "repo://src/ai.rs#L815-L827",
-          "version": "repo-lines-v1:sha256:ca506079bde5869a2030b03b404d24fe4f1aff152bd99d9f352fb55bbbd97ef9:eyJzZWxlY3RlZExpbmVDb3VudCI6MTMsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjBiYjUyNzU4NmNlYTg4OTNjNTY4ZjQzNDcxMTYwZjc1ZmY1YjAwMmJlYmFmYzExYmYyYzAxNDYwZDIzZDM4NTYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjAxYmE0NzE5YzgwYjZmZTkxMWIwOTFhN2MwNTEyNGI2NGVlZWNlOTY0ZTA5YzA1OGVmOGY5ODA1ZGFjYTU0NmIiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImEwMGRiMDQwZjI5MDliZjk3OGZmMGE5NTZhNTVlODNjNjUzNDE4ZGU2YmI2OGY4MDQxZGE5NzZmOGQzZDIyNzAiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImFhNTVkYzhiMzVlYzYwZGNmZDI4Mjc4NzJlMmM4MjAyNmFhN2I3NGZmMjMyMzUxZmQ4MGVmZTgyZjhkNjUzZjUifQ"
         }
       ]
     },
@@ -177,12 +169,12 @@
       "statement": "The wait before retry number n is 2 seconds times n (linear backoff).",
       "evidence": [
         {
-          "resource": "repo://src/ai.rs#L311-L318",
+          "resource": "repo://src/ai.rs#L334-L341",
           "version": "repo-lines-v1:sha256:97d05ac13a6dbe53449bb68ab41111ae5cd01fd47dfe333a12720fe3807b20ca:eyJzZWxlY3RlZExpbmVDb3VudCI6OCwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiOTIwYTE1YWFhMWYxOTliZDY5OWE4ZDYwMmUxZjBmZTI4Y2EzZjY5MDBiYzdkMzRlNTA5ZjA3MDQ5ZjRkN2FiNCIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDEyY2EzNDVjY2Y3NWJmOWMwODA2YmNlNjk1YmU4ZGU4MDhiNzk5ODQyNTFhN2E1NGQyMDJjZjYxMDFkZDQ1MSIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiMTllNzYzNTVjZjExOTk0ODBjM2Y5OTgzNmIxNDg2Yzg0MWQ4OTFmYWFkOTA0YzYwOGUwOTMwMjJkMzc1N2FlZSIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiMmIxNGY0OGEzYjA0NzBiZWJhYWYzOTkyZGUzMWVjOTIzNzQ2OGI2NjhiZGYyOTE3NzgzODdiZGZmNmVmZWM3ZiJ9"
         },
         {
-          "resource": "repo://src/ai.rs#L38-L38",
-          "version": "repo-lines-v1:sha256:c0abec049dcec979dadad10a8d96775d266ee7888872117d2eee460f37308a08:eyJzZWxlY3RlZExpbmVDb3VudCI6MSwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiYzBhYmVjMDQ5ZGNlYzk3OWRhZGFkMTBhOGQ5Njc3NWQyNjZlZTc4ODg4NzIxMTdkMmVlZTQ2MGYzNzMwOGEwOCIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiYzBhYmVjMDQ5ZGNlYzk3OWRhZGFkMTBhOGQ5Njc3NWQyNjZlZTc4ODg4NzIxMTdkMmVlZTQ2MGYzNzMwOGEwOCIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiYjhhZjk3MTA0ZmI2ZjRhYjViOTFiZmQ5YWZhMzNkOTYxOTkwMGU1NWFmYmUwZmRkY2U5ZTFhYjNhYmQwOWNhYyIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiMmY3MWIyNDhhODJmN2JmNDRjNTJmZjg4OTY0M2JlYmY0YjVhY2NmZDg3ZjM5YjM1YjE0ZmI1NTYyZGQxNGU4OCJ9"
+          "resource": "repo://src/ai.rs#L40-L41",
+          "version": "repo-lines-v1:sha256:a6cd14dcc37ca5d183c435aa127ad8dbeef21602bc807930aa189eca98564927:eyJzZWxlY3RlZExpbmVDb3VudCI6MiwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiZGQ1MjU3MzRjZmE3YjA2ZDZmMWRhMDZkZTcwNWQ2MWVhOWU0NzhmNTBlOWVmOTgxYzZkMDUwNTVmMjMzMmY2YSIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiYzBhYmVjMDQ5ZGNlYzk3OWRhZGFkMTBhOGQ5Njc3NWQyNjZlZTc4ODg4NzIxMTdkMmVlZTQ2MGYzNzMwOGEwOCIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiM2JkNzgyOGI3NTc2YTllMzY4ZGZlOTE4Mjc0NGQ5OTVmNGVmNDZmMzkwZDcyYmMxZDNlZjVlMjRlMjY0Y2QwMyIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiMDkxNDgzNWMyYmIwMWE3ZmI2NzE5NTA4ZDFlZTdjMzU1MzJlYTk1MWI4MDE4NDgyNDY1NTRiMzg3ZDI0ZTY2NiJ9"
         }
       ]
     },
@@ -191,14 +183,42 @@
       "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"
+          "resource": "repo://src/ai.rs#L238-L280",
+          "version": "repo-lines-v1:sha256:61675dd2723f8e4314c949dc027ac6e1a543b610794b2f4213100826f4f7eb90:eyJzZWxlY3RlZExpbmVDb3VudCI6NDMsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjYwNzBmYTg1ZjNiN2RjOGYwM2QwYjBmMjA2NWNhYzQ5NWYxNWQyNzQwMWExN2I4YzE2Y2Q4ZmQ5YWIwMWQ0MWQiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6Ijk5OWJkZWZlZWFjYTYzNDBlMDI5MGRjODZlODg5Yzg5ZDVlMTg5YWE1YmE0NGI1ODViM2I3MDczNGY0YTYyZDgiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjEyYTMyMmRjMWM3OTk2OTEzNTU1MGI0YzRkMzYyMThiNTQ4ZDY2NjE4OWRmNDFkMWM1ODIyYjU1MmE3OTljNjMifQ"
+        }
+      ]
+    },
+    {
+      "id": "claim_436ce19bdcf94d028d46da1886350561",
+      "statement": "The image description request comes from vision.md when present, otherwise from the built-in DEFAULT_IMAGE_DESCRIPTION_REQUEST, and a NoReply outcome of a description is turned into an empty-reply error.",
+      "evidence": [
+        {
+          "resource": "repo://src/ai.rs#L184-L196",
+          "version": "repo-lines-v1:sha256:e959c04c9811c5e76d9abb77e95982781c484819d9218c76a6cf30f03ac92baa:eyJzZWxlY3RlZExpbmVDb3VudCI6MTMsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjAyZDhhNWIxYzc4MTIwMGExYTNiYmQ2NGI4MDU0Yzk3N2E3OWZkNDk0MDk3M2I3M2RlMjE5MTVkZjQ2YTFmMGEiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6ImY5YjE5ODJlOGRkNzQ4MTE4MTQ2YjM5YTJkMDA2MmY0MDgxZTM0Nzc5Yjg2NzVjMjM0YzRmYjEyNGRjMzkzOTAiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImI4NjJmMzRhNzU3YWRjNDZhMDMxY2JmY2VjZjI4NTQ1NDJkMDc5ZDEyZjAzNzc4ZGMwMzgwYTI3ODY0YjYyMWUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjQ4NGU3OGMwZTQzNDBjZGJhNzIzZTIwNzdiODQ3NTUzZTg0ZDNlOTkwMjZmZWQxOGIwNDUxNDA1Y2MxNzQxMmEifQ"
+        },
+        {
+          "resource": "repo://src/ai.rs#L22-L23",
+          "version": "repo-lines-v1:sha256:093425babc91b6274b3b8f3a786d50d409a30fc9e71342d62c8cc3f248520993:eyJzZWxlY3RlZExpbmVDb3VudCI6MiwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiYjdhOTc4ZWIwYzk2ZDZlMDlhM2MxNzI4ZjNkNDhlZTE5NTA4MDQ2MGEwMjAyOGMxY2MyM2JiMjYzNTllY2RmMSIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiMjQzZjI1YjQzY2FmYmUxYTg5MDNiMjAwMThiMDEwZWE4NmJiZWQ3N2JhOTk3NjAwZTdjNTAzMmM5Y2M1Y2NmOCIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiNmY1ZjBkMGY4NWQyYWRmMmE1YjFmODFlNDY4NDJkZGRmMjljZTY2NDQ3NGY0MTllYjQ5NTAzMzMwMTNlYTU4NiIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiMDM4NDJhNGNmOTA1YmJmOWY2NmFkNTY5ODljYjZiYmFhNjNhMzQ0YmIwMjUwNjY4YmU2ODVhNjZlMGRjYjgwZiJ9"
+        },
+        {
+          "resource": "repo://src/ai.rs#L225-L236",
+          "version": "repo-lines-v1:sha256:bda19d95aafbb2269b1eb5d3138a4ddf5ed88f848b8254487116da95269ad3df:eyJzZWxlY3RlZExpbmVDb3VudCI6MTIsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6Ijg3ZmUwOTY4MzlmODMzNDkxOTA4ZTQ1NmFhNDI2MDJkZjQzNTc5ZDQyOWExMDg4YmVlNWRhNjE4ZjlkOWYwZmYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjBiYjIyNTU2ZGUzMmVjZTc4YjRlYThhMjUwNjQzODVkMWZhYjdjNjA2ODk5MTQ2NDJmZjk2NjkzNDE4NzA5ZWQiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjA1ZWY0YzRmMGNjMDcxMzBjOTc1ZWVhM2E3MzdkMGJmOGNjZjY3MThiODUwMDY5NWY0NTU2Y2E2OTk5NmQ0YTcifQ"
+        }
+      ]
+    },
+    {
+      "id": "claim_70aafb70d3424afcaadc8fc0e2628054",
+      "statement": "The no_reply tool description only says what the tool does, never when to use it; deciding when to stay silent is left to the system prompt in SOUL.md.",
+      "evidence": [
+        {
+          "resource": "repo://src/ai.rs#L42-L49",
+          "version": "repo-lines-v1:sha256:316aee398af311a49e09bb1b511935db92322af15e12d53c4ab9a88707448d9d:eyJzZWxlY3RlZExpbmVDb3VudCI6OCwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiMzk0M2Q3YWQzMjNmOTlkMDAzM2JjMTI5ODk3MmI3YmUwNzIxYjAxNDZjMmExYjlhZmZjYzc0NTcyYWFjZjNiNSIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiZDk4ODQ5YjY2YWQzY2FiZTdlZmVjOGU1ZmI5YWI3NjJkYTc4MzViMzg4YzM4YWIyOWVkNDM1MjBiNGI4YTk1NSIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiZGNkYjE3MDIxM2RkOWVkM2I2OWExZmU5ZWYwNzg1YWRhMzljNzkzMDA3N2E0OGM5ODMxNWE1ZjJmOWFlYTIzNyIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiNzUwZjY3OTE5ZWU2Y2JmNWYxY2VmYjNkZDIyMTViMWFmOTJmMDkwZDdhNWFjMDg3OGZlY2IwOWQ0YTczYzM4NCJ9"
         }
       ]
     }
   ],
   "verification": {
     "by": "openwiki/0.7.2",
-    "at": "2026-10-20T20:27:06.515Z"
+    "at": "2026-10-21T21:49:12.291Z"
   }
 }

+ 4 - 4
openwiki/.claims/integrations/web-control-panel.json

@@ -1,13 +1,13 @@
 {
   "schemaVersion": 1,
-  "pageVersion": "sha256:659021f213da32179df3e732784be9d211b7b699c07342e5abae11b3d55b0d63",
+  "pageVersion": "sha256:3ffa0ef4024e9b6d6363e8a29f878dc08440b9bdb206eb8cbc0e7a43f37d70ed",
   "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",
+          "resource": "repo://src/main.rs#L88-L97",
           "version": "repo-lines-v1:sha256:54c6448bd45241fd4b90479b1a91e2fd528e7a38f4ac5d543eda728fbf3756d5:eyJzZWxlY3RlZExpbmVDb3VudCI6MTAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImYwZjA3MmY1NzA4NTRiMGM0ODg3YmRlZDU4YzU2NWRiZGExOTNlYmY3NGIwODZmMjYyNTFmZjZhOTZiMmJiYWMiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImRmYWI3MTBkZGY3ODU5ZTU3NTc2NGE5MTNhMDAyYzc0NTg1ZmZhNzAwMTUxMGNiODgxMWRhODNkMmNmNzRiMzQiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImI1OTAxZTAwNGRkYTg1ZmUwNjg0OGVkYjhkMWIzM2Q1NTNmYWM4ZjkyMzZlZTFlMjlkZTNmNGUzMWRmNTM4NTkifQ"
         },
         {
@@ -79,7 +79,7 @@
       "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",
+          "resource": "repo://src/app.rs#L1009-L1025",
           "version": "repo-lines-v1:sha256:6905da13ba37beaf073ac6f3f6eb07a12c4b6a34bcf57e4beca54f40fe6b64b0:eyJzZWxlY3RlZExpbmVDb3VudCI6MTcsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjA3YTEzOTMxODdkZWNmZmM0NzMzYzYwY2FiYjA2MTkwYjdkYjAxMjJjYWEyYWM3YjRjNGUwNzBiNzIxNTk1NWQiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjM2MGUyOWExOGZkNDE5NDVjMjM3NTgxMzM4ZGI3NWFjOTIzYzYxYjVmYmFiOTNlOTM3ODdlNWJmMGMyMjk4OTciLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjQ0NmM2OGRiNzFjZDkwMmMyZDMxY2QyYjQ5Y2MyM2ZjYzFmNjVlYWRlY2JlZGRmNDFhOWEwZjVhNGIxMzE1MmQifQ"
         },
         {
@@ -127,6 +127,6 @@
   ],
   "verification": {
     "by": "openwiki/0.7.2",
-    "at": "2026-10-20T20:27:06.515Z"
+    "at": "2026-10-21T21:49:12.291Z"
   }
 }

+ 25 - 35
openwiki/.claims/integrations/whatsapp-client.json

@@ -1,6 +1,6 @@
 {
   "schemaVersion": 1,
-  "pageVersion": "sha256:2d56656fd165a43e6f66b741865ee0cc8e711720783c722f71a531e3b8ec0c37",
+  "pageVersion": "sha256:6a3c8a9d9be1d3bc11179381bdb384623d59cb3e2c026f4f3d62b3bf3a5f84da",
   "claims": [
     {
       "id": "claim_5cd95bce2100497cbe7ac9e4c2cb1856",
@@ -17,7 +17,7 @@
       "statement": "start requires a UTF-8 session path, creates its parent directory, opens SqliteStore, builds a Bot with an EventForwarder, spawns the Translator and bot, and emits Disconnected(client stopped) when the bot ends.",
       "evidence": [
         {
-          "resource": "repo://src/whatsapp.rs#L211-L244",
+          "resource": "repo://src/whatsapp.rs#L208-L241",
           "version": "repo-lines-v1:sha256:4cd0297c9556cc47b7469013a1a4cd7bc98f9cf55a80ed52c1d041a5958b527e:eyJzZWxlY3RlZExpbmVDb3VudCI6MzQsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImM1Yzc3OTRhMTU2OGMwYTkxN2VkZGI5NWIwOWQyNzRkZDQ1ZWJjMmE1NGQ4NjhlYWU1NTJkMTEyMDIzMzMyY2IiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImY3YmZiOTBiNDk2OGUyODM0NjAyZTY1OTA0ODBkNWNkY2Y5M2M2YjgyOWYxOWIxODkxYjQ2YjFkZjY0MGY3NDciLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjIzNjEyMDNhNTJjMmI5YTk4MmJjMjVkNWE3OTU1OWIwNGJlNjAxODcxYzZlZjk1ZWExZTMzNzU4MmM2YTRlZDcifQ"
         }
       ]
@@ -27,7 +27,7 @@
       "statement": "EventForwarder forwards raw events in order to an internal channel with interest limited to SUBSCRIBED_KINDS, which includes OfflineSyncCompleted.",
       "evidence": [
         {
-          "resource": "repo://src/whatsapp.rs#L247-L268",
+          "resource": "repo://src/whatsapp.rs#L244-L265",
           "version": "repo-lines-v1:sha256:90cc37eed600d805f12bf6cff91abb61379cdfa881ee401bcd902a8e11133f60:eyJzZWxlY3RlZExpbmVDb3VudCI6MjIsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImQxN2YyYmY0MzZiNzViNGYzNDUyNWY2MTk0NjU4OGE5ZmQ3MjRkNDA2ZDNmODk0NjI4MTE2ZGRlNzg0NDVmNTAiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImZiOGQyYjQ2OTczM2U4ZTNmOWVjNWRhMDk2Njk3NDlkMDJiYTczYjM1YTQzYmQ2YzRkNjNhYTE4M2FkY2U5ZGUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImU0NTQ4YjU3YzUzZDdhYmNmY2QwY2E0YTJjZGZiZjdhYjk4NWYzMDYzMGM1N2FiYTA2Y2U1MGZmYjc2ZjZiNmMifQ"
         },
         {
@@ -38,11 +38,11 @@
     },
     {
       "id": "claim_2e8c63c28ca0475da8d2604a3bb43b93",
-      "statement": "WaEvent has Qr, Connected, Disconnected, LoggedOut, OfflineSyncCompleted, Message (with optional image and addressed_to JIDs), Contact and Error variants.",
+      "statement": "WaEvent has Qr, Connected, Disconnected, LoggedOut, OfflineSyncCompleted, Message (with an optional image reference), Contact and Error variants.",
       "evidence": [
         {
-          "resource": "repo://src/whatsapp.rs#L81-L110",
-          "version": "repo-lines-v1:sha256:5068cd514dba30dbb17394b06016a02190c884e7d9acaa01b60ee605e7d8d0d6:eyJzZWxlY3RlZExpbmVDb3VudCI6MzAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImUyMDliMmJhYjE2NjZhZjkyYjQxZWViYjI0MWM1MGQ5ZjI5NmRiMDViOTMyNzI5MzExYzFjM2NkYzY3ZDUxNDkiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImIxNmE2ZjRjMmRkNDg2YjE4NDI2YWUzNGYyMjc5YWViYjM4OTY0OTM3MTE0YmZmZjc1NmY4NGIwY2RmZGM2NzgiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjY1MTM1NTA1ODhkNzA3ZGJiODBlZDU1N2ZjZTRlYTU1NjY0ZWVlMzM2NWE1YzQxOGJjM2FiZjQ4MzNiZDNmZDYifQ"
+          "resource": "repo://src/whatsapp.rs#L81-L108",
+          "version": "repo-lines-v1:sha256:0c68b0b9c76d57c34f906ece59c675e4be58899b2a26f2631d9ee4722c3e8379:eyJzZWxlY3RlZExpbmVDb3VudCI6MjgsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImUyMDliMmJhYjE2NjZhZjkyYjQxZWViYjI0MWM1MGQ5ZjI5NmRiMDViOTMyNzI5MzExYzFjM2NkYzY3ZDUxNDkiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjAxYmE0NzE5YzgwYjZmZTkxMWIwOTFhN2MwNTEyNGI2NGVlZWNlOTY0ZTA5YzA1OGVmOGY5ODA1ZGFjYTU0NmIiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImIxNmE2ZjRjMmRkNDg2YjE4NDI2YWUzNGYyMjc5YWViYjM4OTY0OTM3MTE0YmZmZjc1NmY4NGIwY2RmZGM2NzgiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjcwODc1MzQwZmI2YTRmMjNkNmJmMzU5MGU5MzU3ZDBhODNjMjQ0NWM3OGQwYTg4ODc0ZjQxMGM0OGRlMzA1NGIifQ"
         }
       ]
     },
@@ -51,7 +51,7 @@
       "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-L462",
+          "resource": "repo://src/whatsapp.rs#L413-L459",
           "version": "repo-lines-v1:sha256:2e6abdf7a1ad4472d0ad4892b83fcfcab8abaa1e22f9809e3ff9cc172d6d6669:eyJzZWxlY3RlZExpbmVDb3VudCI6NDcsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjdmNWQyMzJiN2E2YTQ3NzhhZWE4NGU5YTIzNDhjMWU3NDdlZTdjOWY2NzUxNThjNWEwMGMxNDIzMTBlYTAxNTciLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6Ijg2ZDM0ZTcxMTQyZjZhMTJjYmM1NzA1N2QxMTc0ZGE2OGM3MjBkMmRlNGY1YmRjZDMxN2FhMjVjZDljYTE0NDEiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImM2ZTYxOTdhNjY3YmI1ZjgyYzI4ZDdmYWM2OGIzYWE1YzU1YTk5NmQ2NGRkYmYyOTI3ZWQ3YjAyNzk5MDI0NWYifQ"
         }
       ]
@@ -61,18 +61,18 @@
       "statement": "canonical_jid prefers the phone-number JID, resolving LIDs via the alternate JID or the store LID mapping, and reports lookup failures.",
       "evidence": [
         {
-          "resource": "repo://src/whatsapp.rs#L369-L384",
+          "resource": "repo://src/whatsapp.rs#L366-L381",
           "version": "repo-lines-v1:sha256:c9a3a286dc3e2c684087cd5ca273467ac6be5cc180556477e58883a1587ad161:eyJzZWxlY3RlZExpbmVDb3VudCI6MTYsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImRjNzhjNDlmYTU0ZmZiOGEwMmNhNDczYzhjYTFmYjY3YjVjNDQyMjBiNDUyMDYzNWI0NmI1MjgyODRkODc1MjgiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjY3ZWQ4MGI0MjM1MDQ2ZTljZWQ4OTEyNjc0ZDRhMTMwMjM3MzJiNTNhYThhNDdhMGYxNzUyNGI2ODM1ZjAxM2YiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjcyNDliZThhYWJlNTBkZDkyNzY5NDkwNzViMDI3NTA1Y2Y0ZWQ3MWE3NmIxYmJiZjg0ZjFhZTFmMjdkZDU4Y2UifQ"
         }
       ]
     },
     {
       "id": "claim_bdbfcaf218dc45d88ab121c191882d66",
-      "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.",
+      "statement": "handle_live_message skips status broadcasts and silent messages, canonicalizes 1:1 chat JIDs, notes push names and attaches an ImageRef only for image texts.",
       "evidence": [
         {
-          "resource": "repo://src/whatsapp.rs#L478-L525",
-          "version": "repo-lines-v1:sha256:facf13f54250ccf6043e3f41547bfa0285bbb01882144bb37904d51a76ef7d82:eyJzZWxlY3RlZExpbmVDb3VudCI6NDgsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImFkMmYxYjJjNzYwNGI3NDk3NmI5YmMyNjRkMDBlYjJlODJlZWM0MTk5NzE4NzJjMWFjOTE2OTJhNDQ1ZjMxYmYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjdkZTRkYTlhNTUwNmQ1MmI3NWUwOTY1YjY0ZWI2Zjk5ZDhjMWE4MmZjMWU1ZjE1YTZhMjY0MDhlYjc2MmJlOTUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjY2NDFiNWZlM2M5YmRjNTg2MGQzNmM1MzMzZjMyYTY2OGU4OWUzNWJjZTBiNjU2ZTViYjY2MjVhNjFhNTE3ZWQifQ"
+          "resource": "repo://src/whatsapp.rs#L475-L521",
+          "version": "repo-lines-v1:sha256:a4d4ee9a079e03eb111e30743b9678dab815a424e54ce5a0be4a0f1ddd68051c:eyJzZWxlY3RlZExpbmVDb3VudCI6NDcsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImFkMmYxYjJjNzYwNGI3NDk3NmI5YmMyNjRkMDBlYjJlODJlZWM0MTk5NzE4NzJjMWFjOTE2OTJhNDQ1ZjMxYmYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjdkZTRkYTlhNTUwNmQ1MmI3NWUwOTY1YjY0ZWI2Zjk5ZDhjMWE4MmZjMWU1ZjE1YTZhMjY0MDhlYjc2MmJlOTUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjY2NDFiNWZlM2M5YmRjNTg2MGQzNmM1MzMzZjMyYTY2OGU4OWUzNWJjZTBiNjU2ZTViYjY2MjVhNjFhNTE3ZWQifQ"
         }
       ]
     },
@@ -81,25 +81,25 @@
       "statement": "Contact names are ranked Push below Saved and note_name never lets a push name override a saved one.",
       "evidence": [
         {
-          "resource": "repo://src/whatsapp.rs#L270-L277",
+          "resource": "repo://src/whatsapp.rs#L267-L274",
           "version": "repo-lines-v1:sha256:63fcb3bcc3fee239b6af98cdaeb31163e5ebb60f59467e221c3df0d186d7af3c:eyJzZWxlY3RlZExpbmVDb3VudCI6OCwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiYjU2NmIzYWVmYmU1NWMyMjE5ZTM3MzEzYmFiOWMwZDZkNGNmY2Q3NzUzMjZiMDQ1NWUxY2Y1YmM1MzE3OGZiNyIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDEyY2EzNDVjY2Y3NWJmOWMwODA2YmNlNjk1YmU4ZGU4MDhiNzk5ODQyNTFhN2E1NGQyMDJjZjYxMDFkZDQ1MSIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiMTllNzYzNTVjZjExOTk0ODBjM2Y5OTgzNmIxNDg2Yzg0MWQ4OTFmYWFkOTA0YzYwOGUwOTMwMjJkMzc1N2FlZSIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiMjg2NmRmMDRmMjc4MGQyOWI2MDQ1YWM1OTM2NmUzZjI2OTk5ZDIxMTdjZmVhZTcxY2ExZGQ5NzcwNGE5YTY1YiJ9"
         },
         {
-          "resource": "repo://src/whatsapp.rs#L332-L350",
+          "resource": "repo://src/whatsapp.rs#L329-L347",
           "version": "repo-lines-v1:sha256:0cb33ca45f9b72198804d2a68dffd870e11f0018eff0f5b4d6e6639f0dabecb2:eyJzZWxlY3RlZExpbmVDb3VudCI6MTksImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjI3YzZkOTQ3ZWQ4OWVjMDgxZmY1ZDUxZDliMzYxOWRhOWRmMjZhODdlZGYxM2MwNDMwMzhiMDRjN2E5ZTliZWEiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjZmYjQ4MjFjNTZhMzYwOTAzMGFlYzM0MWQ4ODI5NDhmZjBlMmYwY2VlNDQ1MzQ0YzczNTJiYmMwYjlhYTA3Y2EiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjY1NmEzOWUwZGJjMTIyMGQ0OTg3Mjk2NDBiOTEyNWQ0YjIxMWMzNmVmZGRlODVkMWIzZmM4NjgyZTU2NTE0NWEifQ"
         }
       ]
     },
     {
       "id": "claim_835ad8e902fd4a18909210d2c9e1408b",
-      "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.",
+      "statement": "History sync chunks are decoded in spawn_blocking and emit push names, inline contacts, conversation names and messages without image references.",
       "evidence": [
         {
-          "resource": "repo://src/whatsapp.rs#L559-L652",
-          "version": "repo-lines-v1:sha256:70e7c7d582124297e079899f1a4cba88e33cab1397f009bfda85e367709c2754:eyJzZWxlY3RlZExpbmVDb3VudCI6OTQsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjAzMDUzNzcyNjU4MGYxZDA2OWQxMjUwNTI3MmRhODg5OWM0M2RiYmZiZmM4NmZiY2IzOGJhMmNhZWE0OWU4NDIiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjgwMmNlYTE4N2JjY2NlOWZhNDkyM2M4NzAzZDliYjk3ZWZhZTZkNTk2YTc4OTQ2YTlhZWMzMzc4MDM3YjU2MWMiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6Ijk5NzM0ZmViMmI1ZGU2NjEwOTU2ZjI3N2RjMzAyMWJjMTZhNDVhYzBhMjc2MWM1Y2YwOGM3M2NiZjA0NTU0MWYifQ"
+          "resource": "repo://src/whatsapp.rs#L555-L647",
+          "version": "repo-lines-v1:sha256:6c5078dc648f03cc0bba1f6aced5618a9773cf5ce4e6b821cd9284ef4c4268ae:eyJzZWxlY3RlZExpbmVDb3VudCI6OTMsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjAzMDUzNzcyNjU4MGYxZDA2OWQxMjUwNTI3MmRhODg5OWM0M2RiYmZiZmM4NmZiY2IzOGJhMmNhZWE0OWU4NDIiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjgwMmNlYTE4N2JjY2NlOWZhNDkyM2M4NzAzZDliYjk3ZWZhZTZkNTk2YTc4OTQ2YTlhZWMzMzc4MDM3YjU2MWMiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6Ijk5NzM0ZmViMmI1ZGU2NjEwOTU2ZjI3N2RjMzAyMWJjMTZhNDVhYzBhMjc2MWM1Y2YwOGM3M2NiZjA0NTU0MWYifQ"
         },
         {
-          "resource": "repo://src/whatsapp.rs#L709-L750",
+          "resource": "repo://src/whatsapp.rs#L704-L745",
           "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#L841-L899",
+          "resource": "repo://src/whatsapp.rs#L836-L894",
           "version": "repo-lines-v1:sha256:2b1c51b48fce477ab09efb54b0e727a20b91bb212e43c005ffb0ba48ba3f3c2b:eyJzZWxlY3RlZExpbmVDb3VudCI6NTksImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjNjNjE2NzkwYTVhOWYyMTUyNGQyMTkzMGYwODhiYWNiYjkxZmYwNGQ1YTcxZTQzOTljNjEyMjdiNTVlNmRiODAiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjkwNzA2MWVjY2M3ZmRhZTJiN2I0MjlhYTAzYTZkMTNmMjNjNTY5NzZmMmRmOWU5Y2UyY2ZhNjM4ZjdlMTQ1ZDMiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjcwNzFhZGQ3YWQ1Mzg3NDYwZWJmZWY1YTkyOGQyMTAxZGJkMGQ2ZTU2YzRhMzliNzA0NDViMWM3OTU3NmZlNmEifQ"
         }
       ]
@@ -119,7 +119,7 @@
       "statement": "send_text rejects empty text and returns the message id; download_image keeps images in memory with an image/jpeg MIME fallback.",
       "evidence": [
         {
-          "resource": "repo://src/whatsapp.rs#L153-L186",
+          "resource": "repo://src/whatsapp.rs#L150-L183",
           "version": "repo-lines-v1:sha256:e1962d0636f891f6df0c21c92dc4855febf775ec84d05389c1d53f37c8afd846:eyJzZWxlY3RlZExpbmVDb3VudCI6MzQsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImU1MzA5Y2ExM2M4ZDJlNTg5NTFiYTlhZmJlNzM4OGIxMzQ2MGUwMGUzODU1NGY5ZDE1NWVmODc4ZmJjZGU0NDMiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjI0NDRlMWZiZDYyMWQ5MjhjNjUyNDE1NThlNGM0YmIzZjQyZjI1ZGQ1NzY5MjU1YTI5OGZmZTlkZWI3NDE2NjgiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjMwZGZlODgwMmNkNjM3Y2NmMjZjNWIwYjZhOGJhYmNhOWRhYzI5NTNlOTRmYjA4YzFjNzA1NGVkOWUyNmYwMmUifQ"
         }
       ]
@@ -129,39 +129,29 @@
       "statement": "own_jids returns the account PN and LID without device suffix and is empty before pairing.",
       "evidence": [
         {
-          "resource": "repo://src/whatsapp.rs#L194-L200",
+          "resource": "repo://src/whatsapp.rs#L191-L197",
           "version": "repo-lines-v1:sha256:7ea1e000732e0183f2e0d6671934cf8532875e711139eacf688d202ddfc274f2:eyJzZWxlY3RlZExpbmVDb3VudCI6NywiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiY2ZkNDJkOTZmZjZjNWQ3ZDg2YmZjMzI1MDQ4MzYwZWEwMDE4N2VkNzhlNTM1M2Q0NWY0ZGI2NmZiNDBiYjk2NyIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDg5M2Y0MGUzNzgxZWI4OTZjODJkMDQ2YTA4MDY3YTExZTM1Y2RmMmYxZGZhZjI2ZmVhOTBiOGRiNjY3NjQxMyIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiOWY1NTZkYTZjNjZkNTE3ZmZkMmYzMzkyOTY0M2Q4Y2UwMTg5YzNmNTJmNDJhYjc3ZTExNDQ3NGYxNzQ0YzZiMyIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiMWJjZmRiYmE1MDBmYjJlYTIxOWFlN2I0NDQ2ZTNlNTU1MDhhZWIxZDdmN2NiYzgwZjE1NTI2MzMzOTlkZjUxYyJ9"
         },
         {
-          "resource": "repo://src/whatsapp.rs#L830-L832",
+          "resource": "repo://src/whatsapp.rs#L825-L827",
           "version": "repo-lines-v1:sha256:2dff1120b156dde5cd502a03211b000f533177e7214711f8a8a081955c3663ac:eyJzZWxlY3RlZExpbmVDb3VudCI6MywiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiOWY1MGVlZDdhYWNmZGE2Mjk4Mzg4Yzg2MmI1ZjIyNDNlNzIyYWMxNWEyY2FkNzAwNWUyOWQ1NzIxMmE1YThiOSIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDEyY2EzNDVjY2Y3NWJmOWMwODA2YmNlNjk1YmU4ZGU4MDhiNzk5ODQyNTFhN2E1NGQyMDJjZjYxMDFkZDQ1MSIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiNGNjZjc5MzkxNTY5MjhjYzk4Y2ViZDIwYjJlYTA2OWM0ZWNlZGJlZWE5Nzk2MjBiN2IzNWUzYTY5Mjg3YWE4MyIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiM2I4MWNkZTZlMzVlZWI1MjRlMzM5ZDFlMjM1MzE5Y2Y1NTI5YjFhNTBhMmE2MDUyZmUzNWI1ZDQ4NWRkOWUxMyJ9"
         }
       ]
     },
-    {
-      "id": "claim_8594e94c369c4cb383b6c2ee6b3eb916",
-      "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#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",
+          "resource": "repo://src/whatsapp.rs#L989-L992",
           "version": "repo-lines-v1:sha256:af6676593e9e90c7ac6a4b2b64625d687ad77fa0265e7f46bb6b8dbef4446d86:eyJzZWxlY3RlZExpbmVDb3VudCI6NCwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiMzFiZTM3ZmI4YTA3YjI0YzY1ZmVjNGQ3MGI2NWNhNDAzNTU5ODQwOGE3NDhhYjg2YWY4YTY4YWJlMGJjMTNhYSIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDg5M2Y0MGUzNzgxZWI4OTZjODJkMDQ2YTA4MDY3YTExZTM1Y2RmMmYxZGZhZjI2ZmVhOTBiOGRiNjY3NjQxMyIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiYTAwZGIwNDBmMjkwOWJmOTc4ZmYwYTk1NmE1NWU4M2M2NTM0MThkZTZiYjY4ZjgwNDFkYTk3NmY4ZDNkMjI3MCIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiYjQ1MTJjNzhmN2UyZTJiODdhNDZhYThlZWUxYTg3YjIwZjZjMWE0Y2MwZmNkNDQ4ZDhmZjJmMWJlOGYxMGQ4YSJ9"
         },
         {
-          "resource": "repo://src/whatsapp.rs#L440-L449",
+          "resource": "repo://src/whatsapp.rs#L437-L446",
           "version": "repo-lines-v1:sha256:55006adc8131456b9811de9f46520c07874aab146e6d5deb0b359a138bc063e8:eyJzZWxlY3RlZExpbmVDb3VudCI6MTAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjljNjAwZjllYTA2YzVjZTAxYjEzODExOGM5YjdhZDVjNTUxYzBhMmNjZDkxZTJmNWQxMWI4OTNiMWU5YzdjN2IiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjU1MjJjNDJjZDE5YzZkMDNkODA4YzQ1MjcyMDU1MjgyNWI3ZWExYzViNDkxYjIxODUxMWNhYzc0MTVjZjlkOWIiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjlhNDQ2MGQwYzUyNjkwYzM3YTQyOTI4MTc3NjkzNmQ4ZDA3YTJhYjg3MDEyYTRkMDY0MWNiMTQ4ZjkzOThjOGUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjQ3YTNlYmViMDcyOTcwMjY0MTFmZWVhZWViOTNkODBhMDZjOGQzNmE0OWE0NzgyNjhiZDFjNjJkMWRkOTk2YTkifQ"
         },
         {
-          "resource": "repo://src/whatsapp.rs#L808-L815",
+          "resource": "repo://src/whatsapp.rs#L803-L810",
           "version": "repo-lines-v1:sha256:e6e70ed71f30e7f2d2201053fec0c50f7a478e2081980f40b73ee55fd11ccbfe:eyJzZWxlY3RlZExpbmVDb3VudCI6OCwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiN2ZmYzA3OTdhYmQ1NjAwYmU5MWY4MjMyNWZmZjhlMjllMDVmNDY3MGZiOTQ5YzdmODZkZTRlZmJhOTQ5ZmIyOSIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDEyY2EzNDVjY2Y3NWJmOWMwODA2YmNlNjk1YmU4ZGU4MDhiNzk5ODQyNTFhN2E1NGQyMDJjZjYxMDFkZDQ1MSIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiMTllNzYzNTVjZjExOTk0ODBjM2Y5OTgzNmIxNDg2Yzg0MWQ4OTFmYWFkOTA0YzYwOGUwOTMwMjJkMzc1N2FlZSIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiYzYxNmY0NTJmYTU5OWI0YjM5MWIwMmVjYjI1OTNjMzk3ODVhMDFjZDZiZTIwMmM0OTQxMjk2NGViYWQ3YzFmYyJ9"
         }
       ]
@@ -169,6 +159,6 @@
   ],
   "verification": {
     "by": "openwiki/0.7.2",
-    "at": "2026-10-20T20:27:06.515Z"
+    "at": "2026-10-21T21:49:12.291Z"
   }
 }

+ 71 - 41
openwiki/.claims/operations/configuration-and-data.json

@@ -1,18 +1,18 @@
 {
   "schemaVersion": 1,
-  "pageVersion": "sha256:f973194dff48fa997deb016f452d6f1515458eef3b52aa111bc52439dafc1b81",
+  "pageVersion": "sha256:895dd056591483fa81fad9cbc781504961253ccdbdaf42c8ed0f92c3685c50cd",
   "claims": [
     {
       "id": "claim_0519d797500c4fba86d6c64ac1e60df4",
-      "statement": "Configuration is read only from config.toml and no environment variables are used.",
+      "statement": "Configuration is read only from files in the data directory (config.toml, SOUL.md and vision.md) and no environment variables are used.",
       "evidence": [
         {
           "resource": "repo://src/config.rs#L1-L25",
-          "version": "repo-lines-v1:sha256:9e58106875f518f2506700924e5e96bab155046a8df51e59257dc94cac50930e:eyJzZWxlY3RlZExpbmVDb3VudCI6MjUsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImI0MGE0NTBjZTI3ZmY3MjYwYzM4M2U4NDk4MjhjNzY1NTVlZTk1ZTllM2JmMTJmOWYxY2IxY2Q2OGZlZGFkMDgiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjZmNzkwMmU2MjBjYmU0ZDIwMGU3NTVjYmY5YTMyOWYxMzFkYjFhM2UxZDQ2Mzg1MWU1YjFiOTE4NmM2YTVmYzUiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjowLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImUzYjBjNDQyOThmYzFjMTQ5YWZiZjRjODk5NmZiOTI0MjdhZTQxZTQ2NDliOTM0Y2E0OTU5OTFiNzg1MmI4NTUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjcxMzE1MjZlMmQ3ZWFkMDgwNmJkNzJlYjRjMGJlMjE1NjI4ZWZiMGMzMjc3OTM0NjVkYjM1NmQ2NTQ4NDAzN2YifQ"
+          "version": "repo-lines-v1:sha256:4cfa961e2b8156a4d104f9c982c82d3d21b8707225ae8bec3ddc7e273ba1ed45:eyJzZWxlY3RlZExpbmVDb3VudCI6MjUsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImI0MGE0NTBjZTI3ZmY3MjYwYzM4M2U4NDk4MjhjNzY1NTVlZTk1ZTllM2JmMTJmOWYxY2IxY2Q2OGZlZGFkMDgiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6ImMyZTJkOGM5Y2M3MmUzODMyNGJkYzhmODkyZmY3NDdiNTVjMmNkNWRkMjhiYWFhMTdkNjE0NWMwMWI5Mjg0NWMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjowLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImUzYjBjNDQyOThmYzFjMTQ5YWZiZjRjODk5NmZiOTI0MjdhZTQxZTQ2NDliOTM0Y2E0OTU5OTFiNzg1MmI4NTUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjcxMzE1MjZlMmQ3ZWFkMDgwNmJkNzJlYjRjMGJlMjE1NjI4ZWZiMGMzMjc3OTM0NjVkYjM1NmQ2NTQ4NDAzN2YifQ"
         },
         {
-          "resource": "repo://src/config.rs#L254-L263",
-          "version": "repo-lines-v1:sha256:765917d933b8312e24ca9f330a3ebaad4a414c773af0d2ae857bbd7e091afe34:eyJzZWxlY3RlZExpbmVDb3VudCI6MTAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImQ4MDc5NGE2MDA2MzNhZTY5ZGFlMWYzOThjYTk3ZTc5Yzg3NDc5ZDFjNjE3MDg0ZTRkMTgyODZmMTlkODFjMTUiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjgzZDI4NDFkZmUxZTBkZDIxMmQwYzE1ZDAxZTVhYzkyMTBmNTgwMGI4YmRmZTk5M2FkZDAwYmQ5NGYwMTRkNmMiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImU2YzkxMzJjZTZhZmY4ZmJjMmFmMzRiY2JmYWE1ODVhN2RmMWUxODViYjc5OTFlY2I5YzgxYTUxODg3NWUyOGUifQ"
+          "resource": "repo://src/config.rs#L265-L281",
+          "version": "repo-lines-v1:sha256:4ade9876bd8e1653384dcebde47d413e1aab36995b53055ab5441d82e8b28290:eyJzZWxlY3RlZExpbmVDb3VudCI6MTcsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImQ4MDc5NGE2MDA2MzNhZTY5ZGFlMWYzOThjYTk3ZTc5Yzg3NDc5ZDFjNjE3MDg0ZTRkMTgyODZmMTlkODFjMTUiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjgzZDI4NDFkZmUxZTBkZDIxMmQwYzE1ZDAxZTVhYzkyMTBmNTgwMGI4YmRmZTk5M2FkZDAwYmQ5NGYwMTRkNmMiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImU2YzkxMzJjZTZhZmY4ZmJjMmFmMzRiY2JmYWE1ODVhN2RmMWUxODViYjc5OTFlY2I5YzgxYTUxODg3NWUyOGUifQ"
         }
       ]
     },
@@ -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#L166-L170",
+          "resource": "repo://src/config.rs#L164-L168",
           "version": "repo-lines-v1:sha256:dd4141940976146e96656f2de294f92e1275a16efba97aa2c7318676f34bdfa4:eyJzZWxlY3RlZExpbmVDb3VudCI6NSwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiMzVhMDYyMDkxMmIzMTUwZDZlYmQwNDQyOTUxZTdjNWEwODlmODMwMzYzNmJjODZiNmQxOWRmN2U3YTFmMWEzZCIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDEyY2EzNDVjY2Y3NWJmOWMwODA2YmNlNjk1YmU4ZGU4MDhiNzk5ODQyNTFhN2E1NGQyMDJjZjYxMDFkZDQ1MSIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiNGU1ZGI4YzkwMjczZTAyOTc3MTY3ZWMwNDM0N2M0MWQ5MmE1MTE4NDMxMTA0M2UyMTU2YmM4MGFiYjkwMWQ0NyIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiMDY4YzgxNzM5NzQwNTE2YzI4OWQ4ZWRlZWM0N2IwMjcxMGFmOWVmNWM4MjE4M2NhNWQwMTc4MWVhMjI1YjU1MCJ9"
         },
         {
-          "resource": "repo://src/config.rs#L293-L296",
+          "resource": "repo://src/config.rs#L312-L315",
           "version": "repo-lines-v1:sha256:70ea48c62bb6583f73eaa3d204fb92456b268569dccd85ba7c47f85f0175339f:eyJzZWxlY3RlZExpbmVDb3VudCI6NCwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiMDE1Zjk2ZjM2OWY4YjE1NjExZWVjMDljZTBjOThiYjNhZTNhM2U5Y2U2MDEzZGNmMGE3OWY4ZTM1MjVmMjMxNyIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDg5M2Y0MGUzNzgxZWI4OTZjODJkMDQ2YTA4MDY3YTExZTM1Y2RmMmYxZGZhZjI2ZmVhOTBiOGRiNjY3NjQxMyIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiM2UxNTlhZDA5YmYzMzllMTM2ZmY0NzVjMjc4NDNkMTYyY2JiMTI2ZGMxMmFiMDM3MjE3M2M2ODY4ZGM5NzUwMyIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiMTZjMzk0MWE5YjYxMDI1Yjg1ZWU5ZWEzNzYzOTUyYTViMTM5NmNkMmY2ZjczMTkyZGQ3NDYwNDc5OTlhYjY0MSJ9"
         }
       ]
@@ -35,30 +35,26 @@
       "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#L298-L320",
+          "resource": "repo://src/config.rs#L317-L339",
           "version": "repo-lines-v1:sha256:f895d312efc12c901e18e3c47999e82cde54de595ed326f94dc9c80be206fd75:eyJzZWxlY3RlZExpbmVDb3VudCI6MjMsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImIxNjY4MjQ0NGNiOTU1NTQxNjJjOGIzNjYxMjg0YTJhNGMzMDIwZmQxODU3OTYxMDU2ZGE1MjMxM2Y2YjgzYjkiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjIxNTA5ZGYyZDNjMWJjODQxODQ2NDlhOTg3NzAwZTU4ZGVmNTJkZGQ1YzBkZTVlZWIwZjY0YTc1YmQ5MzBlYzEiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImE1MjE2MDRiMzVmYzQwYWYwMGNiYTRlZWEyZmQ1ZTJjM2FmMTg2M2IzOTExODVkN2JmNGJiMDllNzJkMGM4YjMifQ"
         },
         {
-          "resource": "repo://src/config.rs#L47-L56",
+          "resource": "repo://src/config.rs#L45-L54",
           "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, 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.",
+      "statement": "Keys openai_api_key, openai_base_url, openai_model, openai_vision_model, ai_log, reply_delay_seconds, catch_up_seconds, ai_retries and web_port map to Config with defaults (base URL, model, ai_log false, reply delay 30 seconds, catch-up 7200 seconds, 2 retries, web panel disabled); the system prompt is not taken from config.toml.",
       "evidence": [
         {
-          "resource": "repo://src/config.rs#L265-L287",
-          "version": "repo-lines-v1:sha256:980417922309d654c96024f5b863cf6ea53769c253161d25ff0d1c4d3a062c88:eyJzZWxlY3RlZExpbmVDb3VudCI6MjMsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6Ijc2NWZhNGU5MTRkN2UzOWE2MGJhZTc1M2NhMjE2YTg3NGE3NmZmZWY4NTc4OTY5NTZkNjg0MDM4YTY1MWJjZDAiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImM5MGRlZTM4YjdkMWZiYjMzNjNiMjY5Y2EyNjc4MTE4YTU2ZGNlZjlhMTU4MmE2N2Y3YjM4ODY2ZDZiYmFlN2UiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6Ijg4ZWYzM2E3NjQ3YTM0NDVkNzE1NDU4YmE2Yzc0OGY2NmIyYWZiNzYxYzEyODc4NWU3MzRjZjIyOThjOTdkMzUifQ"
-        },
-        {
-          "resource": "repo://src/config.rs#L34-L45",
-          "version": "repo-lines-v1:sha256:20a9554333b6e6a9c3dc056ceac92d156d754d88fece885f6f018e8eb85b4f38:eyJzZWxlY3RlZExpbmVDb3VudCI6MTIsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjdkNWE4ZjI1MGMwMjJlOTMwNzUxZGFlMTg0YjBhZGMxNTUwYWRkNzVlNGJiOTUyMDBiNTE0NmY0NzQ5MjdlODMiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6ImQ1MDFkZTlkMjcyZmZkODVmYTk1OWMxN2JiMTdjYWZkMmJkYjM1YTgyMGQzNDA1NGRkOWQ4NThkNzUyMDMyODIiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6Ijc2YTNkY2UwNTMyZWI3NmI2M2EyNmViYzdlZmNkNWE3YTViYzFjMTFhOWFiZGRhM2Y3ZjJmNzhkODc1Y2Y3M2YiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjdlODA1ZjRiMzVlMzg3NmExMjg5OTE1MjgyYWRhNWNkMzNhNzdmNTMyYmYyOWMxOGM4OWRhMjJiMTRjZjUwYmQifQ"
+          "resource": "repo://src/config.rs#L283-L306",
+          "version": "repo-lines-v1:sha256:337be3d271ffd1034a8dead0b8ab67544c72bc1e22f24bb99e564ca5bb9b1ffb:eyJzZWxlY3RlZExpbmVDb3VudCI6MjQsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6Ijc2NWZhNGU5MTRkN2UzOWE2MGJhZTc1M2NhMjE2YTg3NGE3NmZmZWY4NTc4OTY5NTZkNjg0MDM4YTY1MWJjZDAiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImViOWEwOGJjMjlmNmViZWJlOWE1ZjAxOTAzZDk4NDVkYmNiNTg0MmE0NTVmMDdkNWY0MDAyZGFjMzJhNWM2MDUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6Ijg4ZWYzM2E3NjQ3YTM0NDVkNzE1NDU4YmE2Yzc0OGY2NmIyYWZiNzYxYzEyODc4NWU3MzRjZjIyOThjOTdkMzUifQ"
         },
         {
-          "resource": "repo://src/config.rs#L69-L103",
-          "version": "repo-lines-v1:sha256:7c6f8495ff37fead6b29d635192080812a646802f43e26c9dbaa5406f2f4c18f:eyJzZWxlY3RlZExpbmVDb3VudCI6MzUsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjY2MWQ4OTAwZThkNDcyOWNhMDZlYTYzMjM0ZGI5OTMwMzljZGI5YzRhODA5OGE3ZDQzMmZiNzQ3ZDI5NDUzZDUiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjRlMzMzNjA1NjQyNmQ3MGE1ZDc5YjU3NWEwYTEyNDM5MzljZDE2Nzk2N2I5N2EyMDA3NGVjZGQxZjRiMjE0MDAiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjZlMWQ1YWEyZWYyYTBkODhkY2JlMWViZTI2YTg5OTdkOWVjODhjZGNmMmMzNDA1YzgwZjYwOTFmZmNhZDUzYmQifQ"
+          "resource": "repo://src/config.rs#L34-L43",
+          "version": "repo-lines-v1:sha256:db0845d61fe67c0b00b5070ab68f50571dffeed86fa0119d9004244edcab7733:eyJzZWxlY3RlZExpbmVDb3VudCI6MTAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjdkNWE4ZjI1MGMwMjJlOTMwNzUxZGFlMTg0YjBhZGMxNTUwYWRkNzVlNGJiOTUyMDBiNTE0NmY0NzQ5MjdlODMiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6ImQ1MDFkZTlkMjcyZmZkODVmYTk1OWMxN2JiMTdjYWZkMmJkYjM1YTgyMGQzNDA1NGRkOWQ4NThkNzUyMDMyODIiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6Ijc2YTNkY2UwNTMyZWI3NmI2M2EyNmViYzdlZmNkNWE3YTViYzFjMTFhOWFiZGRhM2Y3ZjJmNzhkODc1Y2Y3M2YiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjdlODA1ZjRiMzVlMzg3NmExMjg5OTE1MjgyYWRhNWNkMzNhNzdmNTMyYmYyOWMxOGM4OWRhMjJiMTRjZjUwYmQifQ"
         }
       ]
     },
@@ -67,7 +63,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#L187-L212",
+          "resource": "repo://src/config.rs#L185-L210",
           "version": "repo-lines-v1:sha256:98536507e8260febe788fbadf4507f155d98cae147436b79fb8b0f633fd9ecdf:eyJzZWxlY3RlZExpbmVDb3VudCI6MjYsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImE2NTEzYThiZDE0NzdlYTVhZTdhMWU5N2IzMjFhZWUxNmY1YjcwZmEyZDA3ZTMzMTAyYmNiY2M3MWE1N2JiMTEiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjRiNzAzOGM3MGY5ZDBmYzEzOTYwY2YyOTRkOTdhODRlOTUwMmNhZmI2Yzk1NjM2ODhiZjc4ZGQyNGY0NWE2YWUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjJkM2E4MDAwMjZjNmY2MDFiYmM4MDdlYWI0YmI1Nzk1ZTY4MjEzNGYxMWU3MTRmNzRkMWVlMzQzOTUxNjE4NDYifQ"
         }
       ]
@@ -77,7 +73,7 @@
       "statement": "An openai_base_url made only of slashes is rejected.",
       "evidence": [
         {
-          "resource": "repo://src/config.rs#L234-L241",
+          "resource": "repo://src/config.rs#L245-L252",
           "version": "repo-lines-v1:sha256:416d7587d25100ea2db4d28ac60e8ff2b5b07566b3bbad27544a561576d2d6d4:eyJzZWxlY3RlZExpbmVDb3VudCI6OCwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiZjg0NWUxMjk4NjExOTI3YTI5YjJhNjA0ZmZiMTBhOTcwMjc5ZjIwYjZhMjBiYTA4MGJiNDFmYWU1NGJkYmFmNiIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDEyY2EzNDVjY2Y3NWJmOWMwODA2YmNlNjk1YmU4ZGU4MDhiNzk5ODQyNTFhN2E1NGQyMDJjZjYxMDFkZDQ1MSIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiZmM2ZTJmOGQ3OGU4N2ViYmYxZTQwNWUyMmIxODU5MGViZjMzNzQ3OTY5MGVlNDVlZGIxNjMxZGVlYjJjZDQ0NiIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiZTM0ODM0OGE5MDUyNDI1NDNlZmRlZTgyZjg3NWRlMTk1NzU5Y2Q0MDkyODQ3ZDBlZWIyNzdmYWQ4M2Q3NzUxZiJ9"
         }
       ]
@@ -87,8 +83,8 @@
       "statement": "Config Debug masks the API key and FileConfig has no Debug implementation.",
       "evidence": [
         {
-          "resource": "repo://src/config.rs#L105-L160",
-          "version": "repo-lines-v1:sha256:5e6170855098f89234451aeac62ee28645a6864149176bea3af06a2c5f101537:eyJzZWxlY3RlZExpbmVDb3VudCI6NTYsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImQwOGE4NDgyODgzYmU5ZjVmMDIxNDY2ZTgyOGE2MjgwZTI5YTNiMjI0ZTQ4MTY4ZjZjMWQxMDY0MzI3YjlhMjYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjY0Zjc0NTFlMTZhYzBlYjU5ZWE3OGI4Nzk0OTdhMjVkNzM4NzUxYjYwZjE1ZmYyYjQ3ODBhM2RmMGYwYTIyNTIiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImFiNzFkMjFhZWI1ZGU3M2Q1YmI3YmU2NDg0YTMzODliNjY1YjdjMDc1YmM4MjlmMDIyYWE0ZDc1MjM2OTNhMTUifQ"
+          "resource": "repo://src/config.rs#L107-L158",
+          "version": "repo-lines-v1:sha256:9241fafa52c556df183667779add419c51d39295a9403897555c062e1ff441ea:eyJzZWxlY3RlZExpbmVDb3VudCI6NTIsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImQwOGE4NDgyODgzYmU5ZjVmMDIxNDY2ZTgyOGE2MjgwZTI5YTNiMjI0ZTQ4MTY4ZjZjMWQxMDY0MzI3YjlhMjYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjY0Zjc0NTFlMTZhYzBlYjU5ZWE3OGI4Nzk0OTdhMjVkNzM4NzUxYjYwZjE1ZmYyYjQ3ODBhM2RmMGYwYTIyNTIiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImFiNzFkMjFhZWI1ZGU3M2Q1YmI3YmU2NDg0YTMzODliNjY1YjdjMDc1YmM4MjlmMDIyYWE0ZDc1MjM2OTNhMTUifQ"
         }
       ]
     },
@@ -104,19 +100,15 @@
     },
     {
       "id": "claim_b8d35a34934748b7bba2314b03d93883",
-      "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.",
+      "statement": "reply_delay_seconds, catch_up_seconds and ai_retries are Option<u32> in the file so negative values are rejected, and 0 means answering at once, disabling the catch-up or disabling retrying.",
       "evidence": [
         {
-          "resource": "repo://src/config.rs#L121-L128",
-          "version": "repo-lines-v1:sha256:8b75e8e687adfeec46a668e0a24355f29c7e83198e5dd972c8716af7e123f9ae:eyJzZWxlY3RlZExpbmVDb3VudCI6OCwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiYTVlZGFiZTdmOTczYjIwMzkwNGU4MTg1MzlkNzExMTk3ZTQwMTc4N2NkODVhZDAyYzNkNjdkYWM1OTMyNzc3YSIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiYjBiOTRjODBmNjUxYzk0ZTBmZTBmZWE4ZDc2NGUyMWFiMjc3NWFhZTkwZTVkYzczN2NjYWVmMTA1NDhjYTIzMCIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiYjg3MzFkYzc5ZDhiMzFhYzFlODZjMDMyOTQ2NDY0MTJmNjY1YzM2ZGNmZmM0MTQ4ZDVlMGJlOTE5ODdkNGRjNyIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiYjRmOGYzYzg1N2I3MjNlMDgyNWQ1MDA5MzQzOGI5ZmYyMjY2MzAxY2I1NzA3MTk4ZTc2NGZlODI0OTZhYjMwYSJ9"
+          "resource": "repo://src/config.rs#L107-L130",
+          "version": "repo-lines-v1:sha256:88a2926a51f37f52593b1be029cb70e800f905c7073db0aa97177ad6d303faaf:eyJzZWxlY3RlZExpbmVDb3VudCI6MjQsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImQwOGE4NDgyODgzYmU5ZjVmMDIxNDY2ZTgyOGE2MjgwZTI5YTNiMjI0ZTQ4MTY4ZjZjMWQxMDY0MzI3YjlhMjYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjY0Zjc0NTFlMTZhYzBlYjU5ZWE3OGI4Nzk0OTdhMjVkNzM4NzUxYjYwZjE1ZmYyYjQ3ODBhM2RmMGYwYTIyNTIiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjNhN2ZlNzVmMzU5ZTMyZTFkYmE4YWFjMDY1ZmMzYWFhMjY4YzcxZDc0NjUyYjU2OWNkNTJhYjEzYmZlYmMwN2UifQ"
         },
         {
-          "resource": "repo://src/config.rs#L529-L547",
-          "version": "repo-lines-v1:sha256:b7c3b1c4f7fdf5b1a22d7cb0c229948c0d3aa1ae4712f0a8be1bf0100cf8fa85:eyJzZWxlY3RlZExpbmVDb3VudCI6MTksImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImRhOTVhYzZlMmVhOGY3NWFlNDRhZDI3NjY4ZTU1MzkwY2MzZmQ2ZjdjZjY0MmU4ODJjMWM4Nzc1ZjUxMjk2MDMiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjA1YTgyYmM5ZDU1NTVjOGUzY2Q0MTUzOTM2MmQwZWU4OTdkNTExN2U2MTRjZDkzN2JiOTliYWMwOGM0M2YyZWYiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImEwMGRiMDQwZjI5MDliZjk3OGZmMGE5NTZhNTVlODNjNjUzNDE4ZGU2YmI2OGY4MDQxZGE5NzZmOGQzZDIyNzAiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImRlMzMxZTU1MjViMTM5ODQzODA2NzZkN2MzNmNkYTBkMmNjMTgyZmMyYTE2N2QyZTVjOTU0NTE0YmI5ZTY5ZmEifQ"
-        },
-        {
-          "resource": "repo://src/config.rs#L7-L19",
-          "version": "repo-lines-v1:sha256:91944cf8df4c789256e6058f9182688671bf3be2314fcf2ad7893746cf2dc4eb:eyJzZWxlY3RlZExpbmVDb3VudCI6MTMsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImNkNDY5ZTYxYzVmYzhjZTc4ZGQzOWQ2ZWQyMTMyY2JlMDA0Mzk5NTlhYjc5MTY0ZTM4YjJhYzY5NDA3ZDU1NmMiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjAxNjUyNTRiZGUzY2I5ZjNkMjU1MDNkMGRlYzczZmZlOGY4MTcyMmUzNjc5Nzc4Y2NiMTRkNDA1M2MyMTJhYTQiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImQwOWMwYjg4Y2IwOTczNTY5NzIxYThlZDNjNjBhOTUzMDU1MTEwNjJjNGYzNmM2NjUzOTE1ZTU3ZGRmZTQ5NDUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6Ijg2MGZlMmRiYmY0ZjEzYjNkNDBjZDhmNjliMGQzY2Y5ZGM3ZjI4Y2U5N2FjZmZjNzFmODgzNWIxYmIyMGZhNmEifQ"
+          "resource": "repo://src/config.rs#L7-L17",
+          "version": "repo-lines-v1:sha256:92e165d4ca3482a66abeebff4316ae732db9bd09e242fa332b4d3ab62e4d7488:eyJzZWxlY3RlZExpbmVDb3VudCI6MTEsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjdlMzA5ZTQ5ODMzYTEwNTQ2YTMxZDhlMDFjZGNkMWRjMGJhMWQ3YzA5Yzg5MGZiMDhlODhjMDFiY2MzZDI1YWIiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjAxNjUyNTRiZGUzY2I5ZjNkMjU1MDNkMGRlYzczZmZlOGY4MTcyMmUzNjc5Nzc4Y2NiMTRkNDA1M2MyMTJhYTQiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6Ijg3ODQxYjgxNDUwYmZlOTMzYzA0ZWYyMzMyN2ZmODg5N2I0Njc1Njg4ODU4YmRkNzg5NWM5ZTQxNzIwMGE2ODAiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6Ijg2MGZlMmRiYmY0ZjEzYjNkNDBjZDhmNjliMGQzY2Y5ZGM3ZjI4Y2U5N2FjZmZjNzFmODgzNWIxYmIyMGZhNmEifQ"
         }
       ]
     },
@@ -125,12 +117,12 @@
       "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#L248-L257",
-          "version": "repo-lines-v1:sha256:ecd1d611f74d5bde7229623335cb8f8bf5cfff645aa0dccc5adb5732d9287806:eyJzZWxlY3RlZExpbmVDb3VudCI6MTAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjY1NThiY2FiNTVlMjRlNWYzNGUzYzg3ZWFkMGI1MmRiNDExOWRhOGM5MmI3MDJmYjkyZTg4YjgyNGYxZDllODAiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImM1YzIyY2NmNTE5YjEyMWM0MDBlNmQ4MTg3YTA4MDI5ZjE5MmE0ZmViMDU1NWYyZTBkYzJmYjg4OWY3NWEwZTYiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjJmNjljNjZiNTk4NDRmMjg4YzY3Yzg4NWY2NzQwNmEwNmExNjg4OWI4ZTc4NDMzNDZiMWUzNmRkNjRlYWIzNTUifQ"
+          "resource": "repo://src/ai.rs#L261-L280",
+          "version": "repo-lines-v1:sha256:80e196adb6578d005aa9692b9e78d664b43c74a606ded3b99a348545c412b065:eyJzZWxlY3RlZExpbmVDb3VudCI6MjAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjUxNzY0ZmUzOGM2MTVjODM3MzM2M2ZhM2U2OTVhNTQ3Y2M0MGM5MjhmZjIwYjE0ZTI3NGRjZjQxNDI5NjI5MjQiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6Ijk5OWJkZWZlZWFjYTYzNDBlMDI5MGRjODZlODg5Yzg5ZDVlMTg5YWE1YmE0NGI1ODViM2I3MDczNGY0YTYyZDgiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjEyYTMyMmRjMWM3OTk2OTEzNTU1MGI0YzRkMzYyMThiNTQ4ZDY2NjE4OWRmNDFkMWM1ODIyYjU1MmE3OTljNjMifQ"
         },
         {
-          "resource": "repo://src/ai.rs#L424-L516",
-          "version": "repo-lines-v1:sha256:89a3eb5bccc99aa1700ff566d228266bde9e75070cec6fc4ff8f961fd2cb30f7:eyJzZWxlY3RlZExpbmVDb3VudCI6OTMsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjNhZmRkOGE1NjBlNzQ4ZmFmNGRlZDhhYjJmOWU4NmUyMTU5NDA1YTAxMGUwYzE4YjI2M2NhNDE4MzAxMzFjZTciLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjcwZmRhNDBjNDBhZTM2Yzk5ODVhZWRjMDE5MDM3NDJmMTViYmY5ZDY4N2IxZjk1MTVmZDkwOWU2ZGUzY2E2ZTkiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjJjZTgwZDQ1MzU3ZWY2NWZiNjUzODgzZWEzOTU1NDMyMDMzN2Q0YjA3MzcxNzQ1ZWE0MGFkYzRmMGFjNTU0YTcifQ"
+          "resource": "repo://src/ai.rs#L473-L566",
+          "version": "repo-lines-v1:sha256:6449a0d503ec713ff2c8159e6629639a1ce13aacf31d4a8c178d630e10e727f4:eyJzZWxlY3RlZExpbmVDb3VudCI6OTQsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjNhZmRkOGE1NjBlNzQ4ZmFmNGRlZDhhYjJmOWU4NmUyMTU5NDA1YTAxMGUwYzE4YjI2M2NhNDE4MzAxMzFjZTciLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImRkNmI1NWM2ODQyOGJkOWU4ZDZiNGU0Y2YwYmJmYmI4Mjc4MWEzMjU4NjEwYWU5ZTAxMjI3NzA0NjM1MGQwN2IiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjJjZTgwZDQ1MzU3ZWY2NWZiNjUzODgzZWEzOTU1NDMyMDMzN2Q0YjA3MzcxNzQ1ZWE0MGFkYzRmMGFjNTU0YTcifQ"
         }
       ]
     },
@@ -139,32 +131,70 @@
       "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",
+          "resource": "repo://src/config.rs#L127-L129",
           "version": "repo-lines-v1:sha256:b4f8f3c857b723e0825d50093438b9ff2266301cb5707198e764fe82496ab30a:eyJzZWxlY3RlZExpbmVDb3VudCI6MywiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiNzEzN2YxMDljNWYyMDU1OGE3MzUwZDRlYjdkMmU2ZTllNDk1NzIwY2Y0YWQzZTk4NjZlY2E2NDAxNWU3MGJkYSIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiN2U0MTE5NDE4OWJhMWY1NzIyMjFiMjA3N2JmMDU4ZmExYTNkZTA3ODdkMzIzZWMyYWI0MGU3MmEyMTRiYmM3OCIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiMjgyZmY5MDM4ZGNhMmQwMGZjZjczNzgzOTQwNWI2NGI3NmVlODMyNzA4NTVlMDMyZDU5YTU4YzYyMTc4NDkwNSIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiOTY4MTE0ZGZhNDU1MWJiNmMwMDBmNGUyNTVjNjhiMDYyMmZjYmUyY2YyZTJiYzJmZThhODNmYTg2ZmY2OTk2MCJ9"
         },
         {
-          "resource": "repo://src/config.rs#L243-L252",
+          "resource": "repo://src/config.rs#L254-L263",
           "version": "repo-lines-v1:sha256:6d490312b71acd64e2e0d7d854caad2acb8ba41147fcb3bcf92195ba26fc850e:eyJzZWxlY3RlZExpbmVDb3VudCI6MTAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjM3NGJkYjgxZDEzY2MxOTQwZWE0Y2EzNTQ4NzZiODhiYmEyOWYxMmIzNTIzZTE1M2VlZDZiOGFlNWM4YjllYTciLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImEzMjJlODc4N2I1NGUwYzg5MmMxMzExNmFmOTM5ZjY5YTZkM2VjNzE2ZTMzMWY3N2JkN2E3OTFmYzAwMjFhOWYiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImUzNDgzNDhhOTA1MjQyNTQzZWZkZWU4MmY4NzVkZTE5NTc1OWNkNDA5Mjg0N2QwZWViMjc3ZmFkODNkNzc1MWYifQ"
         },
         {
-          "resource": "repo://src/config.rs#L451-L475",
+          "resource": "repo://src/config.rs#L504-L528",
           "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.",
+      "statement": "config.toml.global.template lists every supported key with its default value, leaves web_port commented out and points to SOUL.md for the system prompt.",
       "evidence": [
         {
           "resource": "repo://config.toml.global.template",
-          "version": "repo-file-v1:sha256:3b36a2e26714ee5ee10f7f4facca5cf94c8dcced7dd757911044af3066ba6070"
+          "version": "repo-file-v1:sha256:0ecec1d521cb1e678f5b7c0baf141bd90137ab7d2b436941b35aaf71c9ca5004"
+        }
+      ]
+    },
+    {
+      "id": "claim_311d5db128b543a8b8bb49446e518e12",
+      "statement": "SOUL.md and vision.md are read as written with only a leading UTF-8 BOM removed; a missing or blank file yields no prompt, while an unreadable or non-UTF-8 file is an error naming the path.",
+      "evidence": [
+        {
+          "resource": "repo://src/config.rs#L212-L223",
+          "version": "repo-lines-v1:sha256:9bb1147f77fa228eae840306e4371637700927c4c2a2aa01a36e028141fc5351:eyJzZWxlY3RlZExpbmVDb3VudCI6MTIsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6Ijc1ODlmMTZlMTM4ZjMwMTM1ZjE3ZGNkNWQ0OTVhOGM0NzhkOTE4MGI3NzA4YmNmZjBlYzdlN2FjNDA5YTQ4YjIiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjJjNjc1NTVkMWRiNmUxYTQwOTI2MzU5ZmVhYTMyODBlNDcyNTdmMGQ4NzQxN2Y4ZmI1MzBiMWM4ZjhmNmZkYzQiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjJkM2E4MDAwMjZjNmY2MDFiYmM4MDdlYWI0YmI1Nzk1ZTY4MjEzNGYxMWU3MTRmNzRkMWVlMzQzOTUxNjE4NDYifQ"
+        },
+        {
+          "resource": "repo://src/config.rs#L265-L281",
+          "version": "repo-lines-v1:sha256:4ade9876bd8e1653384dcebde47d413e1aab36995b53055ab5441d82e8b28290:eyJzZWxlY3RlZExpbmVDb3VudCI6MTcsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImQ4MDc5NGE2MDA2MzNhZTY5ZGFlMWYzOThjYTk3ZTc5Yzg3NDc5ZDFjNjE3MDg0ZTRkMTgyODZmMTlkODFjMTUiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjgzZDI4NDFkZmUxZTBkZDIxMmQwYzE1ZDAxZTVhYzkyMTBmNTgwMGI4YmRmZTk5M2FkZDAwYmQ5NGYwMTRkNmMiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImU2YzkxMzJjZTZhZmY4ZmJjMmFmMzRiY2JmYWE1ODVhN2RmMWUxODViYjc5OTFlY2I5YzgxYTUxODg3NWUyOGUifQ"
+        }
+      ]
+    },
+    {
+      "id": "claim_5b5fea341d19402280a2707c4ca82af7",
+      "statement": "The legacy config.toml keys system_prompt, cooldown_seconds and group_cooldown_seconds are ignored rather than rejected.",
+      "evidence": [
+        {
+          "resource": "repo://src/config.rs#L107-L130",
+          "version": "repo-lines-v1:sha256:88a2926a51f37f52593b1be029cb70e800f905c7073db0aa97177ad6d303faaf:eyJzZWxlY3RlZExpbmVDb3VudCI6MjQsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImQwOGE4NDgyODgzYmU5ZjVmMDIxNDY2ZTgyOGE2MjgwZTI5YTNiMjI0ZTQ4MTY4ZjZjMWQxMDY0MzI3YjlhMjYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjY0Zjc0NTFlMTZhYzBlYjU5ZWE3OGI4Nzk0OTdhMjVkNzM4NzUxYjYwZjE1ZmYyYjQ3ODBhM2RmMGYwYTIyNTIiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjNhN2ZlNzVmMzU5ZTMyZTFkYmE4YWFjMDY1ZmMzYWFhMjY4YzcxZDc0NjUyYjU2OWNkNTJhYjEzYmZlYmMwN2UifQ"
+        },
+        {
+          "resource": "repo://src/config.rs#L283-L306",
+          "version": "repo-lines-v1:sha256:337be3d271ffd1034a8dead0b8ab67544c72bc1e22f24bb99e564ca5bb9b1ffb:eyJzZWxlY3RlZExpbmVDb3VudCI6MjQsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6Ijc2NWZhNGU5MTRkN2UzOWE2MGJhZTc1M2NhMjE2YTg3NGE3NmZmZWY4NTc4OTY5NTZkNjg0MDM4YTY1MWJjZDAiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImViOWEwOGJjMjlmNmViZWJlOWE1ZjAxOTAzZDk4NDVkYmNiNTg0MmE0NTVmMDdkNWY0MDAyZGFjMzJhNWM2MDUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6Ijg4ZWYzM2E3NjQ3YTM0NDVkNzE1NDU4YmE2Yzc0OGY2NmIyYWZiNzYxYzEyODc4NWU3MzRjZjIyOThjOTdkMzUifQ"
+        }
+      ]
+    },
+    {
+      "id": "claim_ff1c72aeb4f74a44acd82adc2ad022e7",
+      "statement": ".gitignore ignores any SOUL.md and *.log files in addition to the root-level config.toml.",
+      "evidence": [
+        {
+          "resource": "repo://.gitignore",
+          "version": "repo-file-v1:sha256:04dd19185e097f660d8472fbe810a075adcf70f7e81924e24649f934db1b4861"
         }
       ]
     }
   ],
   "verification": {
     "by": "openwiki/0.7.2",
-    "at": "2026-10-20T20:27:06.515Z"
+    "at": "2026-10-21T21:49:12.291Z"
   }
 }

+ 25 - 11
openwiki/.claims/quickstart.json

@@ -1,6 +1,6 @@
 {
   "schemaVersion": 1,
-  "pageVersion": "sha256:f8e55a63196e59cd28b6114d94f0faa4326a003dbe6b743831920c4b4a7fb4c9",
+  "pageVersion": "sha256:0904a154437de3ef07d0a913900c796b3fc862d2fcd254438da8ab3c23dcf515",
   "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#L166-L170",
+          "resource": "repo://src/config.rs#L164-L168",
           "version": "repo-lines-v1:sha256:dd4141940976146e96656f2de294f92e1275a16efba97aa2c7318676f34bdfa4:eyJzZWxlY3RlZExpbmVDb3VudCI6NSwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiMzVhMDYyMDkxMmIzMTUwZDZlYmQwNDQyOTUxZTdjNWEwODlmODMwMzYzNmJjODZiNmQxOWRmN2U3YTFmMWEzZCIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDEyY2EzNDVjY2Y3NWJmOWMwODA2YmNlNjk1YmU4ZGU4MDhiNzk5ODQyNTFhN2E1NGQyMDJjZjYxMDFkZDQ1MSIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiNGU1ZGI4YzkwMjczZTAyOTc3MTY3ZWMwNDM0N2M0MWQ5MmE1MTE4NDMxMTA0M2UyMTU2YmM4MGFiYjkwMWQ0NyIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiMDY4YzgxNzM5NzQwNTE2YzI4OWQ4ZWRlZWM0N2IwMjcxMGFmOWVmNWM4MjE4M2NhNWQwMTc4MWVhMjI1YjU1MCJ9"
         },
         {
-          "resource": "repo://src/config.rs#L47-L56",
+          "resource": "repo://src/config.rs#L45-L54",
           "version": "repo-lines-v1:sha256:6624d58d13d3982b8829e7b759fc7c36a643278f2d243526a887428e34a0d469:eyJzZWxlY3RlZExpbmVDb3VudCI6MTAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImU1MDllZGUxYTUxZmE5MGM4ZmQ0YTNkODdmYjJhMGU5OTMwNzhkMjc0MzU1NGFjMGM4MmIzMTdmNzQ0OTY5ZmYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjFmNzZjMzcwNTRkNWY0YzFhNWZhZDE4YzFlYjI0NTNmODIxNTI0NDQ2M2ZkNjA4MzIwZDEzZDYxNmFlYmVhNmQiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjMwY2VjY2EzYjg3NDgwNDU3ZGVmZTlkOWVkY2I1N2VmMDdlN2ZlZTE2MTljZjdmYWEyNzM0Y2Y2M2Q5MzIxYjEiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjQ1N2RlMDJkYWE2M2RmYzAxZmQxNjkyNDc1ODY2ODA3OGM5NmJlMGM1ODkyZjQ4M2UzMDdmNjAyYTU0MDRkMzQifQ"
         }
       ]
     },
     {
       "id": "claim_f1a67c370b204d5484f31751987d20d9",
-      "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.",
+      "statement": "Unless --version or -V is given, 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#L30-L54",
-          "version": "repo-lines-v1:sha256:78c36720946a6da37fed1551aa53c38d6a246801eca122beca85edd71edb50f1:eyJzZWxlY3RlZExpbmVDb3VudCI6MjUsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImExZDA2ZTA0OGNkMjcyNzM4ZDgzN2IwYzBlYzZhYWIwYjAzMTQ1N2U0NzVmZDQ5YWJiY2JhZmM1ZjczYzAyMGQiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImZhNDUwZGE5MjlmMjA2MmRjNmMxMzBhZTc4MDU2NWE2MGUyYTkyZmI4YjYzNTA5MTZkNTdiM2EwYzA1MDM4MTkiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjcyOGNjMGI0ZGIzNjFhYzMxMDc4YmMxOTZiZDY1MjkxMzVhNmFmYzQ1ZWNhZTkyMWY2MTcxMjI1ZTJmNGNkOTgifQ"
+          "resource": "repo://src/main.rs#L36-L64",
+          "version": "repo-lines-v1:sha256:2f5e5e305de372b540e8f2ab32af7faeb163b55a1cea3b4f31696097c921a6a7:eyJzZWxlY3RlZExpbmVDb3VudCI6MjksImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImExZDA2ZTA0OGNkMjcyNzM4ZDgzN2IwYzBlYzZhYWIwYjAzMTQ1N2U0NzVmZDQ5YWJiY2JhZmM1ZjczYzAyMGQiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6Ijk3YjIxYjgxYWE2YTcxZmY5NGYxNmE2NGVhMThkMjlkODhiYTE5YzkyYjM2MjUzMDhlNmU4NDM2MTkyNzM0MmQiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjAxZmI2ZDM1MThiYTRjMzUwOWRiMWM5MTAyMTk2NjQwMzI4ZjgzM2M3MDJmMzgwNmNmZjFlZGI5MzU4Y2Q5YTgifQ"
         }
       ]
     },
@@ -61,11 +61,11 @@
       "statement": "The AI autoreply can be enabled for private chats and groups but not for broadcasts or newsletters.",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L1127-L1140",
+          "resource": "repo://src/app.rs#L1403-L1416",
           "version": "repo-lines-v1:sha256:76ea2311dd543d33bf1c52b4cee8997184cb67643a8ba3b2f715abf2fea34667:eyJzZWxlY3RlZExpbmVDb3VudCI6MTQsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjA0NDI0MGFiOGNkMTM1NDE2ZTQ5MjMyYTEzMGIzYzlkYmI2NWZmYzg0OGNjYzY3OWJhYTQxYWQzNjY4ZTA3MjQiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjE5ZTc2MzU1Y2YxMTk5NDgwYzNmOTk4MzZiMTQ4NmM4NDFkODkxZmFhZDkwNGM2MDhlMDkzMDIyZDM3NTdhZWUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6Ijg5NGUwY2FkODFjNTBiOWY4MWE5ODI2ZjA1YzUyZjY0MGE0ZGM3NzdiYWQyNGM2MGEwMjQ5NzhlZWJmMWRjMGMifQ"
         },
         {
-          "resource": "repo://src/autoreply.rs#L66-L97",
+          "resource": "repo://src/autoreply.rs#L76-L107",
           "version": "repo-lines-v1:sha256:0ec556787987c44ceb565f6d300f0cf6acd13afd25b5e78a34de780c044b82fc:eyJzZWxlY3RlZExpbmVDb3VudCI6MzIsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjEzNDhjYzQxMDQ1OTNmYjgwNTEzZjBiMmUyNDQwYWYwZjc0M2Y5Yzk5Nzk0YWQzZGNiZGQ4ZjY0YjM4ZDBhNmMiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjFmOThmZjFmNmFmYzAyNDYzN2IxMzgyOWJjNmFlMGJkYjA0NmU3YWQwMjQ0MWZmYTAzZDE4YmI4NzI0NmE5OTQiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjZhYzE3ZjhmYjIwNjZkNTU5ZDU2MmYzNTkwODY4ZDRlNTMwYjIwZmQzNzdkNDkwZTJmZjE4Njc3Y2M3YTBlZTkifQ"
         }
       ]
@@ -75,18 +75,32 @@
       "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",
+          "resource": "repo://src/app.rs#L23-L30",
           "version": "repo-lines-v1:sha256:1b77b4b729c472be09a45c5473cf8594fc6554a18cc8f686ece6245ff2aa26ec:eyJzZWxlY3RlZExpbmVDb3VudCI6OCwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiYmQxOTM5NWQwMDUxZDZhMDgzMGQyZDk5ZmE1OThkNTg3YTI5ZmQ1MDQ5OTA0ZDU5Y2I1MjZiZDdjNzg4MGIwZSIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiYWY2MjM3MDg5YjkxZDEwN2I2NjQ1Nzk3M2E4YmEyYTcxZDg3MDQ0ZWE5MWU5MThjZGUyMDVmODZhY2Q0NmM5MyIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiMWVmZGQwMDhhMTYzNjZhM2YxN2I2NjUzYjI3NzIxOGRjNDY1NGIwYWNhMTkwYjViYWJkNzA2YjIzYjEwNDA3NyIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiZjM1MGFjYmZhYzdlNDEzZmFkNWYxMzk0NTQwYWE3MWM3YWY3ZWZmMDA4MDliYmMxYTlhNzQxZGRhNDFiOGYwZCJ9"
         },
         {
-          "resource": "repo://src/app.rs#L573-L603",
+          "resource": "repo://src/app.rs#L774-L804",
           "version": "repo-lines-v1:sha256:37edcb8b1691dbaa467f7d59b80cdc3781376ce6ce1fc1783f167c52ae5ce169:eyJzZWxlY3RlZExpbmVDb3VudCI6MzEsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjNmODVjMjY1ZjBmMjNkOTA0NDcwMjYxNWZiM2NkMzQ2ODMxYTFiZWNiOTRlYzNkODYyNDYxYzFkNWEwMGQzNDUiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjE5OTRiMGIxZDNhNDA1MzRjYTFiZTVmNDdiNTRlNWY3ZDg3ZTZlMGMxMWE3ZjU5ZjA0NGU2ZGJmNTkyYzNlZDYiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6Ijk2ZWU5ZDllODg2ODFmN2JmM2M4ZmZiZjEzZDgyODk2ODM4MGY2Y2E0M2Y1ZGE4ODI5MmU1MjY3ZDRjMDhlOWYifQ"
         }
       ]
+    },
+    {
+      "id": "claim_463678f8d2c94b51b6b05219b319a405",
+      "statement": "The system prompt is read from SOUL.md in the data directory rather than from config.toml, and the optional vision.md replaces the built-in image description request.",
+      "evidence": [
+        {
+          "resource": "repo://src/config.rs#L19-L25",
+          "version": "repo-lines-v1:sha256:17242e32826a755038c29fdc065c9833b26b2b878da0847d6ff0dee1f741c6ac:eyJzZWxlY3RlZExpbmVDb3VudCI6NywiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiMjhhODQwNDU2NTU5OWJhNzUzM2Q2NWYzYTBjODNmMzE3NzRlM2MxYjg2N2EzNzA2ZjViOTg3OWNkNTg4YTMyYyIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiYzJlMmQ4YzljYzcyZTM4MzI0YmRjOGY4OTJmZjc0N2I1NWMyY2Q1ZGQyOGJhYWExN2Q2MTQ1YzAxYjkyODQ1YyIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiMDg2ZGU3NmMzYjVlNGIwN2Q2MmNkZTNjNDY1ZDEwN2QzNmE4NDEyMjE2ZThlMWFhMjZjMjE2ZmFmMWE4ZDY5OSIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiNzEzMTUyNmUyZDdlYWQwODA2YmQ3MmViNGMwYmUyMTU2MjhlZmIwYzMyNzc5MzQ2NWRiMzU2ZDY1NDg0MDM3ZiJ9"
+        },
+        {
+          "resource": "repo://src/config.rs#L265-L281",
+          "version": "repo-lines-v1:sha256:4ade9876bd8e1653384dcebde47d413e1aab36995b53055ab5441d82e8b28290:eyJzZWxlY3RlZExpbmVDb3VudCI6MTcsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImQ4MDc5NGE2MDA2MzNhZTY5ZGFlMWYzOThjYTk3ZTc5Yzg3NDc5ZDFjNjE3MDg0ZTRkMTgyODZmMTlkODFjMTUiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjgzZDI4NDFkZmUxZTBkZDIxMmQwYzE1ZDAxZTVhYzkyMTBmNTgwMGI4YmRmZTk5M2FkZDAwYmQ5NGYwMTRkNmMiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImU2YzkxMzJjZTZhZmY4ZmJjMmFmMzRiY2JmYWE1ODVhN2RmMWUxODViYjc5OTFlY2I5YzgxYTUxODg3NWUyOGUifQ"
+        }
+      ]
     }
   ],
   "verification": {
     "by": "openwiki/0.7.2",
-    "at": "2026-10-20T20:27:06.515Z"
+    "at": "2026-10-21T21:49:12.291Z"
   }
 }

+ 55 - 37
openwiki/.claims/testing/unit-tests.json

@@ -1,37 +1,37 @@
 {
   "schemaVersion": 1,
-  "pageVersion": "sha256:a01bacd1db7d8ffec9c6c644ce544b4408c7a27be2ca73f04ff774e65a7810cb",
+  "pageVersion": "sha256:10bcb258ed8fc94585fa952d923b1602f7daafeeed0fd6693ff6c44f40ffd393",
   "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#L526-L527",
+          "resource": "repo://src/ai.rs#L576-L577",
           "version": "repo-lines-v1:sha256:03c5b4b27fb001def5489d5e0264e20f2538b8214c3df8ceda3354aaee53c2c8:eyJzZWxlY3RlZExpbmVDb3VudCI6MiwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiMjVjZjVmYzIzZjJhNmQyM2MzZWE1MWU3MmY1ZTg5NmM3NTU1ZmJhYTA1Y2Q1YjFlYTE4MmJmOTYxOGE4ZTgxYiIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiMWUwYWYzZmQ0YTRjM2ExZDQ3YzdmNDcwZWQwZDg2ZDkwZDlkZTIxMGE1ZWIxNDU1ZmJkOTQ0ODJhNDc2NWI3MiIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiZWRjODMxMjQ5NjYxNGM2YzllMTE0NTc0YTc5ZDkxODU3MDM0YmJlM2IzMDk5MmYyNzYwZjcxNjY0ZjAwNWM2OSIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiOTM0OTA4YTE2NjZlZThkZWFiMmIyYTc2NjdjODQ0OGZiYjRkNDZlMzJhYjZjNTY4YmI3MjMyMGU1ZWJmMDlmMyJ9"
         },
         {
-          "resource": "repo://src/app.rs#L1167-L1168",
+          "resource": "repo://src/app.rs#L1443-L1444",
           "version": "repo-lines-v1:sha256:03c5b4b27fb001def5489d5e0264e20f2538b8214c3df8ceda3354aaee53c2c8:eyJzZWxlY3RlZExpbmVDb3VudCI6MiwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiMjVjZjVmYzIzZjJhNmQyM2MzZWE1MWU3MmY1ZTg5NmM3NTU1ZmJhYTA1Y2Q1YjFlYTE4MmJmOTYxOGE4ZTgxYiIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiMWUwYWYzZmQ0YTRjM2ExZDQ3YzdmNDcwZWQwZDg2ZDkwZDlkZTIxMGE1ZWIxNDU1ZmJkOTQ0ODJhNDc2NWI3MiIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiZjY5NGY2NjJmYWIyMzFlOGQ4ZGIwOTRiNTU3OTgxNWYwOTlkYTQwZGU3ZThiOTY1YWUwNDU2YWRjMDQ5YjM4NiIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiOGJlOTk2NWNlNGEzMmY5N2Y5ZjMzYzU1MzhjZWY5ZDE0Njc4YjE0NGQ4ODg1ZDY2Mjk4YTZkZTJjODg3NTZlMSJ9"
         },
         {
-          "resource": "repo://src/autoreply.rs#L528-L529",
+          "resource": "repo://src/autoreply.rs#L505-L506",
           "version": "repo-lines-v1:sha256:03c5b4b27fb001def5489d5e0264e20f2538b8214c3df8ceda3354aaee53c2c8:eyJzZWxlY3RlZExpbmVDb3VudCI6MiwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiMjVjZjVmYzIzZjJhNmQyM2MzZWE1MWU3MmY1ZTg5NmM3NTU1ZmJhYTA1Y2Q1YjFlYTE4MmJmOTYxOGE4ZTgxYiIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiMWUwYWYzZmQ0YTRjM2ExZDQ3YzdmNDcwZWQwZDg2ZDkwZDlkZTIxMGE1ZWIxNDU1ZmJkOTQ0ODJhNDc2NWI3MiIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiOTMyZjlmMDdlZDQ0ZTc0ZjliN2Y1ZGEzNTY1NjIxZjExNGRiNzdlZGY1NDc2NzZjNjZiNzRjMWMwNzQzN2MyMSIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiN2M0NzcyZjY4MTNiYmFmYmNhYjU4MWZlN2JiZmIwOTI4NDRlMGZhOWU3MjQwNmMxNzNjNmYyNTMxMjgyMTZmMSJ9"
         },
         {
-          "resource": "repo://src/config.rs#L323-L324",
+          "resource": "repo://src/config.rs#L342-L343",
           "version": "repo-lines-v1:sha256:03c5b4b27fb001def5489d5e0264e20f2538b8214c3df8ceda3354aaee53c2c8:eyJzZWxlY3RlZExpbmVDb3VudCI6MiwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiMjVjZjVmYzIzZjJhNmQyM2MzZWE1MWU3MmY1ZTg5NmM3NTU1ZmJhYTA1Y2Q1YjFlYTE4MmJmOTYxOGE4ZTgxYiIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiMWUwYWYzZmQ0YTRjM2ExZDQ3YzdmNDcwZWQwZDg2ZDkwZDlkZTIxMGE1ZWIxNDU1ZmJkOTQ0ODJhNDc2NWI3MiIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiMTllNzYzNTVjZjExOTk0ODBjM2Y5OTgzNmIxNDg2Yzg0MWQ4OTFmYWFkOTA0YzYwOGUwOTMwMjJkMzc1N2FlZSIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiNGEwYTUzOWYxODQ2ODA0NzgzMDRlNjEzNTEzNGQyM2FhMjE2ZmZkZTBmMTYwMzJmYTZkNzYyOGQ0MzIxNzFhNSJ9"
         },
         {
-          "resource": "repo://src/history.rs#L481-L482",
+          "resource": "repo://src/history.rs#L543-L544",
           "version": "repo-lines-v1:sha256:03c5b4b27fb001def5489d5e0264e20f2538b8214c3df8ceda3354aaee53c2c8:eyJzZWxlY3RlZExpbmVDb3VudCI6MiwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiMjVjZjVmYzIzZjJhNmQyM2MzZWE1MWU3MmY1ZTg5NmM3NTU1ZmJhYTA1Y2Q1YjFlYTE4MmJmOTYxOGE4ZTgxYiIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiMWUwYWYzZmQ0YTRjM2ExZDQ3YzdmNDcwZWQwZDg2ZDkwZDlkZTIxMGE1ZWIxNDU1ZmJkOTQ0ODJhNDc2NWI3MiIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiNjA2M2M0ZDFhZjhjZDg5YmIwYmRkNzY1N2ZmY2JmNjgwNmE2MTBkNzlkNmM4ZDRjMzA3YzI0MWRmMzkwMDUyMiIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiMjgzMTBhMmUzZTliNGU0MTRjY2FiZmY2ZTdkMTc0YjgwMzVlNmZjZTRmODVmMTdjZTA4MTNmOWZlZTkzOTYxZiJ9"
         },
         {
-          "resource": "repo://src/ui.rs#L558-L559",
+          "resource": "repo://src/ui.rs#L575-L576",
           "version": "repo-lines-v1:sha256:03c5b4b27fb001def5489d5e0264e20f2538b8214c3df8ceda3354aaee53c2c8:eyJzZWxlY3RlZExpbmVDb3VudCI6MiwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiMjVjZjVmYzIzZjJhNmQyM2MzZWE1MWU3MmY1ZTg5NmM3NTU1ZmJhYTA1Y2Q1YjFlYTE4MmJmOTYxOGE4ZTgxYiIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiMWUwYWYzZmQ0YTRjM2ExZDQ3YzdmNDcwZWQwZDg2ZDkwZDlkZTIxMGE1ZWIxNDU1ZmJkOTQ0ODJhNDc2NWI3MiIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiMzViMjE2ODczZmZmNmEwNTc3MDU5NWM1OWNhZDNlNmE5NzM1MmJiNDE3ZDYwYzc2MmVhNDM4MWYxOTkwMjg3OSIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiNDVlYjIxMDg3NDU0NjZhOTVjZjg5YjVhZjdmMDZhMDA2NGU0OGRiMDVkMmMyOWU5Y2YzZDZkMGQxNmY3MjY5ZiJ9"
         },
         {
-          "resource": "repo://src/whatsapp.rs#L1005-L1006",
+          "resource": "repo://src/whatsapp.rs#L965-L966",
           "version": "repo-lines-v1:sha256:03c5b4b27fb001def5489d5e0264e20f2538b8214c3df8ceda3354aaee53c2c8:eyJzZWxlY3RlZExpbmVDb3VudCI6MiwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiMjVjZjVmYzIzZjJhNmQyM2MzZWE1MWU3MmY1ZTg5NmM3NTU1ZmJhYTA1Y2Q1YjFlYTE4MmJmOTYxOGE4ZTgxYiIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiMWUwYWYzZmQ0YTRjM2ExZDQ3YzdmNDcwZWQwZDg2ZDkwZDlkZTIxMGE1ZWIxNDU1ZmJkOTQ0ODJhNDc2NWI3MiIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiZDZkMTNkYTUzZGRmNTRlMDlhOWZiMzc3ODc0MTRhOTYxNDZkNDcxYzVlYmM2Y2UyNmNmOWEzODRmMTg3NzhmMSIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiMWU1MWM0MjBiN2E3NDU0MmZlNGYwMjY4Yzk0YTYwZjRmYjAwZTdhODI0NmFhNTlkNzQ0NTkzYmY3OGUyN2M5MCJ9"
         }
       ]
@@ -48,31 +48,31 @@
     },
     {
       "id": "claim_0bfd5fc27a434254a016373d3bb60e52",
-      "statement": "autoreply tests cover private, group and self-chat detection, should_reply rules including backlog, placeholders, private and group cooldowns and addressed messages, deferral and pending logic, catch-up rules, context_start and build_turns behaviour including group sender prefixes.",
+      "statement": "autoreply tests cover private, group and self-chat detection, should_schedule rules including backlog, placeholders and pending chats, answered_by_owner, catch-up rules, context_start, mention marking and build_turns behaviour including stored image descriptions and group sender prefixes.",
       "evidence": [
         {
-          "resource": "repo://src/autoreply.rs#L528-L1279",
-          "version": "repo-lines-v1:sha256:d591faa1c1e584a76e3e96176666068ce2c59a3d79328f8ff996a71b0abcdd6c:eyJzZWxlY3RlZExpbmVDb3VudCI6NzUyLCJmaXJzdFNlbGVjdGVkTGluZUhhc2giOiIyNWNmNWZjMjNmMmE2ZDIzYzNlYTUxZTcyZjVlODk2Yzc1NTVmYmFhMDVjZDViMWVhMTgyYmY5NjE4YThlODFiIiwibGFzdFNlbGVjdGVkTGluZUhhc2giOiI0MTJjYTM0NWNjZjc1YmY5YzA4MDZiY2U2OTViZThkZTgwOGI3OTk4NDI1MWE3YTU0ZDIwMmNmNjEwMWRkNDUxIiwicHJlY2VkaW5nQ29udGV4dExpbmVDb3VudCI6MywicHJlY2VkaW5nQ29udGV4dEhhc2giOiI5MzJmOWYwN2VkNDRlNzRmOWI3ZjVkYTM1NjU2MjFmMTE0ZGI3N2VkZjU0NzY3NmM2NmI3NGMxYzA3NDM3YzIxIiwiZm9sbG93aW5nQ29udGV4dExpbmVDb3VudCI6MCwiZm9sbG93aW5nQ29udGV4dEhhc2giOiJlM2IwYzQ0Mjk4ZmMxYzE0OWFmYmY0Yzg5OTZmYjkyNDI3YWU0MWU0NjQ5YjkzNGNhNDk1OTkxYjc4NTJiODU1In0"
+          "resource": "repo://src/autoreply.rs#L505-L1093",
+          "version": "repo-lines-v1:sha256:73d6683e98d1a479be5b3c5c82533ad41e429042edcbbc8f6c7d722a1eca2628:eyJzZWxlY3RlZExpbmVDb3VudCI6NTg5LCJmaXJzdFNlbGVjdGVkTGluZUhhc2giOiIyNWNmNWZjMjNmMmE2ZDIzYzNlYTUxZTcyZjVlODk2Yzc1NTVmYmFhMDVjZDViMWVhMTgyYmY5NjE4YThlODFiIiwibGFzdFNlbGVjdGVkTGluZUhhc2giOiI0MTJjYTM0NWNjZjc1YmY5YzA4MDZiY2U2OTViZThkZTgwOGI3OTk4NDI1MWE3YTU0ZDIwMmNmNjEwMWRkNDUxIiwicHJlY2VkaW5nQ29udGV4dExpbmVDb3VudCI6MywicHJlY2VkaW5nQ29udGV4dEhhc2giOiI5MzJmOWYwN2VkNDRlNzRmOWI3ZjVkYTM1NjU2MjFmMTE0ZGI3N2VkZjU0NzY3NmM2NmI3NGMxYzA3NDM3YzIxIiwiZm9sbG93aW5nQ29udGV4dExpbmVDb3VudCI6MCwiZm9sbG93aW5nQ29udGV4dEhhc2giOiJlM2IwYzQ0Mjk4ZmMxYzE0OWFmYmY0Yzg5OTZmYjkyNDI3YWU0MWU0NjQ5YjkzNGNhNDk1OTkxYjc4NTJiODU1In0"
         }
       ]
     },
     {
       "id": "claim_879517863bdd4ae5b7cc952b3e39e8a4",
-      "statement": "history tests cover deduplication, ordering, messages_since, chat names, per-chat and global autoreply flags, chat deletion, contacts and on-disk opening.",
+      "statement": "history tests cover deduplication, ordering, messages_since, chat names, per-chat and global autoreply flags, message_exists, image descriptions, chat deletion, contacts and on-disk opening.",
       "evidence": [
         {
-          "resource": "repo://src/history.rs#L481-L763",
-          "version": "repo-lines-v1:sha256:1d69fc86c05572c4af6df08c56af463f1a0de79555132c853c3e8b7c07eab17d:eyJzZWxlY3RlZExpbmVDb3VudCI6MjgzLCJmaXJzdFNlbGVjdGVkTGluZUhhc2giOiIyNWNmNWZjMjNmMmE2ZDIzYzNlYTUxZTcyZjVlODk2Yzc1NTVmYmFhMDVjZDViMWVhMTgyYmY5NjE4YThlODFiIiwibGFzdFNlbGVjdGVkTGluZUhhc2giOiI0MTJjYTM0NWNjZjc1YmY5YzA4MDZiY2U2OTViZThkZTgwOGI3OTk4NDI1MWE3YTU0ZDIwMmNmNjEwMWRkNDUxIiwicHJlY2VkaW5nQ29udGV4dExpbmVDb3VudCI6MywicHJlY2VkaW5nQ29udGV4dEhhc2giOiI2MDYzYzRkMWFmOGNkODliYjBiZGQ3NjU3ZmZjYmY2ODA2YTYxMGQ3OWQ2YzhkNGMzMDdjMjQxZGYzOTAwNTIyIiwiZm9sbG93aW5nQ29udGV4dExpbmVDb3VudCI6MCwiZm9sbG93aW5nQ29udGV4dEhhc2giOiJlM2IwYzQ0Mjk4ZmMxYzE0OWFmYmY0Yzg5OTZmYjkyNDI3YWU0MWU0NjQ5YjkzNGNhNDk1OTkxYjc4NTJiODU1In0"
+          "resource": "repo://src/history.rs#L543-L852",
+          "version": "repo-lines-v1:sha256:a4d3c1973b9e386233ba1511ab6e3adcf322e06bb30f97f8fb8f6a96258e6360:eyJzZWxlY3RlZExpbmVDb3VudCI6MzEwLCJmaXJzdFNlbGVjdGVkTGluZUhhc2giOiIyNWNmNWZjMjNmMmE2ZDIzYzNlYTUxZTcyZjVlODk2Yzc1NTVmYmFhMDVjZDViMWVhMTgyYmY5NjE4YThlODFiIiwibGFzdFNlbGVjdGVkTGluZUhhc2giOiI0MTJjYTM0NWNjZjc1YmY5YzA4MDZiY2U2OTViZThkZTgwOGI3OTk4NDI1MWE3YTU0ZDIwMmNmNjEwMWRkNDUxIiwicHJlY2VkaW5nQ29udGV4dExpbmVDb3VudCI6MywicHJlY2VkaW5nQ29udGV4dEhhc2giOiI2MDYzYzRkMWFmOGNkODliYjBiZGQ3NjU3ZmZjYmY2ODA2YTYxMGQ3OWQ2YzhkNGMzMDdjMjQxZGYzOTAwNTIyIiwiZm9sbG93aW5nQ29udGV4dExpbmVDb3VudCI6MCwiZm9sbG93aW5nQ29udGV4dEhhc2giOiJlM2IwYzQ0Mjk4ZmMxYzE0OWFmYmY0Yzg5OTZmYjkyNDI3YWU0MWU0NjQ5YjkzNGNhNDk1OTkxYjc4NTJiODU1In0"
         }
       ]
     },
     {
       "id": "claim_a4df2645cea34004b214e75e9d87c9e5",
-      "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.",
+      "statement": "ui tests cover the footer key help, the version label, sender labels, clock time, QR text, wrapping, visible lines, input tail, messages and list titles and selection clamping.",
       "evidence": [
         {
-          "resource": "repo://src/ui.rs#L558-L730",
-          "version": "repo-lines-v1:sha256:2bb51360845dc96ea63e9ba03c980bf2957a86649ecdd32efa1bcf97463d56b1:eyJzZWxlY3RlZExpbmVDb3VudCI6MTczLCJmaXJzdFNlbGVjdGVkTGluZUhhc2giOiIyNWNmNWZjMjNmMmE2ZDIzYzNlYTUxZTcyZjVlODk2Yzc1NTVmYmFhMDVjZDViMWVhMTgyYmY5NjE4YThlODFiIiwibGFzdFNlbGVjdGVkTGluZUhhc2giOiI0MTJjYTM0NWNjZjc1YmY5YzA4MDZiY2U2OTViZThkZTgwOGI3OTk4NDI1MWE3YTU0ZDIwMmNmNjEwMWRkNDUxIiwicHJlY2VkaW5nQ29udGV4dExpbmVDb3VudCI6MywicHJlY2VkaW5nQ29udGV4dEhhc2giOiI5YTc1YTI2ZGRjNmU4NmI3YjZjZjliNWFjNTAwZjBiNWM4ZmM5MWRmZWE4Yjc1YjdkN2NiMDhjYjY3N2ZkOThmIiwiZm9sbG93aW5nQ29udGV4dExpbmVDb3VudCI6MCwiZm9sbG93aW5nQ29udGV4dEhhc2giOiJlM2IwYzQ0Mjk4ZmMxYzE0OWFmYmY0Yzg5OTZmYjkyNDI3YWU0MWU0NjQ5YjkzNGNhNDk1OTkxYjc4NTJiODU1In0"
+          "resource": "repo://src/ui.rs#L575-L752",
+          "version": "repo-lines-v1:sha256:e85913975a63028c40bc84fd3c1b842d226f277fb56a1588907f49fff7f23119:eyJzZWxlY3RlZExpbmVDb3VudCI6MTc4LCJmaXJzdFNlbGVjdGVkTGluZUhhc2giOiIyNWNmNWZjMjNmMmE2ZDIzYzNlYTUxZTcyZjVlODk2Yzc1NTVmYmFhMDVjZDViMWVhMTgyYmY5NjE4YThlODFiIiwibGFzdFNlbGVjdGVkTGluZUhhc2giOiI0MTJjYTM0NWNjZjc1YmY5YzA4MDZiY2U2OTViZThkZTgwOGI3OTk4NDI1MWE3YTU0ZDIwMmNmNjEwMWRkNDUxIiwicHJlY2VkaW5nQ29udGV4dExpbmVDb3VudCI6MywicHJlY2VkaW5nQ29udGV4dEhhc2giOiIwNmE2YTY2NDFiOGNiOGMwMjgxMWEzMzBhMmYyYTMyMzRkOTk5Zjg4YWJlYWNhYzEyYThlYTU1N2ZjZWQ4MzQzIiwiZm9sbG93aW5nQ29udGV4dExpbmVDb3VudCI6MCwiZm9sbG93aW5nQ29udGV4dEhhc2giOiJlM2IwYzQ0Mjk4ZmMxYzE0OWFmYmY0Yzg5OTZmYjkyNDI3YWU0MWU0NjQ5YjkzNGNhNDk1OTkxYjc4NTJiODU1In0"
         }
       ]
     },
@@ -81,50 +81,54 @@
       "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#L748-L762",
+          "resource": "repo://src/history.rs#L837-L851",
           "version": "repo-lines-v1:sha256:47bb26a3e316232f3605730556cc5136a6388c441f30c963b8d723852177dc9f:eyJzZWxlY3RlZExpbmVDb3VudCI6MTUsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjkyYjVlZGY1OTQ5Y2ZlYTY3ODIyY2Y2OTExMzIzNjNkNjhkNmYxZTE2OWY1ZTBkNGY3YzE0ZTVmYTFlNmQ0ZTUiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjdjOWI2OGRlMGIyZmFhYWM3OTQwMzEwNmMxNGRkOWE5MDgyODA4NzEzZmE0MjBjMWUyMmE2OGM1MTE3ZTE0MWMiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjoxLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEifQ"
         },
         {
-          "resource": "repo://src/history.rs#L79-L85",
+          "resource": "repo://src/history.rs#L81-L87",
           "version": "repo-lines-v1:sha256:5c13f3ecfca20f282a95114cf2752173bde3a7eb69f54778891a66c4a626c76f:eyJzZWxlY3RlZExpbmVDb3VudCI6NywiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiNjg5ZWI3YTE3N2JjMWI2YWVjYzNlMTQxNDE2ZGZiMDg3OTYyNDYwMjNlY2YzNDQ5OTdkNDNjZTE1MzRjMWM4MiIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiYjlkYWFmZmFjMzJjZTI2NTE4MGQwMTAzZWRkYjIyYmY1MTAwYjViMTdhZmUyZGNmN2Y1ZTkxYzg0Yzc4Y2Y1ZiIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiOTg3MGE1MGI0M2VjNGFlZDBlMGQ1ZTRiMzBjYTc3YmU3NDdkMzNhMWIxYmE0NmY3Mjg4MzM1YjZjNmRjMjdhMCIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiZDIxMTE4MGQzZDhjMWQ3NGU0NDNmNjNkYThhYjg0N2NlZGIzNjA5MzBlMzhhYTA5YTkwNGU0ODg1OTg2MWZjMSJ9"
         }
       ]
     },
     {
       "id": "claim_1814788a665c474fbe9623328521c079",
-      "statement": "The suite contains 214 test functions: autoreply 77, config 32, whatsapp 27, ai 25, history 16, ui 14, app 14, web 9.",
+      "statement": "The suite contains 205 test functions: autoreply 55, config 37, ai 27, whatsapp 22, app 20, history 18, ui 15, web 9, main 2.",
       "evidence": [
         {
-          "resource": "repo://src/ai.rs#L526-L850",
-          "version": "repo-lines-v1:sha256:39880d0b9281bc470c8334739ad41bf46e3b1b911fc7af86d57c688a64e7e335:eyJzZWxlY3RlZExpbmVDb3VudCI6MzI1LCJmaXJzdFNlbGVjdGVkTGluZUhhc2giOiIyNWNmNWZjMjNmMmE2ZDIzYzNlYTUxZTcyZjVlODk2Yzc1NTVmYmFhMDVjZDViMWVhMTgyYmY5NjE4YThlODFiIiwibGFzdFNlbGVjdGVkTGluZUhhc2giOiI0MTJjYTM0NWNjZjc1YmY5YzA4MDZiY2U2OTViZThkZTgwOGI3OTk4NDI1MWE3YTU0ZDIwMmNmNjEwMWRkNDUxIiwicHJlY2VkaW5nQ29udGV4dExpbmVDb3VudCI6MywicHJlY2VkaW5nQ29udGV4dEhhc2giOiJlZGM4MzEyNDk2NjE0YzZjOWUxMTQ1NzRhNzlkOTE4NTcwMzRiYmUzYjMwOTkyZjI3NjBmNzE2NjRmMDA1YzY5IiwiZm9sbG93aW5nQ29udGV4dExpbmVDb3VudCI6MCwiZm9sbG93aW5nQ29udGV4dEhhc2giOiJlM2IwYzQ0Mjk4ZmMxYzE0OWFmYmY0Yzg5OTZmYjkyNDI3YWU0MWU0NjQ5YjkzNGNhNDk1OTkxYjc4NTJiODU1In0"
+          "resource": "repo://src/ai.rs#L576-L905",
+          "version": "repo-lines-v1:sha256:d19f1348b5c8956f0781aa1895f12dd84dfe7f18a54ac9b446ef380fc38b9548:eyJzZWxlY3RlZExpbmVDb3VudCI6MzMwLCJmaXJzdFNlbGVjdGVkTGluZUhhc2giOiIyNWNmNWZjMjNmMmE2ZDIzYzNlYTUxZTcyZjVlODk2Yzc1NTVmYmFhMDVjZDViMWVhMTgyYmY5NjE4YThlODFiIiwibGFzdFNlbGVjdGVkTGluZUhhc2giOiI0MTJjYTM0NWNjZjc1YmY5YzA4MDZiY2U2OTViZThkZTgwOGI3OTk4NDI1MWE3YTU0ZDIwMmNmNjEwMWRkNDUxIiwicHJlY2VkaW5nQ29udGV4dExpbmVDb3VudCI6MywicHJlY2VkaW5nQ29udGV4dEhhc2giOiJlZGM4MzEyNDk2NjE0YzZjOWUxMTQ1NzRhNzlkOTE4NTcwMzRiYmUzYjMwOTkyZjI3NjBmNzE2NjRmMDA1YzY5IiwiZm9sbG93aW5nQ29udGV4dExpbmVDb3VudCI6MCwiZm9sbG93aW5nQ29udGV4dEhhc2giOiJlM2IwYzQ0Mjk4ZmMxYzE0OWFmYmY0Yzg5OTZmYjkyNDI3YWU0MWU0NjQ5YjkzNGNhNDk1OTkxYjc4NTJiODU1In0"
         },
         {
-          "resource": "repo://src/app.rs#L1167-L1295",
-          "version": "repo-lines-v1:sha256:bc3ce2ecf6be61a9ab36898b788851fb831cf4bcc6aebdb1206ec8df826e9505:eyJzZWxlY3RlZExpbmVDb3VudCI6MTI5LCJmaXJzdFNlbGVjdGVkTGluZUhhc2giOiIyNWNmNWZjMjNmMmE2ZDIzYzNlYTUxZTcyZjVlODk2Yzc1NTVmYmFhMDVjZDViMWVhMTgyYmY5NjE4YThlODFiIiwibGFzdFNlbGVjdGVkTGluZUhhc2giOiI0MTJjYTM0NWNjZjc1YmY5YzA4MDZiY2U2OTViZThkZTgwOGI3OTk4NDI1MWE3YTU0ZDIwMmNmNjEwMWRkNDUxIiwicHJlY2VkaW5nQ29udGV4dExpbmVDb3VudCI6MywicHJlY2VkaW5nQ29udGV4dEhhc2giOiJmNjk0ZjY2MmZhYjIzMWU4ZDhkYjA5NGI1NTc5ODE1ZjA5OWRhNDBkZTdlOGI5NjVhZTA0NTZhZGMwNDliMzg2IiwiZm9sbG93aW5nQ29udGV4dExpbmVDb3VudCI6MCwiZm9sbG93aW5nQ29udGV4dEhhc2giOiJlM2IwYzQ0Mjk4ZmMxYzE0OWFmYmY0Yzg5OTZmYjkyNDI3YWU0MWU0NjQ5YjkzNGNhNDk1OTkxYjc4NTJiODU1In0"
+          "resource": "repo://src/app.rs#L1443-L1646",
+          "version": "repo-lines-v1:sha256:49775da3ba12a328474dedc2a4e2a0d82d1fb6d37c8adda1940d1fc34c97ec42:eyJzZWxlY3RlZExpbmVDb3VudCI6MjA0LCJmaXJzdFNlbGVjdGVkTGluZUhhc2giOiIyNWNmNWZjMjNmMmE2ZDIzYzNlYTUxZTcyZjVlODk2Yzc1NTVmYmFhMDVjZDViMWVhMTgyYmY5NjE4YThlODFiIiwibGFzdFNlbGVjdGVkTGluZUhhc2giOiI0MTJjYTM0NWNjZjc1YmY5YzA4MDZiY2U2OTViZThkZTgwOGI3OTk4NDI1MWE3YTU0ZDIwMmNmNjEwMWRkNDUxIiwicHJlY2VkaW5nQ29udGV4dExpbmVDb3VudCI6MywicHJlY2VkaW5nQ29udGV4dEhhc2giOiJmNjk0ZjY2MmZhYjIzMWU4ZDhkYjA5NGI1NTc5ODE1ZjA5OWRhNDBkZTdlOGI5NjVhZTA0NTZhZGMwNDliMzg2IiwiZm9sbG93aW5nQ29udGV4dExpbmVDb3VudCI6MCwiZm9sbG93aW5nQ29udGV4dEhhc2giOiJlM2IwYzQ0Mjk4ZmMxYzE0OWFmYmY0Yzg5OTZmYjkyNDI3YWU0MWU0NjQ5YjkzNGNhNDk1OTkxYjc4NTJiODU1In0"
         },
         {
-          "resource": "repo://src/autoreply.rs#L528-L1279",
-          "version": "repo-lines-v1:sha256:d591faa1c1e584a76e3e96176666068ce2c59a3d79328f8ff996a71b0abcdd6c:eyJzZWxlY3RlZExpbmVDb3VudCI6NzUyLCJmaXJzdFNlbGVjdGVkTGluZUhhc2giOiIyNWNmNWZjMjNmMmE2ZDIzYzNlYTUxZTcyZjVlODk2Yzc1NTVmYmFhMDVjZDViMWVhMTgyYmY5NjE4YThlODFiIiwibGFzdFNlbGVjdGVkTGluZUhhc2giOiI0MTJjYTM0NWNjZjc1YmY5YzA4MDZiY2U2OTViZThkZTgwOGI3OTk4NDI1MWE3YTU0ZDIwMmNmNjEwMWRkNDUxIiwicHJlY2VkaW5nQ29udGV4dExpbmVDb3VudCI6MywicHJlY2VkaW5nQ29udGV4dEhhc2giOiI5MzJmOWYwN2VkNDRlNzRmOWI3ZjVkYTM1NjU2MjFmMTE0ZGI3N2VkZjU0NzY3NmM2NmI3NGMxYzA3NDM3YzIxIiwiZm9sbG93aW5nQ29udGV4dExpbmVDb3VudCI6MCwiZm9sbG93aW5nQ29udGV4dEhhc2giOiJlM2IwYzQ0Mjk4ZmMxYzE0OWFmYmY0Yzg5OTZmYjkyNDI3YWU0MWU0NjQ5YjkzNGNhNDk1OTkxYjc4NTJiODU1In0"
+          "resource": "repo://src/autoreply.rs#L505-L1093",
+          "version": "repo-lines-v1:sha256:73d6683e98d1a479be5b3c5c82533ad41e429042edcbbc8f6c7d722a1eca2628:eyJzZWxlY3RlZExpbmVDb3VudCI6NTg5LCJmaXJzdFNlbGVjdGVkTGluZUhhc2giOiIyNWNmNWZjMjNmMmE2ZDIzYzNlYTUxZTcyZjVlODk2Yzc1NTVmYmFhMDVjZDViMWVhMTgyYmY5NjE4YThlODFiIiwibGFzdFNlbGVjdGVkTGluZUhhc2giOiI0MTJjYTM0NWNjZjc1YmY5YzA4MDZiY2U2OTViZThkZTgwOGI3OTk4NDI1MWE3YTU0ZDIwMmNmNjEwMWRkNDUxIiwicHJlY2VkaW5nQ29udGV4dExpbmVDb3VudCI6MywicHJlY2VkaW5nQ29udGV4dEhhc2giOiI5MzJmOWYwN2VkNDRlNzRmOWI3ZjVkYTM1NjU2MjFmMTE0ZGI3N2VkZjU0NzY3NmM2NmI3NGMxYzA3NDM3YzIxIiwiZm9sbG93aW5nQ29udGV4dExpbmVDb3VudCI6MCwiZm9sbG93aW5nQ29udGV4dEhhc2giOiJlM2IwYzQ0Mjk4ZmMxYzE0OWFmYmY0Yzg5OTZmYjkyNDI3YWU0MWU0NjQ5YjkzNGNhNDk1OTkxYjc4NTJiODU1In0"
         },
         {
-          "resource": "repo://src/config.rs#L323-L636",
-          "version": "repo-lines-v1:sha256:bceb8ba81c1ab6dfce9ecafd1fcf9cc1260d95a063990abea051a038a91cdec8:eyJzZWxlY3RlZExpbmVDb3VudCI6MzE0LCJmaXJzdFNlbGVjdGVkTGluZUhhc2giOiIyNWNmNWZjMjNmMmE2ZDIzYzNlYTUxZTcyZjVlODk2Yzc1NTVmYmFhMDVjZDViMWVhMTgyYmY5NjE4YThlODFiIiwibGFzdFNlbGVjdGVkTGluZUhhc2giOiI0MTJjYTM0NWNjZjc1YmY5YzA4MDZiY2U2OTViZThkZTgwOGI3OTk4NDI1MWE3YTU0ZDIwMmNmNjEwMWRkNDUxIiwicHJlY2VkaW5nQ29udGV4dExpbmVDb3VudCI6MywicHJlY2VkaW5nQ29udGV4dEhhc2giOiIxOWU3NjM1NWNmMTE5OTQ4MGMzZjk5ODM2YjE0ODZjODQxZDg5MWZhYWQ5MDRjNjA4ZTA5MzAyMmQzNzU3YWVlIiwiZm9sbG93aW5nQ29udGV4dExpbmVDb3VudCI6MCwiZm9sbG93aW5nQ29udGV4dEhhc2giOiJlM2IwYzQ0Mjk4ZmMxYzE0OWFmYmY0Yzg5OTZmYjkyNDI3YWU0MWU0NjQ5YjkzNGNhNDk1OTkxYjc4NTJiODU1In0"
+          "resource": "repo://src/config.rs#L342-L691",
+          "version": "repo-lines-v1:sha256:f633f3663fa11b33dd8d6391a60b3b839aaf69dfaa341e10d37f025c069d49b3:eyJzZWxlY3RlZExpbmVDb3VudCI6MzUwLCJmaXJzdFNlbGVjdGVkTGluZUhhc2giOiIyNWNmNWZjMjNmMmE2ZDIzYzNlYTUxZTcyZjVlODk2Yzc1NTVmYmFhMDVjZDViMWVhMTgyYmY5NjE4YThlODFiIiwibGFzdFNlbGVjdGVkTGluZUhhc2giOiI0MTJjYTM0NWNjZjc1YmY5YzA4MDZiY2U2OTViZThkZTgwOGI3OTk4NDI1MWE3YTU0ZDIwMmNmNjEwMWRkNDUxIiwicHJlY2VkaW5nQ29udGV4dExpbmVDb3VudCI6MywicHJlY2VkaW5nQ29udGV4dEhhc2giOiIxOWU3NjM1NWNmMTE5OTQ4MGMzZjk5ODM2YjE0ODZjODQxZDg5MWZhYWQ5MDRjNjA4ZTA5MzAyMmQzNzU3YWVlIiwiZm9sbG93aW5nQ29udGV4dExpbmVDb3VudCI6MCwiZm9sbG93aW5nQ29udGV4dEhhc2giOiJlM2IwYzQ0Mjk4ZmMxYzE0OWFmYmY0Yzg5OTZmYjkyNDI3YWU0MWU0NjQ5YjkzNGNhNDk1OTkxYjc4NTJiODU1In0"
         },
         {
-          "resource": "repo://src/history.rs#L481-L763",
-          "version": "repo-lines-v1:sha256:1d69fc86c05572c4af6df08c56af463f1a0de79555132c853c3e8b7c07eab17d:eyJzZWxlY3RlZExpbmVDb3VudCI6MjgzLCJmaXJzdFNlbGVjdGVkTGluZUhhc2giOiIyNWNmNWZjMjNmMmE2ZDIzYzNlYTUxZTcyZjVlODk2Yzc1NTVmYmFhMDVjZDViMWVhMTgyYmY5NjE4YThlODFiIiwibGFzdFNlbGVjdGVkTGluZUhhc2giOiI0MTJjYTM0NWNjZjc1YmY5YzA4MDZiY2U2OTViZThkZTgwOGI3OTk4NDI1MWE3YTU0ZDIwMmNmNjEwMWRkNDUxIiwicHJlY2VkaW5nQ29udGV4dExpbmVDb3VudCI6MywicHJlY2VkaW5nQ29udGV4dEhhc2giOiI2MDYzYzRkMWFmOGNkODliYjBiZGQ3NjU3ZmZjYmY2ODA2YTYxMGQ3OWQ2YzhkNGMzMDdjMjQxZGYzOTAwNTIyIiwiZm9sbG93aW5nQ29udGV4dExpbmVDb3VudCI6MCwiZm9sbG93aW5nQ29udGV4dEhhc2giOiJlM2IwYzQ0Mjk4ZmMxYzE0OWFmYmY0Yzg5OTZmYjkyNDI3YWU0MWU0NjQ5YjkzNGNhNDk1OTkxYjc4NTJiODU1In0"
+          "resource": "repo://src/history.rs#L543-L852",
+          "version": "repo-lines-v1:sha256:a4d3c1973b9e386233ba1511ab6e3adcf322e06bb30f97f8fb8f6a96258e6360:eyJzZWxlY3RlZExpbmVDb3VudCI6MzEwLCJmaXJzdFNlbGVjdGVkTGluZUhhc2giOiIyNWNmNWZjMjNmMmE2ZDIzYzNlYTUxZTcyZjVlODk2Yzc1NTVmYmFhMDVjZDViMWVhMTgyYmY5NjE4YThlODFiIiwibGFzdFNlbGVjdGVkTGluZUhhc2giOiI0MTJjYTM0NWNjZjc1YmY5YzA4MDZiY2U2OTViZThkZTgwOGI3OTk4NDI1MWE3YTU0ZDIwMmNmNjEwMWRkNDUxIiwicHJlY2VkaW5nQ29udGV4dExpbmVDb3VudCI6MywicHJlY2VkaW5nQ29udGV4dEhhc2giOiI2MDYzYzRkMWFmOGNkODliYjBiZGQ3NjU3ZmZjYmY2ODA2YTYxMGQ3OWQ2YzhkNGMzMDdjMjQxZGYzOTAwNTIyIiwiZm9sbG93aW5nQ29udGV4dExpbmVDb3VudCI6MCwiZm9sbG93aW5nQ29udGV4dEhhc2giOiJlM2IwYzQ0Mjk4ZmMxYzE0OWFmYmY0Yzg5OTZmYjkyNDI3YWU0MWU0NjQ5YjkzNGNhNDk1OTkxYjc4NTJiODU1In0"
         },
         {
-          "resource": "repo://src/ui.rs#L558-L730",
-          "version": "repo-lines-v1:sha256:2bb51360845dc96ea63e9ba03c980bf2957a86649ecdd32efa1bcf97463d56b1:eyJzZWxlY3RlZExpbmVDb3VudCI6MTczLCJmaXJzdFNlbGVjdGVkTGluZUhhc2giOiIyNWNmNWZjMjNmMmE2ZDIzYzNlYTUxZTcyZjVlODk2Yzc1NTVmYmFhMDVjZDViMWVhMTgyYmY5NjE4YThlODFiIiwibGFzdFNlbGVjdGVkTGluZUhhc2giOiI0MTJjYTM0NWNjZjc1YmY5YzA4MDZiY2U2OTViZThkZTgwOGI3OTk4NDI1MWE3YTU0ZDIwMmNmNjEwMWRkNDUxIiwicHJlY2VkaW5nQ29udGV4dExpbmVDb3VudCI6MywicHJlY2VkaW5nQ29udGV4dEhhc2giOiI5YTc1YTI2ZGRjNmU4NmI3YjZjZjliNWFjNTAwZjBiNWM4ZmM5MWRmZWE4Yjc1YjdkN2NiMDhjYjY3N2ZkOThmIiwiZm9sbG93aW5nQ29udGV4dExpbmVDb3VudCI6MCwiZm9sbG93aW5nQ29udGV4dEhhc2giOiJlM2IwYzQ0Mjk4ZmMxYzE0OWFmYmY0Yzg5OTZmYjkyNDI3YWU0MWU0NjQ5YjkzNGNhNDk1OTkxYjc4NTJiODU1In0"
+          "resource": "repo://src/main.rs#L186-L208",
+          "version": "repo-lines-v1:sha256:6f0ef80029cd8b3a3ed5570438cff5846f3f70c6e0fe4866184c8210aac3873f:eyJzZWxlY3RlZExpbmVDb3VudCI6MjMsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjI1Y2Y1ZmMyM2YyYTZkMjNjM2VhNTFlNzJmNWU4OTZjNzU1NWZiYWEwNWNkNWIxZWExODJiZjk2MThhOGU4MWIiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjM1ZjZmNDY2ZTU5Y2ZjMzA3MDNjNDVmNzJhZjY1ZGU1NGFjMzYzZDM1NGNjMWEzMzMwMzUwYWNkNDUyNDM0YzQiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjowLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImUzYjBjNDQyOThmYzFjMTQ5YWZiZjRjODk5NmZiOTI0MjdhZTQxZTQ2NDliOTM0Y2E0OTU5OTFiNzg1MmI4NTUifQ"
+        },
+        {
+          "resource": "repo://src/ui.rs#L575-L752",
+          "version": "repo-lines-v1:sha256:e85913975a63028c40bc84fd3c1b842d226f277fb56a1588907f49fff7f23119:eyJzZWxlY3RlZExpbmVDb3VudCI6MTc4LCJmaXJzdFNlbGVjdGVkTGluZUhhc2giOiIyNWNmNWZjMjNmMmE2ZDIzYzNlYTUxZTcyZjVlODk2Yzc1NTVmYmFhMDVjZDViMWVhMTgyYmY5NjE4YThlODFiIiwibGFzdFNlbGVjdGVkTGluZUhhc2giOiI0MTJjYTM0NWNjZjc1YmY5YzA4MDZiY2U2OTViZThkZTgwOGI3OTk4NDI1MWE3YTU0ZDIwMmNmNjEwMWRkNDUxIiwicHJlY2VkaW5nQ29udGV4dExpbmVDb3VudCI6MywicHJlY2VkaW5nQ29udGV4dEhhc2giOiIwNmE2YTY2NDFiOGNiOGMwMjgxMWEzMzBhMmYyYTMyMzRkOTk5Zjg4YWJlYWNhYzEyYThlYTU1N2ZjZWQ4MzQzIiwiZm9sbG93aW5nQ29udGV4dExpbmVDb3VudCI6MCwiZm9sbG93aW5nQ29udGV4dEhhc2giOiJlM2IwYzQ0Mjk4ZmMxYzE0OWFmYmY0Yzg5OTZmYjkyNDI3YWU0MWU0NjQ5YjkzNGNhNDk1OTkxYjc4NTJiODU1In0"
         },
         {
           "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"
+          "resource": "repo://src/whatsapp.rs#L965-L1199",
+          "version": "repo-lines-v1:sha256:c10f061195e8e976464dc5e22a7640a0b1e09f1c92b8731f435f83832ed65415:eyJzZWxlY3RlZExpbmVDb3VudCI6MjM1LCJmaXJzdFNlbGVjdGVkTGluZUhhc2giOiIyNWNmNWZjMjNmMmE2ZDIzYzNlYTUxZTcyZjVlODk2Yzc1NTVmYmFhMDVjZDViMWVhMTgyYmY5NjE4YThlODFiIiwibGFzdFNlbGVjdGVkTGluZUhhc2giOiI0MTJjYTM0NWNjZjc1YmY5YzA4MDZiY2U2OTViZThkZTgwOGI3OTk4NDI1MWE3YTU0ZDIwMmNmNjEwMWRkNDUxIiwicHJlY2VkaW5nQ29udGV4dExpbmVDb3VudCI6MywicHJlY2VkaW5nQ29udGV4dEhhc2giOiJkNmQxM2RhNTNkZGY1NGUwOWE5ZmIzNzc4NzQxNGE5NjE0NmQ0NzFjNWViYzZjZTI2Y2Y5YTM4NGYxODc3OGYxIiwiZm9sbG93aW5nQ29udGV4dExpbmVDb3VudCI6MCwiZm9sbG93aW5nQ29udGV4dEhhc2giOiJlM2IwYzQ0Mjk4ZmMxYzE0OWFmYmY0Yzg5OTZmYjkyNDI3YWU0MWU0NjQ5YjkzNGNhNDk1OTkxYjc4NTJiODU1In0"
         }
       ]
     },
@@ -133,8 +137,8 @@
       "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#L768-L812",
-          "version": "repo-lines-v1:sha256:72ae9920c7dd147c055f69fff83f0efcaa72533a47bee96c58a159af5688aa90:eyJzZWxlY3RlZExpbmVDb3VudCI6NDUsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjBjMzZlMGI1ZTQxNmY4NDQ1N2EzODNjZGNlNDU0Y2QzY2E2NmQ5OTFlMzRjOTlmZDQ4NzQ2NzIxZTA4ZmRjZTkiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImEwMGRiMDQwZjI5MDliZjk3OGZmMGE5NTZhNTVlODNjNjUzNDE4ZGU2YmI2OGY4MDQxZGE5NzZmOGQzZDIyNzAiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImRmMzliNWQwMzM4NGE5NWU4MjgyY2FiNWM0NDY3ZGFkZTM2OWRjNjJlOTVkMDZmMTNhYzgyOTkwMjRhZDE5YmQifQ"
+          "resource": "repo://src/ai.rs#L820-L868",
+          "version": "repo-lines-v1:sha256:28e9f0691f3d1a68739be49bd3325b406b41e5a83c60e4c0c671eed81f91a958:eyJzZWxlY3RlZExpbmVDb3VudCI6NDksImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjkyYjVlZGY1OTQ5Y2ZlYTY3ODIyY2Y2OTExMzIzNjNkNjhkNmYxZTE2OWY1ZTBkNGY3YzE0ZTVmYTFlNmQ0ZTUiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjAxYmE0NzE5YzgwYjZmZTkxMWIwOTFhN2MwNTEyNGI2NGVlZWNlOTY0ZTA5YzA1OGVmOGY5ODA1ZGFjYTU0NmIiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImIwOTRhNzlkYTZkYmY5M2I2MzljZGM3ZTA3OGUxYWI3YzJkZmI5YWE2MzE1YjMyN2YzOWM1NjA0NWU1MjZkNWQiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjczZDVlZjE2ZDc2NzNhODk3ZWNhNGZmY2E5YzQ2NjExNmY2NGMxMzQxMGU4YjljMGM2YmJmMWNmNTAyMjY5OWQifQ"
         }
       ]
     },
@@ -147,10 +151,24 @@
           "version": "repo-lines-v1:sha256:932c4dcc02d7a1f510bc2e787ba8755c60639a9c77352797a663dc6ac0e063f3:eyJzZWxlY3RlZExpbmVDb3VudCI6ODAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjI1Y2Y1ZmMyM2YyYTZkMjNjM2VhNTFlNzJmNWU4OTZjNzU1NWZiYWEwNWNkNWIxZWExODJiZjk2MThhOGU4MWIiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjhmZDM2OGIzODI2OTRjMDYxZWMzMDkyOTYzNjNkYTBlYWNkNzI3NDlmZTQzYmFiNTM4ZWMyMjlhMmM5MDE0NTkiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjowLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImUzYjBjNDQyOThmYzFjMTQ5YWZiZjRjODk5NmZiOTI0MjdhZTQxZTQ2NDliOTM0Y2E0OTU5OTFiNzg1MmI4NTUifQ"
         }
       ]
+    },
+    {
+      "id": "claim_23753ae74db44075ab1f7bec1da19733",
+      "statement": "Config prompt-file tests write per-process scratch files under the target directory and remove them after reading.",
+      "evidence": [
+        {
+          "resource": "repo://src/config.rs#L349-L375",
+          "version": "repo-lines-v1:sha256:074ff1586edc8fcdfee555ac451b2810d5deb73073328d3f7f989ee02aca2fb4:eyJzZWxlY3RlZExpbmVDb3VudCI6MjcsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjdmY2Y5NzI1NmRkZTlkNzYwNGJkZmZmYTMyZjVjOTI5ZGM1ZjIzN2UxNmZhNThhMWEzMmNiMTQyMWY1ZTY4NmEiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImNhYWZmODY5NzZkYWEyYzYwYjNlZmU4MDE2MTQxZDI5ODM3N2NhNTI2OGQ0ZWJlNTk1ZWVhZDVkYzQyMzJiNmIiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjU1NWQ1YWZiMzJlOWU4YmJlMjI3ZGY0NzAyMGQwNzU4YzcyZjc1Yjk0ZTBjOTVlODg2MGQyOTFlNmQ2MWJhMWMifQ"
+        },
+        {
+          "resource": "repo://src/config.rs#L418-L454",
+          "version": "repo-lines-v1:sha256:1860a1e1d35b88e371e7056a3261ef07ddfe79442e512b5a65c493d79e7c6a47:eyJzZWxlY3RlZExpbmVDb3VudCI6MzcsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjkyYjVlZGY1OTQ5Y2ZlYTY3ODIyY2Y2OTExMzIzNjNkNjhkNmYxZTE2OWY1ZTBkNGY3YzE0ZTVmYTFlNmQ0ZTUiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjEzZWE5YzkzNGFmY2Y5Yzc5NDE0NjE0MTZhODY4ODAxNjNjYzc5NGZkZjQ4ZDk3NWVjNTAxZDE5MWJhY2UxMDkiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjMzMDUyMTUxNzFiNzNmMmE3MTliYTZkYzhmZTYzMGM1ZGUyNDExN2JmYzIwNTU3NmZjNjhmODY1NDg1YzQ0YzUifQ"
+        }
+      ]
     }
   ],
   "verification": {
     "by": "openwiki/0.7.2",
-    "at": "2026-10-20T20:27:06.515Z"
+    "at": "2026-10-21T21:49:12.291Z"
   }
 }

+ 149 - 75
openwiki/.claims/workflows/ai-autoreply.json

@@ -1,6 +1,6 @@
 {
   "schemaVersion": 1,
-  "pageVersion": "sha256:ccc0e095054a5eb2aa06cbab3cb29b6a721ba01910ea260ce580279a25de25d8",
+  "pageVersion": "sha256:2d4f2f85140ac84b4be12c4f25bec9245efc316b27132e2568facabe948aff2b",
   "claims": [
     {
       "id": "claim_4abb79458e064f78a8d565a7069cb356",
@@ -17,11 +17,11 @@
       "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#L1127-L1140",
+          "resource": "repo://src/app.rs#L1403-L1416",
           "version": "repo-lines-v1:sha256:76ea2311dd543d33bf1c52b4cee8997184cb67643a8ba3b2f715abf2fea34667:eyJzZWxlY3RlZExpbmVDb3VudCI6MTQsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjA0NDI0MGFiOGNkMTM1NDE2ZTQ5MjMyYTEzMGIzYzlkYmI2NWZmYzg0OGNjYzY3OWJhYTQxYWQzNjY4ZTA3MjQiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjE5ZTc2MzU1Y2YxMTk5NDgwYzNmOTk4MzZiMTQ4NmM4NDFkODkxZmFhZDkwNGM2MDhlMDkzMDIyZDM3NTdhZWUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjQ0MDNkOWZmNDhiN2ZiNmQ3MjNkZTVjOWIzMTg5MzEzZTI0NjcwYTFhOTYyYWFlMWU2MTg0YTI5ZWZjNzFjNWQifQ"
         },
         {
-          "resource": "repo://src/app.rs#L763-L794",
+          "resource": "repo://src/app.rs#L976-L1007",
           "version": "repo-lines-v1:sha256:c4358a68adc4872cb86ae5a6cf9f5e527660022add8d3e68770933ca4f7e6ea6:eyJzZWxlY3RlZExpbmVDb3VudCI6MzIsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImEzYTUwNTFjOTU0MGU0MGQ2ZDM4ZTgzMWIzNTIxZWM3MWRiN2M5N2ZjN2IwYzRiZWQ1ZDlmYzY1YWVjMjBkNjkiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjkzNzNhMTgxNzI2YzQ3ZjdjNzlmOTI3ZDI2NzhhYzQ3NjYzY2VhMzI1OGYxOGEwNTM0ZTg2NDVlYmFlOTlkMGUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImUzZWNmNGIzNzRlN2M0ODFlZjUzODQ1Y2Y1YjlmNDI5MDk3NmQxYTlmNDlkOWY5NWJjNjIxNWEyY2VhMDYyOTAifQ"
         }
       ]
@@ -31,44 +31,40 @@
       "statement": "A chat is private when its JID server is s.whatsapp.net or lid, a group when it is g.us, and autoreply is supported only for these two kinds.",
       "evidence": [
         {
-          "resource": "repo://src/autoreply.rs#L48-L97",
-          "version": "repo-lines-v1:sha256:2c5bff97825cbfe1e0822d2f2450e3d71e38f9eba0e4ba5d78ce48dd225acc29:eyJzZWxlY3RlZExpbmVDb3VudCI6NTAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImRlNzAzNDZhYmE0OTZiOTAzNzI1MTY5OWQzY2YyNDAwNmU0ZGYyMDM2N2MzNDRiZjllZjkyOWEwMjJmYTRjZGYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjIxNTY3ZTI1ZmQ3ZTExMzc3YmU4NDk5OTc2NDEzOThhYzRmZTMxNTBiNzE3OTA4NjNiNzc0ZTAzYmNiZGM0ZjkiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjZhYzE3ZjhmYjIwNjZkNTU5ZDU2MmYzNTkwODY4ZDRlNTMwYjIwZmQzNzdkNDkwZTJmZjE4Njc3Y2M3YTBlZTkifQ"
+          "resource": "repo://src/autoreply.rs#L58-L107",
+          "version": "repo-lines-v1:sha256:2c5bff97825cbfe1e0822d2f2450e3d71e38f9eba0e4ba5d78ce48dd225acc29:eyJzZWxlY3RlZExpbmVDb3VudCI6NTAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImRlNzAzNDZhYmE0OTZiOTAzNzI1MTY5OWQzY2YyNDAwNmU0ZGYyMDM2N2MzNDRiZjllZjkyOWEwMjJmYTRjZGYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjIxNTY3ZTI1ZmQ3ZTExMzc3YmU4NDk5OTc2NDEzOThhYzRmZTMxNTBiNzE3OTA4NjNiNzc0ZTAzYmNiZGM0ZjkiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjE2MzhiZmQ4ZjNiOGY1OTQzZDQyODJhMmY3MGQ0ODBhOWE5MGVkMGE3ZTA1ZWQyOWNmMWE0YzRjMzZkYWJkN2IifQ"
         }
       ]
     },
     {
       "id": "claim_01e8cc3ee34a49b6a579c25728a8efa1",
-      "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.",
+      "statement": "Only newly saved messages reach maybe_autoreply, which requires an AI client and system prompt and treats the chat as enabled when the global or per-chat autoreply is on.",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L304-L346",
-          "version": "repo-lines-v1:sha256:155b5cdb56d090f18143cef6a1591677f8c4bce133e1ab20f4e42b6361ece72f:eyJzZWxlY3RlZExpbmVDb3VudCI6NDMsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjYyNjcxZjMwOGZhYjk0MTMwNzgwNDU4MzFhNDMxYjJjZTM3MzhhMTEyMzMyNDg4OGY2ZTgzNTY1ODhjYjNmOTciLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImQ1Zjg3MzRjYWZkNWU0MTNiNTUyZDMzZjFmMThlOTEyNWI2YTkwMTFhODU2OTM2YmJjMzRmMzAxMGM4Yzg1ZjkiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6Ijk0NDE2M2IzN2JlYTdlZjFjNmJiMDZkODAzMTQxOGRmZjZjNTBlNjgzMmUyMWVkZTI2ZjlhN2VjODA5MDlhMTIifQ"
+          "resource": "repo://src/app.rs#L333-L366",
+          "version": "repo-lines-v1:sha256:6c4f5e0458f8089356df9af12ebae1223140c0e16a7db40e5cd5c048fe12b1a0:eyJzZWxlY3RlZExpbmVDb3VudCI6MzQsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6Ijk2ZjNjMTFjYTY5OWY4OTA5MTUwMjAyYzhkZTVlMjdlZjAwNTIyOGRmZmNjMzAzOTlmNDg4OTdmYzdhOTVlMTEiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjdmYzAyNDRhNGJkOTI4YWRmMDE3YjljOWQ0ODIxYjU4NjgwZGRhMmI2YTBhZDEyYWU2MWVhNDBlOWZkMjM3OGMiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImI1YjUzYzQyYTJjZjU2YmRlZWIxOWJhYTZhZjdhYTQzZmRkNjFmN2VhNTQzOGVjOWRmYTAwYjc0Nzg3NWM1ZGQifQ"
         },
         {
-          "resource": "repo://src/app.rs#L366-L372",
-          "version": "repo-lines-v1:sha256:9190cc2d950eb45e132eae01c1ae01b5fb4af82470bb42fae8b4e0227a315710:eyJzZWxlY3RlZExpbmVDb3VudCI6NywiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiYmMzZGZhMjZjNzdiNTNkZDcyZWZmZGI1MmEyNWFlNGY5YzMwNTdiZTk2NWI0MzRlMTMyOWIwMTM2MGZmNGViNyIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDg5M2Y0MGUzNzgxZWI4OTZjODJkMDQ2YTA4MDY3YTExZTM1Y2RmMmYxZGZhZjI2ZmVhOTBiOGRiNjY3NjQxMyIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiNjM0ODk4ZWNhYTE1OWQ3NDg1NDNjOTU0Y2NkNTAxZGNiMmRjZGFlMzE4YTk2Y2JhNDE3NWNmNTM0YTkzYjU5MSIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiMjIwZTI2OTMzMWFlNTY1MmJhMzE1ZWJhN2ZjNGU1YzE1YWE0OWEzN2IyMTY0Zjc2NGViY2JhMDU4MTlkYzI0MCJ9"
+          "resource": "repo://src/app.rs#L521-L527",
+          "version": "repo-lines-v1:sha256:9190cc2d950eb45e132eae01c1ae01b5fb4af82470bb42fae8b4e0227a315710:eyJzZWxlY3RlZExpbmVDb3VudCI6NywiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiYmMzZGZhMjZjNzdiNTNkZDcyZWZmZGI1MmEyNWFlNGY5YzMwNTdiZTk2NWI0MzRlMTMyOWIwMTM2MGZmNGViNyIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDg5M2Y0MGUzNzgxZWI4OTZjODJkMDQ2YTA4MDY3YTExZTM1Y2RmMmYxZGZhZjI2ZmVhOTBiOGRiNjY3NjQxMyIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiMTYyZWU1N2U0MWE4YzIwYWUwMTY0ZGYyNjE0MzlkY2M0OTlkMzUyMDgzNDE1MTUxNmQwNDdlOGRlYWU4YTQ2NiIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiOTEyYzE5ODI4NjExZDJhZjdmZWFmNjQyN2ZmNzE0MjFiN2JhMTU4MzM5OWY0YWU5ODBiMzJkNThlNDUzYTJiYSJ9"
         },
         {
-          "resource": "repo://src/app.rs#L430-L436",
-          "version": "repo-lines-v1:sha256:8954f8190e201652fd68eb454a728e5e4173257eddebb901b8a0b8d4c773564e:eyJzZWxlY3RlZExpbmVDb3VudCI6NywiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiODA1ZTgyYjdjMjlkNzVkZDI3YmZhYTZiYjkxMzZiOTMwMjQyYWEzYjJkNmM5NjI4ZWVmMmY3ZjIxNzdlMzhhNiIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDg5M2Y0MGUzNzgxZWI4OTZjODJkMDQ2YTA4MDY3YTExZTM1Y2RmMmYxZGZhZjI2ZmVhOTBiOGRiNjY3NjQxMyIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiMjRmNGQ3NzU2OTA1OWI4NjMyNGU2YTVmNGY2YWU4YWI4NjM4YmUzNTY5YjM5MjRlZDJkOWJiZjY0MzczMjUyNCIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiZTRhNDZkM2QxYTE0NjM2YTRmZGVmODJkZWRhMWZlM2UzMjgzYjA3ZTBlMzc1ZWU5NGQyODIwZDRiYzI5MzFmNCJ9"
+          "resource": "repo://src/app.rs#L628-L634",
+          "version": "repo-lines-v1:sha256:8954f8190e201652fd68eb454a728e5e4173257eddebb901b8a0b8d4c773564e:eyJzZWxlY3RlZExpbmVDb3VudCI6NywiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiODA1ZTgyYjdjMjlkNzVkZDI3YmZhYTZiYjkxMzZiOTMwMjQyYWEzYjJkNmM5NjI4ZWVmMmY3ZjIxNzdlMzhhNiIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDg5M2Y0MGUzNzgxZWI4OTZjODJkMDQ2YTA4MDY3YTExZTM1Y2RmMmYxZGZhZjI2ZmVhOTBiOGRiNjY3NjQxMyIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiMjRmNGQ3NzU2OTA1OWI4NjMyNGU2YTVmNGY2YWU4YWI4NjM4YmUzNTY5YjM5MjRlZDJkOWJiZjY0MzczMjUyNCIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiYTZlYTYzN2IyZDc3YWVjZjY1Y2EyOWFjNTgzOGQ4NmRmNGEwODM4NTdmMDQwM2U5OWM2NWU3NzQyMzI5NDMwYyJ9"
         }
       ]
     },
     {
       "id": "claim_4d8d74d736ed4ca19a1d0de711c9f12f",
-      "statement": "should_reply requires the chat flag, an allowed sender, a private or group chat, a timestamp not before process start, non-placeholder text and an elapsed per-chat cooldown.",
+      "statement": "should_schedule requires no reply already pending for the chat, the chat enabled, an allowed sender, a private or group chat, a timestamp not before process start and non-blank, non-placeholder text.",
       "evidence": [
         {
-          "resource": "repo://src/autoreply.rs#L176-L194",
-          "version": "repo-lines-v1:sha256:9e93a0db0fa4ced6aa27a7f47b3a56fb1208bdf99886614dbc09fce7164c9ec4:eyJzZWxlY3RlZExpbmVDb3VudCI6MTksImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjExMGZiOGMzY2ZlZGE5ODcwZGRhNjg4NjBjYTA4YmJlZjI4ODIxYzc5ZTQyZTNkZDQ5MDY2MzQzZGEyMjcyY2UiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjU0ZWYyYjYzZjg4Njk1ZTc2OTIzMjVmMjE3Mzk4NzJjMDhiNTgxNjdjY2YyZWM2NTY2MGYzMDJkOTMyYTEyY2EiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjQ0MGUyMTJhMmMxNWZlMGJmOWRiNGZjYTEyOWU0NzAwYTViMjBjNWJmMzg2ZjI0N2YyMTMyNDAwNWE2YzM3MTQifQ"
+          "resource": "repo://src/autoreply.rs#L182-L191",
+          "version": "repo-lines-v1:sha256:1b2ab043319224b4a08306fe10e69a7a41bd99836ab89ee9cbb0c1a1f49fbf28:eyJzZWxlY3RlZExpbmVDb3VudCI6MTAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImRhNjA3MDVjZGJlZjdmYjI4MGZmYjZkOTRhM2YyNTBhOGRmNTAzZGVhNTgxZGZhMzU0NTM4NTM4ODJhYjc3NTEiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjU0ZWYyYjYzZjg4Njk1ZTc2OTIzMjVmMjE3Mzk4NzJjMDhiNTgxNjdjY2YyZWM2NTY2MGYzMDJkOTMyYTEyY2EiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjhkOWU4ZmE2ZTkzOTA1ZTQwYzdmNmM4YWM2NWRkNTQ4MzQyNDQ3M2I1MmMxYWUxMDE5MGIwNGNmNzI4ZTZiNWEifQ"
         },
         {
-          "resource": "repo://src/autoreply.rs#L258-L282",
-          "version": "repo-lines-v1:sha256:5c6b334c7ae6b4472da173dee6b729d06f5dcbdc994c15293c1090b8a2bbd3dd:eyJzZWxlY3RlZExpbmVDb3VudCI6MjUsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImI3MWJmYWRhZDk1ZTAyNzJiOTdkYmQ2Y2JjMzhkY2U5MWZlOGZiMDFmN2IwMzI3MzA4MDc3Y2U0M2IzZGI0MDIiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjJhOGZmNzc1N2U2ODI2MGYyM2Q1ODU0ZWFlMTExNDQ1NDM3MDNlMzE4MWQxMjIyOGU5MzRmOTM4NmQ1ZjBmYWYiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjJmYmE5MjI2NzJlMWM1MjY1NTMwZjBkYTNlN2RlZDBmOWQxZjUxNjY4YjJlMWMyZWI2MThlYWEzM2VlZmZkMDMifQ"
-        },
-        {
-          "resource": "repo://src/autoreply.rs#L309-L320",
-          "version": "repo-lines-v1:sha256:615cc2e52db1ca6101cf9b4078bacea06df42f2f0c7854818c096cdca18b44b6:eyJzZWxlY3RlZExpbmVDb3VudCI6MTIsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImUyNWNkOTE3ZmM2NjY0NDRlZmFkM2UxYTVjNDBlNWI1ZDA2OTIzNWMzNTdiODg0NmZkMmM5MGRjNDFkZjg3NDQiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjcxMjVhNjRhNDA1MjAxNjg3OTIzNWNhMjI0ODhiMTMyMTIzZmI2MDEyMDMxZTllOGVmZjlkYmEwNjU4OWM4MjkiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImZjMzI3YTcyNGI1MTQzMjY4NjE4YjJiMzEzNDU3MmIzMWI4OTFmZjczZDAzNGU4ODFhZDg1OWEzNDliZGRjMTcifQ"
+          "resource": "repo://src/autoreply.rs#L241-L265",
+          "version": "repo-lines-v1:sha256:bbcff26d49ec29313b51b2a51c2d5df2f8c9ed3f715d9fc6b4af559d93b7a68d:eyJzZWxlY3RlZExpbmVDb3VudCI6MjUsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6Ijc3OWM1NTkxNmMzNjZmZDdhNmU2ZTYwZmE4MDVjOTYxZGExYjVhNDFhNGJkMDk0MDkwZjZkM2YxODc5NTJkMTIiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6Ijc3MDE1ZTdkMThiMDkxMTVlZjkyMzU2NGJlMDQ3N2IxODllZTViYWI2ZjdkYmI4ZDg5NTZkMjQ0M2YwMjFmOWIiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjJmYmE5MjI2NzJlMWM1MjY1NTMwZjBkYTNlN2RlZDBmOWQxZjUxNjY4YjJlMWMyZWI2MThlYWEzM2VlZmZkMDMifQ"
         }
       ]
     },
@@ -77,118 +73,122 @@
       "statement": "Own messages trigger only when human-written in the self-chat, detected by comparing with own JIDs without device suffix.",
       "evidence": [
         {
-          "resource": "repo://src/autoreply.rs#L145-L174",
-          "version": "repo-lines-v1:sha256:b0f425dbb2a112090ca31624b7a4edec4b2bddb2492d830fb200b8c235a4b88f:eyJzZWxlY3RlZExpbmVDb3VudCI6MzAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQyNTc4ZDBlMjk2NDIwYTNkMWFmOGUyZDU1ZjJhYzFmOWFkZmZjNTY4ODIwOTI4Y2I1ZTU5M2YzMDMxN2Q0N2YiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6Ijk5OWJkZWZlZWFjYTYzNDBlMDI5MGRjODZlODg5Yzg5ZDVlMTg5YWE1YmE0NGI1ODViM2I3MDczNGY0YTYyZDgiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6Ijc3M2M5Y2QwZGRhYmU0MWEzMjJjMGRlNTI0OTIxOGM5YjNiYTZhODZlNDhlNDc0MWUwNmVmYmUyOGRlZmE0ZTcifQ"
+          "resource": "repo://src/autoreply.rs#L148-L180",
+          "version": "repo-lines-v1:sha256:32e4689dd25915397864463e71677879f2543d73b615303632cb241a548e433b:eyJzZWxlY3RlZExpbmVDb3VudCI6MzMsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjBjMzM4MTNhM2RiZmNlNjRmYzY2MTk0YmVlY2NlNDQxMjgxZjBiMTBkYjkyMzJjMmNmMzQ4N2NiMmExOGExZDgiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImQ3YTcwYTRiOGY2ODNiMmVhZGFlYzIyYWRkYzU4NzY3ZDUwZDAwZDlmYTQ0OTU0YjQzOTY1NTdmZDMwZWU3M2UiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImI0ZTI0ODc3YWIzZWIyYTI0NWE4ZWQxOTJiMDIxNjI5MjE2N2RhN2ViMDRlM2Y1Y2MxYjQ5Yzc1MjEzMzUyYjQifQ"
         },
         {
-          "resource": "repo://src/autoreply.rs#L349-L365",
+          "resource": "repo://src/autoreply.rs#L293-L309",
           "version": "repo-lines-v1:sha256:daeb52efe3c35b1565aea1badb2260d9cfb4caef892ee11755af3f03d40c0788:eyJzZWxlY3RlZExpbmVDb3VudCI6MTcsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImRhNTE4MDYxM2Q4NjEwNGE4Zjk1N2FhYTI2ODI4YjBmMjQ2NTk3ZTg4M2UyMzk3YTc3NDEyMzVkNjNhMTkzZmMiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjE5ZTc2MzU1Y2YxMTk5NDgwYzNmOTk4MzZiMTQ4NmM4NDFkODkxZmFhZDkwNGM2MDhlMDkzMDIyZDM3NTdhZWUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjBjNGVkNWUzYjNkYWRhZTk2YzQyZDg5ZDk2MTBkNTYyMmY5ZTkxOGI2ZTU5ZTQ2ZDMzNzUzMjNiNTNmNGJmMWUifQ"
         }
       ]
     },
     {
       "id": "claim_aabba3ec6866414a832790bbba7669db",
-      "statement": "record_reply is invoked in start_reply before the AI request is spawned and clears any pending deferral of the chat.",
+      "statement": "A scheduled chat stays pending from schedule_reply until finish_reply, which runs after every AI outcome, so messages arriving during the wait or the generation schedule no additional reply.",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L438-L457",
-          "version": "repo-lines-v1:sha256:77c613038db86228ee90623150e68a8e459a6add92955e4833a1ac3e5f863adc:eyJzZWxlY3RlZExpbmVDb3VudCI6MjAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImU0YTg3YjlhYzc5ZmRlZWFlMjkyODk0ZTMxNDQ3NzU4ZWQwYjdiNTFlZmMwMjJkN2YxNDhlZTU5MTY1MDY2N2QiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjFiYWU5M2RkYzVmMDAxZTlkYTEyOWJmNDgwOTM0NzBiNWVkOGYzYzc3Y2MwNjI0NmIyY2U5MTg3ZjEyMzRiNGQiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjA2ZWViM2RhYjZhYTAxODdiMjY5ZjlmNGY1NjUyODE3ZTljNDE0ZTQzZjQ1Nzc5OWY4YWJlMjliMjhiM2IzZmQifQ"
+          "resource": "repo://src/app.rs#L267-L283",
+          "version": "repo-lines-v1:sha256:30440aa53c9dd61b844f344f5fb331a44d386119170b337bf66d0dbcc172b2f0:eyJzZWxlY3RlZExpbmVDb3VudCI6MTcsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjVkOGIyMTkxMzczNzkzN2Y5NWE1M2YzYzg2ODM3Y2JhZmY3MWVmZDM0MjhlMDM3NGVkNGUxZGUzODg3Mzg1YzgiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImEwOTM2MDA2NDY1Y2FkNmM5ZDNiZjBlYjFkNWUxZDUzYTEyMDdhYjdiZTBhMmU0YjA3NDhjZjJhNGRiNTExMTIiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjAyNjE2ZDk0ZDJmZDg4NWViMDZhMGFmZjVkYjQ1YWUyOTdiMWQ5NzViYzU0MjRlZGIyYWUwZmE2NzZkMzE0ZTYifQ"
+        },
+        {
+          "resource": "repo://src/app.rs#L368-L376",
+          "version": "repo-lines-v1:sha256:df493befb56dd10251865172577fc5c0bae10fe73f070b409398d5734657fd5f:eyJzZWxlY3RlZExpbmVDb3VudCI6OSwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiMjVhYmM1NWJiNzAyNjM3YTU5MGU3ZjU3ZjVlNGQ1ZDI3MTc5NmYzNmY4YWQxNzM5ODI3NWZlZThlOWY3ODM4NCIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDg5M2Y0MGUzNzgxZWI4OTZjODJkMDQ2YTA4MDY3YTExZTM1Y2RmMmYxZGZhZjI2ZmVhOTBiOGRiNjY3NjQxMyIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiOTk5YmRlZmVlYWNhNjM0MGUwMjkwZGM4NmU4ODljODlkNWUxODlhYTViYTQ0YjU4NWIzYjcwNzM0ZjRhNjJkOCIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiMGZkOTUxZjMzMzEyYjBkYjE5OTA3M2NkMjM0MDgxYzczMzI0MjFmNjBmNGM2ZDgwMDdiZTZiMzVjMzA4NjE0ZSJ9"
         },
         {
-          "resource": "repo://src/autoreply.rs#L246-L256",
-          "version": "repo-lines-v1:sha256:150894127b938da2f1738e8bb2cf4936f7132bcad9feabc703f5b262caedc1f8:eyJzZWxlY3RlZExpbmVDb3VudCI6MTEsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjBkNmYxZjI2ZTdiNWQ1MzU3ZmJmMzU3NTJjYzQwY2Y5MzE5NDViYWEzNDdlMTNlNGEzOTBhZjAzZTc0NDE2NDUiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjM2Y2JmMjg5MDUyMmJjN2ZkOGM4MjNjODY3M2E4Y2Q3MjAxYzVhODQyODc4NDg0YzYyNjY5YzFjZWJjYTVkZGIiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjUwN2UwYTVmMjdiMDAxOGQxMDJiZjJlY2JlNDVlNGQ3ZDI0NTcyNGY2YWY2MTFiMmZkYjZmYzU0M2FlMWI0M2IifQ"
+          "resource": "repo://src/app.rs#L499-L519",
+          "version": "repo-lines-v1:sha256:9c95e2b6201e18825840eb6e7889d5b1dd8fdc2f20ba7da3471f9d4641456107:eyJzZWxlY3RlZExpbmVDb3VudCI6MjEsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ1NDc2YjZjNjU4MzI4NGViNzUyMTA1Mzg3NGFjZmU3NDY1MGQ0NTJhOTRhMjYxYzg4YTFhYzBjYTYzNWYwY2MiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6Ijk5OWJkZWZlZWFjYTYzNDBlMDI5MGRjODZlODg5Yzg5ZDVlMTg5YWE1YmE0NGI1ODViM2I3MDczNGY0YTYyZDgiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImNmMGY4ZWU3NGMyYmNhMjZlNTlmY2NmYjUzZmQyYWI2OGJjNzIwZTY2MGZiZjM3M2IwZjFhNTY4ZmIyNjBjOTUifQ"
         }
       ]
     },
     {
       "id": "claim_e5cd4fb182a34e47aec8344c6682627c",
-      "statement": "build_turns starts with the system prompt plus a role note, keeps the last 200 non-blank messages, maps roles depending on self-chat, prefixes group user turns with the sender label and rewrites only user turns for voice notes and images.",
+      "statement": "build_turns starts with the system prompt plus the chat line and role note (only the self-chat note in the self-chat), keeps the last 200 non-blank messages, maps roles depending on self-chat, prefixes group user turns with the sender label and rewrites only user turns for voice notes and images.",
       "evidence": [
         {
           "resource": "repo://src/autoreply.rs#L13-L27",
-          "version": "repo-lines-v1:sha256:a71b2b1ae4d31324f82e6858e3af144db8883e18a974555890172b34e25e4971:eyJzZWxlY3RlZExpbmVDb3VudCI6MTUsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImJkYTIxOWRlYzM3NjlkNDZkZjMxMmE5MzYzZGFhZmI5N2IzMGQxMTRkNWE0NGY4MmY4ZmNhZjJmNTJlNWY1NzkiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjdjY2U3ZjMxMmFjMmQ5ZmMzNGFlOGJjZDJhM2RkZmNiNGVjNjEyNDM0OTBjNTYwMDVhZmJmZmE4ZGM5ZjFiMTgiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImZkMGM2MzgyNzI5ZDA4ZTE0NGQ1NmJiNWJlNzAwYjMzMjEzMTMyMjAwNzQ3ZWI3M2JlYjc0NTAwOTlhYjg2Y2UiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImNiMDliNDAxZTM5ZTFlMWEyMGU3ZWI0MTc0MTE5MThmZWRkZWU3OTBiZmVhOTdlMDY4MzNlMDc0YmY1NDc1YzAifQ"
+          "version": "repo-lines-v1:sha256:a71b2b1ae4d31324f82e6858e3af144db8883e18a974555890172b34e25e4971:eyJzZWxlY3RlZExpbmVDb3VudCI6MTUsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImJkYTIxOWRlYzM3NjlkNDZkZjMxMmE5MzYzZGFhZmI5N2IzMGQxMTRkNWE0NGY4MmY4ZmNhZjJmNTJlNWY1NzkiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjdjY2U3ZjMxMmFjMmQ5ZmMzNGFlOGJjZDJhM2RkZmNiNGVjNjEyNDM0OTBjNTYwMDVhZmJmZmE4ZGM5ZjFiMTgiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImZkMGM2MzgyNzI5ZDA4ZTE0NGQ1NmJiNWJlNzAwYjMzMjEzMTMyMjAwNzQ3ZWI3M2JlYjc0NTAwOTlhYjg2Y2UiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjM1N2I1ZjVkNWU3MTlhMDlmOTM2OGE5OWI5MzEwZTU2YjM1YTI1NDNjYjE2MGE2NmJiYzIyNjgyOTM3Nzk5ZWEifQ"
         },
         {
-          "resource": "repo://src/autoreply.rs#L367-L526",
-          "version": "repo-lines-v1:sha256:2cb857b8e50b9c0653a2a3640c4cdd6c7ed1a0bca468b66c729aa8cb45c35556:eyJzZWxlY3RlZExpbmVDb3VudCI6MTYwLCJmaXJzdFNlbGVjdGVkTGluZUhhc2giOiIzZTBiMjBmNDZiMWYzNDE5OGQ3NGE0N2I3ZGNiZDUxZjI5NzhmYjNhM2YyNWE2OGQ0YzE1NzA3YTUwNTI0NjVlIiwibGFzdFNlbGVjdGVkTGluZUhhc2giOiI0MTJjYTM0NWNjZjc1YmY5YzA4MDZiY2U2OTViZThkZTgwOGI3OTk4NDI1MWE3YTU0ZDIwMmNmNjEwMWRkNDUxIiwicHJlY2VkaW5nQ29udGV4dExpbmVDb3VudCI6MywicHJlY2VkaW5nQ29udGV4dEhhc2giOiIxOWU3NjM1NWNmMTE5OTQ4MGMzZjk5ODM2YjE0ODZjODQxZDg5MWZhYWQ5MDRjNjA4ZTA5MzAyMmQzNzU3YWVlIiwiZm9sbG93aW5nQ29udGV4dExpbmVDb3VudCI6MywiZm9sbG93aW5nQ29udGV4dEhhc2giOiI3YmM5OGQ2YjBkYzQ5MmM1MDE2ZGYxMDgwNWEzYTU0ZWI5YzE4NGE1YTYxMzdlN2Y0YmVjZmEwYzIyMDU1MGQwIn0"
+          "resource": "repo://src/autoreply.rs#L311-L436",
+          "version": "repo-lines-v1:sha256:5a954d83ee76e833ed24d427d0e8e36e66ff9c8a1ac999e67ad6a3124d5df508:eyJzZWxlY3RlZExpbmVDb3VudCI6MTI2LCJmaXJzdFNlbGVjdGVkTGluZUhhc2giOiIzZTBiMjBmNDZiMWYzNDE5OGQ3NGE0N2I3ZGNiZDUxZjI5NzhmYjNhM2YyNWE2OGQ0YzE1NzA3YTUwNTI0NjVlIiwibGFzdFNlbGVjdGVkTGluZUhhc2giOiI0MTJjYTM0NWNjZjc1YmY5YzA4MDZiY2U2OTViZThkZTgwOGI3OTk4NDI1MWE3YTU0ZDIwMmNmNjEwMWRkNDUxIiwicHJlY2VkaW5nQ29udGV4dExpbmVDb3VudCI6MywicHJlY2VkaW5nQ29udGV4dEhhc2giOiIxOWU3NjM1NWNmMTE5OTQ4MGMzZjk5ODM2YjE0ODZjODQxZDg5MWZhYWQ5MDRjNjA4ZTA5MzAyMmQzNzU3YWVlIiwiZm9sbG93aW5nQ29udGV4dExpbmVDb3VudCI6MywiZm9sbG93aW5nQ29udGV4dEhhc2giOiI1NGJhMWQ4NGE2YjE3M2RlOGMwZjhlNjNlZTgzNTFhOTE2ZGUyYTNlYWViZjFhZDBjZjA1YmExN2JiYzY1YjdjIn0"
+        },
+        {
+          "resource": "repo://src/autoreply.rs#L482-L503",
+          "version": "repo-lines-v1:sha256:9f1d57d8cf162701d624737e5d0477297db574585ccc1e38af3f9cd95ce227a1:eyJzZWxlY3RlZExpbmVDb3VudCI6MjIsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImFmMDViNzI5MDFlOTRmOTRjNzZmMmI3YjY0YjU5YjlmMDg3M2JlNWFiY2ZhYTBlY2ZhMWU0NTk3ZDE4MDJhZWMiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjY3NDJmNGI4NTg1Y2M2ZDZjMmFiNzdiYTVmNGVhMmY0OWQxNzFiMzRmNWExMjdlYmM4NzU2ZWUzOWQ2OTI4ZGQiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjdiYzk4ZDZiMGRjNDkyYzUwMTZkZjEwODA1YTNhNTRlYjljMTg0YTVhNjEzN2U3ZjRiZWNmYTBjMjIwNTUwZDAifQ"
         }
       ]
     },
     {
       "id": "claim_3e40d76522d14d6a8010a842c8b4d312",
-      "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.",
+      "statement": "Before a reply, images in the context without a stored or in-progress description, at most the 5 most recent, are downloaded and described in parallel by the vision model; the reply starts after their ImagesDescribed event.",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L459-L490",
-          "version": "repo-lines-v1:sha256:885842d6f63043fbcf5c591add81a58c3216ed176aadc89bb23c76042527af03:eyJzZWxlY3RlZExpbmVDb3VudCI6MzIsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImQ2Njg5ODM3ODc3YzUxMGVhYWU0ZDE5YmFhMjkxYWI0NmQ2ZDhjMGEzMGU0M2I5MjViNDNiY2I4NTc4ZTdlMDQiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjE2MmVlNTdlNDFhOGMyMGFlMDE2NGRmMjYxNDM5ZGNjNDk5ZDM1MjA4MzQxNTE1MTZkMDQ3ZThkZWFlOGE0NjYiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImQwMjI2ZGQ4YmZkY2JkMDhlNjUxMjUzOGY4MmU5YWNiOTEyNzg2NTgwNDEwMmQ2YjRlMzQ4NmRmMjU4YjMxNmYifQ"
+          "resource": "repo://src/app.rs#L1231-L1292",
+          "version": "repo-lines-v1:sha256:3fb067a4234c53059cbc431004a05bb2d3c9fba17d27286a1860ffc3bbc41547:eyJzZWxlY3RlZExpbmVDb3VudCI6NjIsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjBmNDNmYzY5OWMxYzQyODRjMjI1MWJmMDdiZDk5ZDcyNjQzMGU4YzhhYWE2YzM2YTQyM2ZkZTQyYzc2Mzk2YmYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImRmYWI3MTBkZGY3ODU5ZTU3NTc2NGE5MTNhMDAyYzc0NTg1ZmZhNzAwMTUxMGNiODgxMWRhODNkMmNmNzRiMzQiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjU0MDAyNGFmZmIxODk1NGEyZjYxODAzYjk5ODBiOGY1NmVkNTlmZjJjN2FiODAyZDU5NDI2NGZiMDFkNmMyOGUifQ"
         },
         {
-          "resource": "repo://src/app.rs#L1023-L1086",
-          "version": "repo-lines-v1:sha256:c97db5fc22afa0f7a649b64cc9adfad1301d31a11a6024b3c54cf65fe46016df:eyJzZWxlY3RlZExpbmVDb3VudCI6NjQsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6Ijk0MTM1OGQzZmUwMGY3ZDM4YWM4MjNjMDc4OWNlZmU2NTNhMTE2YzkyZmNjNWMyYjYwNWY4ODA0MjA2MzRjYTAiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImQxZTM5MDcyMDkzN2I1OWI2MzAyZjNjZGUwY2Y5NzgyMTUxZjc2OTg0MDM4Yjc4ZjBiYzJmNjkxYjFmZTcwM2EiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImMwMDY1Njk0Y2MyNGUxNGQxNjdiNzI0YjgxNDZhNDIyYzI4MGUyNjE4MmNhNjFmMDVlMmQ2M2I1MTRiNTQxYjUifQ"
+          "resource": "repo://src/app.rs#L430-L463",
+          "version": "repo-lines-v1:sha256:9715aab5bcb9a42cb7f22b16e2ae0f531098ff6d6be6ca1ed6da8c95353afde8:eyJzZWxlY3RlZExpbmVDb3VudCI6MzQsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjRmODNjYmFiZjFkYzg0MDU0ZTM0OWEyOWI5M2QzODU0NTQ3NGZjYjkzMDU0ZjNkOTYzMDg4MWQxODNjZWI0MzciLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjI4ZTNjM2Y4MmI0ZjZmMzU0Mzg1NWNmMjZkNjljZmI0M2NjN2RmMTgxYzE5N2NmNDI0YmJhZjM0ODExZDA5M2UiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImUzZjUxYTZkNTgwMzYzOTIyNGZmZWQwOTU0MTEyNTZhNzhlMzA2NTU5NDZjZWY1ZWFmMzlmZjJkNDExMGM1MjIifQ"
         }
       ]
     },
     {
       "id": "claim_045b183e6c6b444c84449e47227a98cb",
-      "statement": "AI replies are discarded if both the global and per-chat autoreply were disabled meanwhile, otherwise sent and stored with is_ai true.",
+      "statement": "AI text replies are discarded if both the global and per-chat autoreply were disabled meanwhile or the owner wrote by hand after the answered context, otherwise sent and stored with is_ai true.",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L506-L564",
-          "version": "repo-lines-v1:sha256:20e1a027e2d853ac00e51605760fdd2bc3cb9bc4f976288e27576a8323a29479:eyJzZWxlY3RlZExpbmVDb3VudCI6NTksImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImEwMWQxYjkzMjk5YjRiOTc3ZWNiOGUzMWE0MDhmMWUxZjczMjA5OGY3ZmI4MjMxYzllOTgwMmJhNmQzNmRlYzciLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjQxN2NmMGY3N2ZlYjJiMGFiOWViN2M5NWUyZmNhZjQ5Mzg3ZDYyMjk4ZmYyZGE2ZGYyZGQwYmRlMWQwMGE2OGYiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjYzYjQ0YTU2ODZkYjM0MjNlMDVmOGMxOWMwYmMyNzlmMDA1NTE4NDJmMzgwZjIxMTM1ZTkzMTZhY2IwNmQ5ODEifQ"
+          "resource": "repo://src/app.rs#L703-L765",
+          "version": "repo-lines-v1:sha256:dc263d71dc225c968c33c780874494b82440bfd214546483d0aef9fd6a02bf46:eyJzZWxlY3RlZExpbmVDb3VudCI6NjMsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjBkNzQ1Mzc3ODY0MzI0MmZhZDE3MGZhNjcxNTUxYTJiYjI4ODM4Y2M2NmQ5ZjMwZWNkODhjZTNmNjFhYjRlMGEiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6Ijk5OWJkZWZlZWFjYTYzNDBlMDI5MGRjODZlODg5Yzg5ZDVlMTg5YWE1YmE0NGI1ODViM2I3MDczNGY0YTYyZDgiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjYzYjQ0YTU2ODZkYjM0MjNlMDVmOGMxOWMwYmMyNzlmMDA1NTE4NDJmMzgwZjIxMTM1ZTkzMTZhY2IwNmQ5ODEifQ"
         }
       ]
     },
     {
       "id": "claim_66877f18a3ca434fa2dc9b5857ba48b9",
-      "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.",
+      "statement": "The reply is sent reply_delay_seconds after the first unanswered message of a chat: a timer task emits ReplyDue after that delay.",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L191-L229",
-          "version": "repo-lines-v1:sha256:b8e7258ce42c14156856b5548408686e166ef6c02cf336560ffc75ccd7ecf145:eyJzZWxlY3RlZExpbmVDb3VudCI6MzksImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjdmNTYzZjU4ZTBlOWQyYjgxMjgwMTZhOWEyZGZhMTU2YjgzNTM0NWYzOTkzYTVhYjdkNjVhNWU4ODFmMDg0Y2UiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjBhOGJlZTA0NjIzODBmZjdmN2Y0ZDZhYzE2MGViODRmYjk3YjgwZGRjMjM2ZGFmZWY4NTQ4OTYxYjcwZDQ4ZmQiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImU3M2U3ZTFjNDIxZDk5NzQ5ZWVmNzNjNjJiNmE4ODAyODhjNGNhZDM4ZTYwZWQyMTNjNmM1ZDI1ZjAyMDU3OGMifQ"
+          "resource": "repo://src/app.rs#L1199-L1209",
+          "version": "repo-lines-v1:sha256:be3fc24d7db22727c3dddd8c5116b8fd64a13c8a473f99818b61f3bb6ac76204:eyJzZWxlY3RlZExpbmVDb3VudCI6MTEsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImMxMmIwOGNlNWY0N2RmY2RjMTQ1MTA4ZDFkNmQ2NzhiYTllNDhjZjBmMGY5YzdiYTBhNzMxYTkzMzFjYjc2ZDMiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjRjNjg0ZTk4OWVmNzk4ODJiNmIyOTMwNDdiMTJiNDg4YjkwNjlhYmUzMzc2ZTVhNTNhOGEyZTI5YjY4ODcyMTAiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjA1ZDJlYmFjZThlMTE0NjAzZTYyZDc1YjVmNmRjODFjMzJiMzgzMmI4YjBiMGUzZDVhZDY0OTMyMDQ4YTZkMzgifQ"
         },
         {
-          "resource": "repo://src/autoreply.rs#L322-L346",
-          "version": "repo-lines-v1:sha256:192ac46e05e25b4f5fb6e0cc1832fd04f3f34de846a5b57af4e0a59f064034cf:eyJzZWxlY3RlZExpbmVDb3VudCI6MjUsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImM4NzhjMGI1MjIwNTBlYjIxNDQ5MmY3MmM4OTM5MWIyNjFmNmRjZTBjY2NhNWRiMTU1ZjAwMWU1ZmJkYzdhNWYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjNiYTZmNjc2ZmYyNGZjN2VhMjM2NzZhOTNkYjc2MzRhNmJmMzM3Y2ViYmE1OWJkZmExYWM5NmQ2ZGRjNDUwMDEiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjhjYTUwMTNjY2ZiYWFhNGY4YTdiMmRlYTU1MDU2NTRhN2VmN2ExNGVmNTZhZGI3NzEyNGM2Y2NkMWVhMWNjN2UifQ"
+          "resource": "repo://src/app.rs#L368-L376",
+          "version": "repo-lines-v1:sha256:df493befb56dd10251865172577fc5c0bae10fe73f070b409398d5734657fd5f:eyJzZWxlY3RlZExpbmVDb3VudCI6OSwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiMjVhYmM1NWJiNzAyNjM3YTU5MGU3ZjU3ZjVlNGQ1ZDI3MTc5NmYzNmY4YWQxNzM5ODI3NWZlZThlOWY3ODM4NCIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDg5M2Y0MGUzNzgxZWI4OTZjODJkMDQ2YTA4MDY3YTExZTM1Y2RmMmYxZGZhZjI2ZmVhOTBiOGRiNjY3NjQxMyIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiOTk5YmRlZmVlYWNhNjM0MGUwMjkwZGM4NmU4ODljODlkNWUxODlhYTViYTQ0YjU4NWIzYjcwNzM0ZjRhNjJkOCIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiMGZkOTUxZjMzMzEyYjBkYjE5OTA3M2NkMjM0MDgxYzczMzI0MjFmNjBmNGM2ZDgwMDdiZTZiMzVjMzA4NjE0ZSJ9"
         },
         {
-          "resource": "repo://src/autoreply.rs#L99-L107",
-          "version": "repo-lines-v1:sha256:44aff225b65bc9725548abb963fccb03939a521d1c74d6e56d69851eaa0c0b4d:eyJzZWxlY3RlZExpbmVDb3VudCI6OSwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiY2M5OGJhYTA3YzIyNTBkNzRlY2VmNWE2NTZmMWQ3MzdjNjg2MzJmNzRmN2VjYTkzNmJiYTU0MTljZDVkNDk0ZSIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDEyY2EzNDVjY2Y3NWJmOWMwODA2YmNlNjk1YmU4ZGU4MDhiNzk5ODQyNTFhN2E1NGQyMDJjZjYxMDFkZDQ1MSIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiYjJiMThkY2EzNzJhZGFlNGQxMGM3ODdjNzVmOGE4NGE1MzAyZjViNTg0MjBkYTU2Yzc0YTUzZWY4OTVjNDI4MiIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiMTYzOGJmZDhmM2I4ZjU5NDNkNDI4MmEyZjcwZDQ4MGE5YTkwZWQwYTdlMDVlZDI5Y2YxYTRjNGMzNmRhYmQ3YiJ9"
+          "resource": "repo://src/config.rs#L38-L39",
+          "version": "repo-lines-v1:sha256:3841a56ef56846ac61df41e175eaa085bd0d61eea8f2b034ebc6e5737f160c6e:eyJzZWxlY3RlZExpbmVDb3VudCI6MiwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiMDZiYjVjMWVhMTg4OTM2NGMyMzE4M2JjYmU1ZDY2YjhmNmMxNTVlMjgxMGRmY2JkZTc4NjY5ZTBkZjhkN2YzYSIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiZDcxNDZlZjZhMzU1NWEzNTk2Y2EyNzZlMDU0MmExN2Y4ZGNmOGMxNDY5MjNlZmZiNGNkOTBlOTZmM2Y1YjRjZiIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiYWNlODhhMjI1Nzc5MzI5MWRhYzNiM2E5YmUxMTVjODQ2MDM2NmU3ZGNlODM5NTNiYzZkNTI5NzdlMDc3NjNhNCIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiZmZlNWFjNDU3ZTA5MTNkNmMzN2UzMDkxZGE4MWNiYzg4MzU0NDRlODk0MjA1Njk1ODQ0NGM4MzkxNWE1M2YxYiJ9"
         }
       ]
     },
     {
       "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 and the chat is still enabled; groups never defer.",
+      "statement": "When the reply is due it is dropped if the autoreply was disabled, the AI is missing, the chat is empty or the latest message was written by hand by the owner outside the self-chat.",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L315-L364",
-          "version": "repo-lines-v1:sha256:0f24c79bfda4f961235e5abf567bb77dfafce7f2765ead440107588291772518:eyJzZWxlY3RlZExpbmVDb3VudCI6NTAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjMyMTY3MzlmZjBiNWNmNWE1YTZiNjY4NDAwY2RkNDZjMTY2MTQ3NjFmMGUzZmY4YzBjYWI0ZTkwMWM1ZTFiNjEiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImRhOWQ4MzUyMGI4MzFmNTE4YThlYzVjMTUzYmI1ZTE5ODgwNjI2ZWZmNjJhODRkNGVkMDkxNWNmMTNjMzI3NTgiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImNmMGY4ZWU3NGMyYmNhMjZlNTlmY2NmYjUzZmQyYWI2OGJjNzIwZTY2MGZiZjM3M2IwZjFhNTY4ZmIyNjBjOTUifQ"
+          "resource": "repo://src/app.rs#L417-L451",
+          "version": "repo-lines-v1:sha256:a63345aecce224550feeba0b579240e2e6f55ea65600d2512c4b1e8291f1ed06:eyJzZWxlY3RlZExpbmVDb3VudCI6MzUsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjhhMGZkOGQ4YmVjYTA3ZDNkNGZkZDViOGFiMWI4NmQ1Y2MwNGVlYzdkNzY3ZTc1Y2Y3ODgxMDk2OGI2N2Q3NDUiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjE2MmVlNTdlNDFhOGMyMGFlMDE2NGRmMjYxNDM5ZGNjNDk5ZDM1MjA4MzQxNTE1MTZkMDQ3ZThkZWFlOGE0NjYiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjU3ODQ1MTBjNTZmNWE5YTNiMmY0ZTdkNmZjMDFjNGM4NTEyOWJjYzUzM2U1YTZjZTA2MTYyY2FiMmIwNGY4YzIifQ"
         },
         {
-          "resource": "repo://src/app.rs#L986-L997",
-          "version": "repo-lines-v1:sha256:c3506143ab1af95e5059073dbe2e9099d3c5115a9502bf0faa04a3d2578cf991:eyJzZWxlY3RlZExpbmVDb3VudCI6MTIsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImZiNzQ3YTE0ZDllZGE0MjlmN2RlOTA1M2UxYmMyNzMzODZhNDQyZWE4MDQzYzI0NWE5ZDQ1M2JmMjk2Y2I2NmIiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjRjNjg0ZTk4OWVmNzk4ODJiNmIyOTMwNDdiMTJiNDg4YjkwNjlhYmUzMzc2ZTVhNTNhOGEyZTI5YjY4ODcyMTAiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjA1ZDJlYmFjZThlMTE0NjAzZTYyZDc1YjVmNmRjODFjMzJiMzgzMmI4YjBiMGUzZDVhZDY0OTMyMDQ4YTZkMzgifQ"
+          "resource": "repo://src/app.rs#L529-L547",
+          "version": "repo-lines-v1:sha256:45691eccde8d968061d2835ed9d342c7665536f57d65eba18d6818bf6cdbaccc:eyJzZWxlY3RlZExpbmVDb3VudCI6MTksImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImRlNjMzYzk0ZmI0NTZhMDkxMTliNmQyOWYyZGMzNjQ2MTI5ZTVlY2M4MDYwMTdlNzVkYzk0MTM3N2RjZDQxNjMiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImJkYTA0OWEwZDljNDFjMmNjZGQ4OWY2MmQ2Zjc1OTA0ODBjNjQyZWI3Mzc5NTcwM2Q0MTQ1Y2I5MzQ2Y2VhZjIiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjNhMzhmYjI3YWI5N2YyMTc1ZGQ0MDc1NDhmMTcyMGUxNzMwYzY4YjA5ZGI1M2MzZTZiZTQ4OGQyYjMzMzMxZmYifQ"
         },
         {
-          "resource": "repo://src/autoreply.rs#L196-L244",
-          "version": "repo-lines-v1:sha256:7b191379781acc86afacb1fc13ed7598963b49d4924f91aba89a3bdfb10afc5a:eyJzZWxlY3RlZExpbmVDb3VudCI6NDksImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjhlY2IzMTBjMzIwZWFhYTRiMjM2MDllZjBmNjZiZjZiMWQ4MGE3NjQzOGVhZTgxYmVkYWMwZmI5ZTA3MDMxODQiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjVkOGQ4Y2NkOGU3YTk0NWI5NzY3YjgwOTkwMDMzYzM4NDVlMmQ3MzZjYWFmY2FjZGE4OTIzMTQ5OWNlNWQ4YmYiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjYyZDBkODQyNmQ1NThjNWM0ZjJiZDI5M2E0NDNlYTJlZGVmNGMxODgzODQyNzQyOWViMjUxNjU4ZjRlZWQxYzQifQ"
+          "resource": "repo://src/autoreply.rs#L231-L239",
+          "version": "repo-lines-v1:sha256:e449aa643a18bfc13c65470f8160c3d49e5fc14fad1a09a5a427b0337ee2c354:eyJzZWxlY3RlZExpbmVDb3VudCI6OSwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiNDc3NWNlNjg3YjU5YjZlZTJkYjRhOThlZTgwOGIzNDEwNThmOWRjNTY4NDMxYjg2NDA2Y2NhMjVkMzdkNGM2MiIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDg5M2Y0MGUzNzgxZWI4OTZjODJkMDQ2YTA4MDY3YTExZTM1Y2RmMmYxZGZhZjI2ZmVhOTBiOGRiNjY3NjQxMyIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiMzZjYmYyODkwNTIyYmM3ZmQ4YzgyM2M4NjczYThjZDcyMDFjNWE4NDI4Nzg0ODRjNjI2NjljMWNlYmNhNWRkYiIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiZjZhMWQ3MTgwMWZlMmYwNzkwNjkwZDA3NjczMjAxODY5MDRhODZhYmQzNGNhMTk1ZmMwZjhkYzJkYjlhMzc0OCJ9"
         }
       ]
     },
     {
       "id": "claim_d8b08d3eeb4d432f82bfd00abc49cbd2",
-      "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.",
+      "statement": "On the first OfflineSyncCompleted per process, catch_up prepares one reply without delay for 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, including its offline images, and reports the count in the status.",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L36-L36",
-          "version": "repo-lines-v1:sha256:e4cb6140de212ef27ad95651543bae53cb782d2cd46e494db3057b17e46a2635:eyJzZWxlY3RlZExpbmVDb3VudCI6MSwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiZTRjYjYxNDBkZTIxMmVmMjdhZDk1NjUxNTQzYmFlNTNjYjc4MmQyY2Q0NmU0OTRkYjMwNTdiMTdlNDZhMjYzNSIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiZTRjYjYxNDBkZTIxMmVmMjdhZDk1NjUxNTQzYmFlNTNjYjc4MmQyY2Q0NmU0OTRkYjMwNTdiMTdlNDZhMjYzNSIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiOWRjMmZmMjdhMGM0NWJjOTE0NmMzMWI4ZTc1NWIzMjc3YjEyYTVjMjRkOGJiZGQ2N2Q2MWU2OTYwMWNjOWIxNSIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiZjY4OWU1OTZlNGI2YmY2YmM1M2M5NmMxYTdmMTk4MGY4ZDE1NTRhMDQ0NTAxMmI0ZDRjMDlhY2JmZjY2YmI3OSJ9"
+          "resource": "repo://src/app.rs#L567-L626",
+          "version": "repo-lines-v1:sha256:662ddb2a45a183036d34d982acfd7438be6f423e58df87505555df7b3c0a5c12:eyJzZWxlY3RlZExpbmVDb3VudCI6NjAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImM5ZmJjMjYxZDk1MDVkNTBjZTU1MWY4ZDI4OTQ2MTZjMWM2MGY3NTlhZTAwYTE1OWY4Yzg3OGVmMWJjM2Y2ZWEiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6Ijg0ZTkyMmFmNjJhMTJlOTYxYjczZWM0ZGMyMzNjMTk2MDhjNzRmNDU1MzExNzUxOWQzZWU3MjViZDZkNWNkNmQiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6Ijc4OTU5NGE5NzIzN2Y2NjVkMmU0Y2RhMTIyZmY1NTIyNzJjMjVmZTFhMTI0NmZjMGNkNjllMDQ4MjE2ZTNlYWMifQ"
         },
         {
-          "resource": "repo://src/app.rs#L374-L428",
-          "version": "repo-lines-v1:sha256:1b48cd00806f0dcaf496a0f9d76527769cfe0ca58e68ae4518b62289b190ab8d:eyJzZWxlY3RlZExpbmVDb3VudCI6NTUsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImM5ZmJjMjYxZDk1MDVkNTBjZTU1MWY4ZDI4OTQ2MTZjMWM2MGY3NTlhZTAwYTE1OWY4Yzg3OGVmMWJjM2Y2ZWEiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImJkYTA0OWEwZDljNDFjMmNjZGQ4OWY2MmQ2Zjc1OTA0ODBjNjQyZWI3Mzc5NTcwM2Q0MTQ1Y2I5MzQ2Y2VhZjIiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6Ijc4OTU5NGE5NzIzN2Y2NjVkMmU0Y2RhMTIyZmY1NTIyNzJjMjVmZTFhMTI0NmZjMGNkNjllMDQ4MjE2ZTNlYWMifQ"
-        },
-        {
-          "resource": "repo://src/autoreply.rs#L284-L307",
-          "version": "repo-lines-v1:sha256:cf7440fc0934f44bb4a4f220c328b46fba2e06a8e9670a0af1fa746714b2e56d:eyJzZWxlY3RlZExpbmVDb3VudCI6MjQsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjNlZTNjZmNlMWFhMTdjOTk2YzhlZDhiZmVmN2UzODZkZjU1NDZmZjk1ODUyNzUxNTdhMjEwMzI3OGE1NGE0ZjQiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImUwZGY3NGRlMGI2ZGVlYjk5NjI4MGU2YzFiZDRlOGIyNTQ1MDUyODZhNzBhZjZkN2U4MWE0MTRjOWJhYzA3MGEiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjQ5MjhiMjFiN2NiMTg5YTQ5MTU5NzAxZGYxOGQzM2ZkZTdmZjU0MWU2MWU1MzlhMGYxZTRiM2VkY2FlOWRiYTIifQ"
+          "resource": "repo://src/autoreply.rs#L267-L290",
+          "version": "repo-lines-v1:sha256:07065945215d59bcdce533cf448c284c0de51f32fc4f1311433dbb8a554ba7c4:eyJzZWxlY3RlZExpbmVDb3VudCI6MjQsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjNlZTNjZmNlMWFhMTdjOTk2YzhlZDhiZmVmN2UzODZkZjU1NDZmZjk1ODUyNzUxNTdhMjEwMzI3OGE1NGE0ZjQiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImUwZGY3NGRlMGI2ZGVlYjk5NjI4MGU2YzFiZDRlOGIyNTQ1MDUyODZhNzBhZjZkN2U4MWE0MTRjOWJhYzA3MGEiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjhjYTUwMTNjY2ZiYWFhNGY4YTdiMmRlYTU1MDU2NTRhN2VmN2ExNGVmNTZhZGI3NzEyNGM2Y2NkMWVhMWNjN2UifQ"
         }
       ]
     },
@@ -197,22 +197,22 @@
       "statement": "The AI context is loaded with messages_since from local midnight of yesterday, capped at CONTEXT_MESSAGES.",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L438-L457",
-          "version": "repo-lines-v1:sha256:77c613038db86228ee90623150e68a8e459a6add92955e4833a1ac3e5f863adc:eyJzZWxlY3RlZExpbmVDb3VudCI6MjAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImU0YTg3YjlhYzc5ZmRlZWFlMjkyODk0ZTMxNDQ3NzU4ZWQwYjdiNTFlZmMwMjJkN2YxNDhlZTU5MTY1MDY2N2QiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjFiYWU5M2RkYzVmMDAxZTlkYTEyOWJmNDgwOTM0NzBiNWVkOGYzYzc3Y2MwNjI0NmIyY2U5MTg3ZjEyMzRiNGQiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjA2ZWViM2RhYjZhYTAxODdiMjY5ZjlmNGY1NjUyODE3ZTljNDE0ZTQzZjQ1Nzc5OWY4YWJlMjliMjhiM2IzZmQifQ"
+          "resource": "repo://src/app.rs#L636-L643",
+          "version": "repo-lines-v1:sha256:7c77ded5ac6d33c39416f329b08cebb428d4745fd351bf74af8942d0b7a7ca02:eyJzZWxlY3RlZExpbmVDb3VudCI6OCwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiNTUxNGM3ODU1Zjc2MTlkMWNmNzRmYjkxNzViOTNiMDc1ZTAwMGFjNTA0MjFlMWRmMWQ0Nzc4NWQ3NDkxYTdmOSIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDg5M2Y0MGUzNzgxZWI4OTZjODJkMDQ2YTA4MDY3YTExZTM1Y2RmMmYxZGZhZjI2ZmVhOTBiOGRiNjY3NjQxMyIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiMWJhZTkzZGRjNWYwMDFlOWRhMTI5YmY0ODA5MzQ3MGI1ZWQ4ZjNjNzdjYzA2MjQ2YjJjZTkxODdmMTIzNGI0ZCIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiNmZlMTlkZGI5ZjIxNTk1NWQzNzM5NjBlODk1MTI1ZGE0ZmYwYTYwNTAxMGY1ZmVmYWExYTI2ZDE0NWQ4NmNkMiJ9"
         },
         {
-          "resource": "repo://src/autoreply.rs#L476-L497",
-          "version": "repo-lines-v1:sha256:50ee4ad69b117378616ca21b79b698134c88777e95ce91db1351b2411baa697f:eyJzZWxlY3RlZExpbmVDb3VudCI6MjIsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImZkNDcwYzIwOWRjNTFhNjVkMGE4MzYzYjE3ZmI5NGI2MTZiOTMwNjU4MTIxZTc3ZjFmMTcwZGU2OWEzMGZiYzEiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjFhZWNlZmYwMmE0Y2M3YzRkNDU0ODYxNjk0OTM2OTFmNzk3Y2U5ZWZkOWFhZjU1OTQwZGE3NWIwNWVhM2RiY2IiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjIwYzRkZTBiYjA5ZGM2YTUzMmVjOGUxOGQ0ODFjMGQyZWJjMDA3ODNlMzhiODk4MDMyNzJiMmY4ZTMwOTJhMzIifQ"
+          "resource": "repo://src/autoreply.rs#L459-L480",
+          "version": "repo-lines-v1:sha256:50ee4ad69b117378616ca21b79b698134c88777e95ce91db1351b2411baa697f:eyJzZWxlY3RlZExpbmVDb3VudCI6MjIsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImZkNDcwYzIwOWRjNTFhNjVkMGE4MzYzYjE3ZmI5NGI2MTZiOTMwNjU4MTIxZTc3ZjFmMTcwZGU2OWEzMGZiYzEiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjAzYTEwMzdmMDcxOTYxZmIzYjEzYTdkYWZmNjlhY2MyYmFiODEwZmU4YmRhMTZhMmIyMjcxNTU0MzcxNGNjNzUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjIwYzRkZTBiYjA5ZGM2YTUzMmVjOGUxOGQ0ODFjMGQyZWJjMDA3ODNlMzhiODk4MDMyNzJiMmY4ZTMwOTJhMzIifQ"
         }
       ]
     },
     {
       "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.",
+      "statement": "The global autoreply does not bypass the chat-type rule: meets_content_rules 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"
+          "resource": "repo://src/autoreply.rs#L251-L265",
+          "version": "repo-lines-v1:sha256:3500de003e15b84a4a4ea0d82efa712d262694653367bfa7e0ba301b46eb3133:eyJzZWxlY3RlZExpbmVDb3VudCI6MTUsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImVhYjdlMDgxZWM2YWFmNTMwMmViYzE1MWJiYTc0NGYxZTI5MGU5NzllNGQxN2Y4YjQ3NjMzNmZhNjk2NzQyNGUiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjY0ZDMxNTc4NWMxODA1YWU3YzEzYzUwZWEyZjhhN2E2ZTcyYjNiMWVhOGQxNjgxNDIyMzYxYWZlNGRjNDIwMzEiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjJmYmE5MjI2NzJlMWM1MjY1NTMwZjBkYTNlN2RlZDBmOWQxZjUxNjY4YjJlMWMyZWI2MThlYWEzM2VlZmZkMDMifQ"
         }
       ]
     },
@@ -221,18 +221,92 @@
       "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",
+          "resource": "repo://src/app.rs#L1052-L1069",
           "version": "repo-lines-v1:sha256:05cde78eab7927a8b5c33ce971f85c0e210ce60936eada88f8c9481585c1519e:eyJzZWxlY3RlZExpbmVDb3VudCI6MTgsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjlhMjY2MjVlMDNiMmE0MDg1NDg0YjQ2NjUzOTllOWRmOWFhZDQzYmVkNGQxNWFkODEwODMzMGIyMDhjNmM5ZTYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6Ijk5OWJkZWZlZWFjYTYzNDBlMDI5MGRjODZlODg5Yzg5ZDVlMTg5YWE1YmE0NGI1ODViM2I3MDczNGY0YTYyZDgiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImU2ZWQ1MmZlYjhjZTliMjNiYmNiYTkwMGFkYjk0MzE3MjA4ODNhZTk0ODY4NmQyNjhlNmRkMjFlNjljYTU3YjkifQ"
         },
         {
-          "resource": "repo://src/history.rs#L285-L315",
-          "version": "repo-lines-v1:sha256:7446cd3d43ecedda818c27075602eb8b9f1b3ef669ea39edf3a6c5520cc4bec3:eyJzZWxlY3RlZExpbmVDb3VudCI6MzEsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImE2OTlhMTQ5OTg2YzgxZjQ4OGYwMjQxMmU4Zjk0YjE1NjdhNWQ2OTEzZTNiYTg0NjJlMmFmYzY5NDJkYWYxMTMiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjRjZTNjZDVlMGE4OTI3NjIwMWQ2ZWU2OWE2ZGM4ZTEyMzgxNDVmMzEyYmE2ODQxZjUzMGYzNDQ0MmU0Njc2NjIiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6Ijk4NTAwNmMxYjY0YjM5MzI5ZWZhMmUxZmU2YTEwMWRiYzg0MTIzYjkxNjkyYjM1MjhjOTg4YzhhOGVmNGRjY2QifQ"
+          "resource": "repo://src/history.rs#L287-L317",
+          "version": "repo-lines-v1:sha256:7446cd3d43ecedda818c27075602eb8b9f1b3ef669ea39edf3a6c5520cc4bec3:eyJzZWxlY3RlZExpbmVDb3VudCI6MzEsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImE2OTlhMTQ5OTg2YzgxZjQ4OGYwMjQxMmU4Zjk0YjE1NjdhNWQ2OTEzZTNiYTg0NjJlMmFmYzY5NDJkYWYxMTMiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjRjZTNjZDVlMGE4OTI3NjIwMWQ2ZWU2OWE2ZGM4ZTEyMzgxNDVmMzEyYmE2ODQxZjUzMGYzNDQ0MmU0Njc2NjIiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImQ5YzdmYzM1ZDBjZDRiYzE1MWI4N2I1Y2IyMDhiZjEwYjM5NjA5MmY4YzMzMmJmZjNmN2ZiNWUwOWNhZWRjMmUifQ"
+        }
+      ]
+    },
+    {
+      "id": "claim_fd5c7a184d1e434a85a1c63419bbadce",
+      "statement": "After a reply, if a newer message that passes should_schedule arrived after the answered context, finish_reply schedules one new reply keeping the images received meanwhile.",
+      "evidence": [
+        {
+          "resource": "repo://src/app.rs#L1307-L1314",
+          "version": "repo-lines-v1:sha256:b961fc471f97ce4ba30ba2d05c7240d1d8e6038ccae49f417602f58eb534920b:eyJzZWxlY3RlZExpbmVDb3VudCI6OCwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiYzNhMDkyMjQ3MTNmYjQ0MDIzMjJlMGFjODEwMWI3OTI4ZWQwNThlZTExZTU5NmZkYjMwM2NjMGRhOGJiZTc3YyIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDEyY2EzNDVjY2Y3NWJmOWMwODA2YmNlNjk1YmU4ZGU4MDhiNzk5ODQyNTFhN2E1NGQyMDJjZjYxMDFkZDQ1MSIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiMTVlMTI4NTMzNjllMzNkZDlkMmUyZDQxNjFlYmJhNWNjYWMxMDkxYzIwMzg1ZTViMzNlMjkwMGIyNjdlOTVlYiIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiNDlhNTk5ZGY5ZDUwOWVhMzBiNDMzZjkzYjY4ZjE4ZGQ1ODY2ZDVmMGJjMGMzMGEyMTU1OWFkN2U4MWM1ZmMwMCJ9"
+        },
+        {
+          "resource": "repo://src/app.rs#L499-L519",
+          "version": "repo-lines-v1:sha256:9c95e2b6201e18825840eb6e7889d5b1dd8fdc2f20ba7da3471f9d4641456107:eyJzZWxlY3RlZExpbmVDb3VudCI6MjEsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ1NDc2YjZjNjU4MzI4NGViNzUyMTA1Mzg3NGFjZmU3NDY1MGQ0NTJhOTRhMjYxYzg4YTFhYzBjYTYzNWYwY2MiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6Ijk5OWJkZWZlZWFjYTYzNDBlMDI5MGRjODZlODg5Yzg5ZDVlMTg5YWE1YmE0NGI1ODViM2I3MDczNGY0YTYyZDgiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImNmMGY4ZWU3NGMyYmNhMjZlNTlmY2NmYjUzZmQyYWI2OGJjNzIwZTY2MGZiZjM3M2IwZjFhNTY4ZmIyNjBjOTUifQ"
+        }
+      ]
+    },
+    {
+      "id": "claim_4cb6835327794c709466ce1c02eda889",
+      "statement": "Voice notes and images become factual texts in user turns: images use their stored description when available, otherwise an 'image you cannot see' note, followed by the caption.",
+      "evidence": [
+        {
+          "resource": "repo://src/autoreply.rs#L29-L56",
+          "version": "repo-lines-v1:sha256:d65df4f7d8a51106465aa35579f8737ba749b01ce8e5a297b156f48b73cde070:eyJzZWxlY3RlZExpbmVDb3VudCI6MjgsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQyMDQ3NjYzODdjNzE0ODkzYjdiZTA0YTBjMzdiM2RiYmMyM2MyMGU3YjJmMzgzZWFiNjc3M2RlMzQyMDMxNmIiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ1ZjQ5ZWI0NjVhZjcyYTdjYTQyNTU3NTE5MGQ2ZGUwZTEwZTllNWY3YTJjMzc1ZGRmYjQ5NmFiY2JhOTAxMGYiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImY1NDhiZWY1MzdjMDk5NjEzNDNhYzg0YTM5MTI2Nzk5NDhhMDQ4M2UxZWNiYmNmNzBiNTc5ZGZjNDljNjYzOTIiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjI0YjAwMzU1NGEzZTBhYjRhOWM2MGM5ODRhMWQ1ZGU5NTRkMGEyNWQ2NGJlOGQ4NWE4MmQ5YmNmNTMyNTdlODkifQ"
+        },
+        {
+          "resource": "repo://src/autoreply.rs#L336-L379",
+          "version": "repo-lines-v1:sha256:d5ff8e0615e8b48256c14bd7751292dd94626fd4c21d40b36d73c5e2f3cb8740:eyJzZWxlY3RlZExpbmVDb3VudCI6NDQsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjZjNDVmNTRiYjMyMGQzN2U2ZjZmZGJjYTUzMjBlMmYyNjI1ODJmODkwNzlhYmY1ODJjNjI4MjgzOTM5Zjg2ZjciLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImY5YWU5MTBkNzg0M2M3YWNhZWUxMGFhMDhhMGE2ZGRhYzFhZjhjMWExMjc5ZDQ2MTRhOGNhODViOTgxMmIwODkiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjQwNWQ4YWU3NjdiNjQ4YzZmM2Y4ODE1ZDZjODQ1MjFiOTQ5MWVmZTEyNmRiYTVmZjU0MWIyMmJjNWY3YjllNWQifQ"
+        }
+      ]
+    },
+    {
+      "id": "claim_37fd6eca27b74d1eb20276bf635feac0",
+      "statement": "Mentions of the account owner's number or LID in message texts are replaced with @[te] before the turns are built, without matching longer numbers.",
+      "evidence": [
+        {
+          "resource": "repo://src/app.rs#L654-L673",
+          "version": "repo-lines-v1:sha256:0a937f62450210fb1b4a45c2eb392c8533cab8fc5e7f2ca88caec3ba64669dbb:eyJzZWxlY3RlZExpbmVDb3VudCI6MjAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImZkMjA2ZWQ4NGMzNGNhMWE0ZmNmZmQ1NmFlOTBiNDgyZDIyNGM2OWQ0ZmRlMDY4MmI2MTllODNlMDRlNWJhYjYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjEyZTA5MzVlNDEzNWQ5MWEyZGFlMDUxOGE3ZTY5NjQ0MmIxYmM5ZGYxOThiNDA1ZmVmMGQzMjUwY2VmOWU1ZWQiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImQwMjI2ZGQ4YmZkY2JkMDhlNjUxMjUzOGY4MmU5YWNiOTEyNzg2NTgwNDEwMmQ2YjRlMzQ4NmRmMjU4YjMxNmYifQ"
+        },
+        {
+          "resource": "repo://src/autoreply.rs#L159-L170",
+          "version": "repo-lines-v1:sha256:50d1a6dfe10b9769d40a33924bcb0bf9d4aa264c3e7775b149a95cc7fa629e6e:eyJzZWxlY3RlZExpbmVDb3VudCI6MTIsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjM4NmEyNGE3ZTg2NTVkY2M2ODIzMDg0NGM1ZGNjZTA5ODgzNDU1MzFjOTZlNjdjZTIxOTZkMjU1Mjg3NGM2MTMiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImRiZjcwNGZiMzY2MTg3NDdmNjJlYTQ1M2MxMDg3ZGNiMWVjNDZlYmYxNmJjNjFmMDg5MWZjMjhkZGNiMDgzYTIiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImY4OGMyOWJiOTczMDg1Yzk0N2E2NWRmM2MxMjU0NTI3ZTA0MGFiZWFlMmM2MzM3Mzc3ODA2NjA4NDYwNjVhMmIifQ"
+        },
+        {
+          "resource": "repo://src/autoreply.rs#L438-L457",
+          "version": "repo-lines-v1:sha256:9a7559f07b9cf4dc7eb7bab1c174e1b73cd3a491c63f7dcb65449c9d15f42438:eyJzZWxlY3RlZExpbmVDb3VudCI6MjAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjFlNmM4N2FkOGEyMmJiNTMzZmQ5ZjQxZDk2ZTkyNTFkNGUxNTZlY2Q2YjIwNjcxYzVhZmYxZDM2ZTk3YTVjOWIiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImU5NmY4Y2IwODNmYTgwMGE0ZGNkMjk2OTcyMmQ3YjkyNjg2YWRkOWViZTM2NTU0OTJmYTY3MmIxYTdjZWQyNDIiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjZhZjExM2E5ZmE3NTZlZTI4ODYzNGUyZTYzOWM5M2Q0OTJkOTRlMGMyZjRmZWE0ZTJiYjFlZjA3Y2EwOGFmOGUifQ"
+        }
+      ]
+    },
+    {
+      "id": "claim_aa274cb1145a4194b4f510ea3fb7f9c3",
+      "statement": "Image descriptions are stored only when their message still exists; failed descriptions leave those images undescribed and set a status with the failure count and first error.",
+      "evidence": [
+        {
+          "resource": "repo://src/app.rs#L1316-L1324",
+          "version": "repo-lines-v1:sha256:a271b225aba65e2765d3d7b1ab0f8a02114cdf60cf3a5298066b80ba40c06576:eyJzZWxlY3RlZExpbmVDb3VudCI6OSwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiZmI5YzZkZjEyNGI2NDgxODMyZDk5NTgyMjRmNzFjYjRjZDJjM2Y3YTU3Njc1OWQ0OGM5NjBkZDNmZTE4OTliZCIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDEyY2EzNDVjY2Y3NWJmOWMwODA2YmNlNjk1YmU4ZGU4MDhiNzk5ODQyNTFhN2E1NGQyMDJjZjYxMDFkZDQ1MSIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiZjk2YWY2OWU3OGEwNDBlNTAxNjM2YjMwNDhkNGVjNTQ2Yzc0OTU1ZTEyN2IyY2M5OTU4MDFiZDI2YWM2NjZkZSIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiOTViMzliZDc5NmMyYjVjYThiYjhjMWEzNjUzODk5NzY5MGE0OTE5ZDY5NzVlNGMxMGZiMWE1ZGE3MGU1Mjc3OSJ9"
+        },
+        {
+          "resource": "repo://src/app.rs#L465-L497",
+          "version": "repo-lines-v1:sha256:49bdb09f74d399910bca1c218286bf995bae8e8457efe83cb97034814f6d67d5:eyJzZWxlY3RlZExpbmVDb3VudCI6MzMsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImJkOGRiMTAzNzg3MWI3NGU1OTVmYTZhZThkNjAwNTE4NDIyNDNkNzJkODZkNDM2OGUyZGU1OWI2MTdkYWM2NmIiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImJhMGM2MzVlMzlkMTFmNTgyOGE1Mzk1MzhkYjE0ZTdlNzU1YWFiMzlhZDNhNzljYmM0ZTFmZTQ4ZDgyZGZmYzIiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjBhN2YyNDA4YTExOTJiY2VmZTI0ODJhNTg2MGM1MTVhMmQ4NGIxMzQzZTE1ZDdmMGVmNGRhMTA0YzY1ZTJiM2EifQ"
+        }
+      ]
+    },
+    {
+      "id": "claim_d7ecd7ebb6e94bf093bec3de37a63db1",
+      "statement": "Each image download makes up to 3 attempts, each abandoned after 60 seconds, waiting 2 seconds times the attempt number between attempts.",
+      "evidence": [
+        {
+          "resource": "repo://src/app.rs#L1336-L1362",
+          "version": "repo-lines-v1:sha256:503c0157ef6bb7cec106cb2a0c2a270703d688580b0dcdc4e710dc7b9dc1fc08:eyJzZWxlY3RlZExpbmVDb3VudCI6MjcsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjZmMjVmNjQxZDQ5ZGRkYTllYmMyNjgyOGViZDJmNzM2OWFkNDBmMjBmODRhNDAwZDFkYjliYzE1YTNlMmQyNTEiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjAwOTlhMTBhNjE5YjUxNmM0MDM5NjFmZmQ4NzhlYWJiMDM2NGRjYjEzYjcxNWFlODI5OTg2ZjhkY2VjMzA1ZGIiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjY5OTNmMTFjZmFmODg0N2M5ZTgxNmQ0NDhiMWE0MzZiNjlhMjk1NjQzZmVmMGRlNDliYTMyY2FkYmFjNTIxM2UifQ"
+        },
+        {
+          "resource": "repo://src/app.rs#L64-L72",
+          "version": "repo-lines-v1:sha256:165e39ca6d47b5010cdaa8900029d4f8be1acafd1768902f613c6b9232914b22:eyJzZWxlY3RlZExpbmVDb3VudCI6OSwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiYmYzYmQxMmI3NWRiNDMzNWFmZjQwOGNhYzM3YjU4YjIxZDEyYWZlZTZlODgyYWM0ODlkYmNlN2JlMWJjOWYzNiIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDg4NjhmM2IxNzUxODE3YTdlZTgwYTMwNTU1MzhhNDBmNmU4ZTlhMmE3N2QwZTZmNDYwMGIwOTAwOTkyN2I2YiIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiODQyYTI1MTg4ZDEyNDhhNDk1MmZjN2E4MGI2OWM5ZThhNzE1NjJmYjhiOGFiNTQ4MmU5MGRmYjQwZDUwOWUwOSIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiYzhkMTA1ZjI4ZjVmMWMwMzFmYTE3MjY3NDE4MDgxN2NkZWUxZDJlODBiZmRiNWM4MzUzOWY3YTFhNzY5MGUwYSJ9"
         }
       ]
     }
   ],
   "verification": {
     "by": "openwiki/0.7.2",
-    "at": "2026-10-20T20:27:06.515Z"
+    "at": "2026-10-21T21:49:12.291Z"
   }
 }

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

@@ -1,7 +1,7 @@
 {
-  "updatedAt": "2026-10-20T20:35:25.184Z",
+  "updatedAt": "2026-10-21T21:59:27.228Z",
   "command": "update",
-  "gitHead": "8229b35a985f6217af4aeaa5f86474e815225859",
+  "gitHead": "e147fe64421138ee5b6068d78986d551fd7e6be7",
   "model": "host-agent/claude",
   "status": "complete",
   "language": "en"

+ 44 - 44
openwiki/.page-manifest.json

@@ -2,81 +2,81 @@
   "schemaVersion": 1,
   "pages": {
     "/openwiki/architecture/app-state.md": {
-      "pageVersion": "sha256:34c88f57821c236e84d8cbbd5e3868cc3ff6629a5c6da55afacfbd3989da5c5a",
+      "pageVersion": "sha256:a11d9385ed0fa92efef8cc9b1d8eb7a4c392af8f751c1b660a60d6044eac8893",
       "completedBy": "claude-code",
-      "completedRunId": "22858bff-a907-4f99-a9b6-ae9a801d890d",
-      "gitHead": "8229b35a985f6217af4aeaa5f86474e815225859",
-      "sourceFingerprint": "sha256:3b21dde5e9b745e08dd416e8c87b4d81d9fe5026c49dd9001bf190232d2e76a7"
+      "completedRunId": "0da681ea-cfc0-4af9-a2fe-1b34cb0fad26",
+      "gitHead": "e147fe64421138ee5b6068d78986d551fd7e6be7",
+      "sourceFingerprint": "sha256:c91de01aa07805e76fe5ac531adad9e790a95e925f32e6ef21aa6b743d82f3e9"
     },
     "/openwiki/architecture/runtime-and-event-loop.md": {
-      "pageVersion": "sha256:7cbc08bb6eae59bb32a760bcc976c115e6702ecca67c1710b68b7a232fb16f49",
+      "pageVersion": "sha256:3abe82512bfb153e658472ff7afa50967adbaa76f4efa6bb2af3215c6b83795a",
       "completedBy": "claude-code",
-      "completedRunId": "22858bff-a907-4f99-a9b6-ae9a801d890d",
-      "gitHead": "8229b35a985f6217af4aeaa5f86474e815225859",
-      "sourceFingerprint": "sha256:3b21dde5e9b745e08dd416e8c87b4d81d9fe5026c49dd9001bf190232d2e76a7"
+      "completedRunId": "0da681ea-cfc0-4af9-a2fe-1b34cb0fad26",
+      "gitHead": "e147fe64421138ee5b6068d78986d551fd7e6be7",
+      "sourceFingerprint": "sha256:c91de01aa07805e76fe5ac531adad9e790a95e925f32e6ef21aa6b743d82f3e9"
     },
     "/openwiki/architecture/terminal-ui.md": {
-      "pageVersion": "sha256:52543285487729f07ef002bcc4cb827b749e6ec744b94cd40269ba91da70160f",
+      "pageVersion": "sha256:d47df0cc7d426e68c11679d15aeb6042a2f2f7ca975321194f44268297b7bc05",
       "completedBy": "claude-code",
-      "completedRunId": "22858bff-a907-4f99-a9b6-ae9a801d890d",
-      "gitHead": "8229b35a985f6217af4aeaa5f86474e815225859",
-      "sourceFingerprint": "sha256:3b21dde5e9b745e08dd416e8c87b4d81d9fe5026c49dd9001bf190232d2e76a7"
+      "completedRunId": "0da681ea-cfc0-4af9-a2fe-1b34cb0fad26",
+      "gitHead": "e147fe64421138ee5b6068d78986d551fd7e6be7",
+      "sourceFingerprint": "sha256:c91de01aa07805e76fe5ac531adad9e790a95e925f32e6ef21aa6b743d82f3e9"
     },
     "/openwiki/concepts/history-store.md": {
-      "pageVersion": "sha256:af82818f0b14ff0161574d8625a43bed0807de3f4b8dc152a3283e1a40bccf24",
+      "pageVersion": "sha256:fb61743878eebfa7a936223b03e0c9a3d4247468218b42035889a35e1283bc0a",
       "completedBy": "claude-code",
-      "completedRunId": "22858bff-a907-4f99-a9b6-ae9a801d890d",
-      "gitHead": "8229b35a985f6217af4aeaa5f86474e815225859",
-      "sourceFingerprint": "sha256:3b21dde5e9b745e08dd416e8c87b4d81d9fe5026c49dd9001bf190232d2e76a7"
+      "completedRunId": "0da681ea-cfc0-4af9-a2fe-1b34cb0fad26",
+      "gitHead": "e147fe64421138ee5b6068d78986d551fd7e6be7",
+      "sourceFingerprint": "sha256:c91de01aa07805e76fe5ac531adad9e790a95e925f32e6ef21aa6b743d82f3e9"
     },
     "/openwiki/integrations/ai-client.md": {
-      "pageVersion": "sha256:e30d17eaaf9e0d8e88f0bc51ae0a0c586c23d7bea67344aa86f5188912c80441",
+      "pageVersion": "sha256:e71947f4ffd79153e75af279f7eade4504e8b9a729d276a1f1938a45a094f7a0",
       "completedBy": "claude-code",
-      "completedRunId": "22858bff-a907-4f99-a9b6-ae9a801d890d",
-      "gitHead": "8229b35a985f6217af4aeaa5f86474e815225859",
-      "sourceFingerprint": "sha256:3b21dde5e9b745e08dd416e8c87b4d81d9fe5026c49dd9001bf190232d2e76a7"
+      "completedRunId": "0da681ea-cfc0-4af9-a2fe-1b34cb0fad26",
+      "gitHead": "e147fe64421138ee5b6068d78986d551fd7e6be7",
+      "sourceFingerprint": "sha256:c91de01aa07805e76fe5ac531adad9e790a95e925f32e6ef21aa6b743d82f3e9"
     },
     "/openwiki/integrations/web-control-panel.md": {
-      "pageVersion": "sha256:659021f213da32179df3e732784be9d211b7b699c07342e5abae11b3d55b0d63",
+      "pageVersion": "sha256:3ffa0ef4024e9b6d6363e8a29f878dc08440b9bdb206eb8cbc0e7a43f37d70ed",
       "completedBy": "claude-code",
-      "completedRunId": "22858bff-a907-4f99-a9b6-ae9a801d890d",
-      "gitHead": "8229b35a985f6217af4aeaa5f86474e815225859",
-      "sourceFingerprint": "sha256:3b21dde5e9b745e08dd416e8c87b4d81d9fe5026c49dd9001bf190232d2e76a7"
+      "completedRunId": "0da681ea-cfc0-4af9-a2fe-1b34cb0fad26",
+      "gitHead": "e147fe64421138ee5b6068d78986d551fd7e6be7",
+      "sourceFingerprint": "sha256:c91de01aa07805e76fe5ac531adad9e790a95e925f32e6ef21aa6b743d82f3e9"
     },
     "/openwiki/integrations/whatsapp-client.md": {
-      "pageVersion": "sha256:2d56656fd165a43e6f66b741865ee0cc8e711720783c722f71a531e3b8ec0c37",
+      "pageVersion": "sha256:6a3c8a9d9be1d3bc11179381bdb384623d59cb3e2c026f4f3d62b3bf3a5f84da",
       "completedBy": "claude-code",
-      "completedRunId": "22858bff-a907-4f99-a9b6-ae9a801d890d",
-      "gitHead": "8229b35a985f6217af4aeaa5f86474e815225859",
-      "sourceFingerprint": "sha256:3b21dde5e9b745e08dd416e8c87b4d81d9fe5026c49dd9001bf190232d2e76a7"
+      "completedRunId": "0da681ea-cfc0-4af9-a2fe-1b34cb0fad26",
+      "gitHead": "e147fe64421138ee5b6068d78986d551fd7e6be7",
+      "sourceFingerprint": "sha256:c91de01aa07805e76fe5ac531adad9e790a95e925f32e6ef21aa6b743d82f3e9"
     },
     "/openwiki/operations/configuration-and-data.md": {
-      "pageVersion": "sha256:f973194dff48fa997deb016f452d6f1515458eef3b52aa111bc52439dafc1b81",
+      "pageVersion": "sha256:895dd056591483fa81fad9cbc781504961253ccdbdaf42c8ed0f92c3685c50cd",
       "completedBy": "claude-code",
-      "completedRunId": "22858bff-a907-4f99-a9b6-ae9a801d890d",
-      "gitHead": "8229b35a985f6217af4aeaa5f86474e815225859",
-      "sourceFingerprint": "sha256:3b21dde5e9b745e08dd416e8c87b4d81d9fe5026c49dd9001bf190232d2e76a7"
+      "completedRunId": "0da681ea-cfc0-4af9-a2fe-1b34cb0fad26",
+      "gitHead": "e147fe64421138ee5b6068d78986d551fd7e6be7",
+      "sourceFingerprint": "sha256:c91de01aa07805e76fe5ac531adad9e790a95e925f32e6ef21aa6b743d82f3e9"
     },
     "/openwiki/quickstart.md": {
-      "pageVersion": "sha256:f8e55a63196e59cd28b6114d94f0faa4326a003dbe6b743831920c4b4a7fb4c9",
+      "pageVersion": "sha256:0904a154437de3ef07d0a913900c796b3fc862d2fcd254438da8ab3c23dcf515",
       "completedBy": "claude-code",
-      "completedRunId": "22858bff-a907-4f99-a9b6-ae9a801d890d",
-      "gitHead": "8229b35a985f6217af4aeaa5f86474e815225859",
-      "sourceFingerprint": "sha256:3b21dde5e9b745e08dd416e8c87b4d81d9fe5026c49dd9001bf190232d2e76a7"
+      "completedRunId": "0da681ea-cfc0-4af9-a2fe-1b34cb0fad26",
+      "gitHead": "e147fe64421138ee5b6068d78986d551fd7e6be7",
+      "sourceFingerprint": "sha256:c91de01aa07805e76fe5ac531adad9e790a95e925f32e6ef21aa6b743d82f3e9"
     },
     "/openwiki/testing/unit-tests.md": {
-      "pageVersion": "sha256:a01bacd1db7d8ffec9c6c644ce544b4408c7a27be2ca73f04ff774e65a7810cb",
+      "pageVersion": "sha256:10bcb258ed8fc94585fa952d923b1602f7daafeeed0fd6693ff6c44f40ffd393",
       "completedBy": "claude-code",
-      "completedRunId": "22858bff-a907-4f99-a9b6-ae9a801d890d",
-      "gitHead": "8229b35a985f6217af4aeaa5f86474e815225859",
-      "sourceFingerprint": "sha256:3b21dde5e9b745e08dd416e8c87b4d81d9fe5026c49dd9001bf190232d2e76a7"
+      "completedRunId": "0da681ea-cfc0-4af9-a2fe-1b34cb0fad26",
+      "gitHead": "e147fe64421138ee5b6068d78986d551fd7e6be7",
+      "sourceFingerprint": "sha256:c91de01aa07805e76fe5ac531adad9e790a95e925f32e6ef21aa6b743d82f3e9"
     },
     "/openwiki/workflows/ai-autoreply.md": {
-      "pageVersion": "sha256:ccc0e095054a5eb2aa06cbab3cb29b6a721ba01910ea260ce580279a25de25d8",
+      "pageVersion": "sha256:2d4f2f85140ac84b4be12c4f25bec9245efc316b27132e2568facabe948aff2b",
       "completedBy": "claude-code",
-      "completedRunId": "22858bff-a907-4f99-a9b6-ae9a801d890d",
-      "gitHead": "8229b35a985f6217af4aeaa5f86474e815225859",
-      "sourceFingerprint": "sha256:3b21dde5e9b745e08dd416e8c87b4d81d9fe5026c49dd9001bf190232d2e76a7"
+      "completedRunId": "0da681ea-cfc0-4af9-a2fe-1b34cb0fad26",
+      "gitHead": "e147fe64421138ee5b6068d78986d551fd7e6be7",
+      "sourceFingerprint": "sha256:c91de01aa07805e76fe5ac531adad9e790a95e925f32e6ef21aa6b743d82f3e9"
     }
   }
 }

+ 131 - 97
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 (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.
+description: How the App struct in src/app.rs holds UI state, routes AppEvent values (keys, WhatsApp, AI replies, image descriptions, reply 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-20T20:27:06.515Z
+    at: 2026-10-21T21:49:12.291Z
 sources:
   - id: openwiki-source-9fec9d1af408501c9c455161
     resource: repo://src/app.rs
-generated: { by: "claude-code", at: "2026-10-20T20:27:06.515Z" }
+generated: { by: "claude-code", at: "2026-10-21T21:49:12.291Z" }
 ---
 
 # Application state and event handling
@@ -23,51 +23,62 @@ and [web panel](../integrations/web-control-panel.md) activity.
 
 ## Events
 
-`AppEvent` (`src/app.rs:73-98`) is the single message type of the main channel:
+`AppEvent` (`src/app.rs:90-121`) is the single message type of the main channel:
 
 | Variant | Producer | Meaning |
 |---|---|---|
 | `Key(KeyEvent)` | input thread | A key press (release/repeat events are filtered out upstream). |
 | `Redraw` | input thread | Terminal resized; nothing to change, just redraw. |
 | `Wa(WaEvent)` | WhatsApp forwarder | An event from the [WhatsApp client](../integrations/whatsapp-client.md). |
-| `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. |
+| `AiReply { chat_jid, result }` | AI background task | `Reply::Text`, `Reply::NoReply` (model called the `no_reply` tool) or an error string. |
+| `ImagesDescribed { target, results }` | image description task | One description (or error) per image of a chat, keyed by message id; `target` says whether to answer the chat afterwards. |
+| `ReplyDue { chat_jid }` | reply timer task | The reply delay of a chat has elapsed. |
 | `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: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.
+`handle_event` (`src/app.rs:267-283`) dispatches each variant; after an
+`AiReply` it always calls `finish_reply` to end the chat's pending state. 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:147-184`) exposes UI fields read by the renderer: `screen`,
+`App` (`src/app.rs:169-215`) 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
+the WhatsApp handle and a clone of the event sender, plus the reply
+bookkeeping:
+
+| Field | Purpose |
+|---|---|
+| `pending_images` | images received by chats with a scheduled reply, per chat, in arrival order |
+| `backlog_images` | images delivered with offline messages, kept until the catch-up runs |
+| `describing` | message ids whose image description is in progress (never described twice) |
+| `answered_up_to` | per chat, the id of the last message in the context of the reply in progress |
+| `own_jids_known`, `catch_up_done` | one-shot flags for own JIDs and the startup catch-up |
+| `pending_delete` | the chat awaiting a deletion confirmation |
+
+- `Screen` (`src/app.rs:131-137`): `Connecting` → `Pairing` (QR shown) → `Main`.
+- `Focus` (`src/app.rs:139-144`): `List` or `Input`.
+- `ListMode` (`src/app.rs:146-151`): `Chats` or `Contacts`.
+
+`App::new` (`src/app.rs:222-260`) 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.
+`ReplyGate` with the current time, 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.
+`build_ai` (`src/app.rs:1393-1401`) returns no client when the system prompt
+(`SOUL.md`, see [Configuration](../operations/configuration-and-data.md)) is
+missing — status `AI non configurata: imposta openai_api_key in config.toml e
+il prompt in SOUL.md` — and reports a client build error with the
+`AI non disponibile` prefix.
 
 ## WhatsApp events
 
-`handle_wa_event` (`src/app.rs:257-282`):
+`handle_wa_event` (`src/app.rs:289-312`):
 
 - `Qr` → switch to `Pairing` and store the QR payload.
 - `Connected` → switch to `Main`, status `Connesso`, refresh own JIDs.
@@ -76,53 +87,68 @@ 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:288-296`).
-- `Message { message, image, addressed_to }` → `on_message`
-  (`src/app.rs:304-313`).
+  refresh the open chat name (`src/app.rs:318-326`).
+- `Message { message, image }` → `on_message` (`src/app.rs:333-342`).
 
 `on_message` saves the message and stops immediately when
 `History::save_message` reports a duplicate, so history sync replays never
 trigger UI reloads or autoreplies twice. New messages reload the chat list, the
-open conversation and then go through `maybe_autoreply` together with the
-`addressed_to` JIDs (mentions/quotes) used by the group cooldown (see
+open conversation and then go through `maybe_autoreply` (see
 [AI autoreply](../workflows/ai-autoreply.md)).
 
-Own JIDs are fetched by `refresh_own_jids` (`src/app.rs:497-504`); the list is
+Own JIDs are fetched by `refresh_own_jids` (`src/app.rs:680-687`); 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
+## Autoreply orchestration
 
 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: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: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: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
-  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`)
+is on (`autoreply_enabled`, `src/app.rs:525-527`). `ai_setup`
+(`src/app.rs:632-634`) returns nothing, and so stops every path, when the AI
+client or the system prompt is missing. The full workflow is in
+[AI autoreply](../workflows/ai-autoreply.md); the state machine in `App` is:
+
+- **Schedule** — `maybe_autoreply` (`src/app.rs:351-366`) calls
+  `schedule_reply` when `ReplyGate::should_schedule` allows it: the chat is
+  marked pending and `spawn_reply_timer` (`src/app.rs:1204-1209`) emits
+  `ReplyDue` after `reply_delay_seconds`. A chat already pending gets no second
+  timer, so messages arriving meanwhile are answered together. An image sent by
+  an allowed sender in an enabled chat is kept by `keep_image`
+  (`src/app.rs:384-398`): in `pending_images` for a pending chat, in
+  `backlog_images` for an offline image before the catch-up, or described at
+  once without reply (`describe_late_image`) after it.
+- **Reply due** — `handle_reply_due` (`src/app.rs:422-428`) takes the kept
+  images and, if the chat is still pending, calls `prepare_reply`
+  (`src/app.rs:439-451`). `reply_context` (`src/app.rs:535-547`) returns
+  nothing when the autoreply was disabled, the AI is missing, the chat is
+  empty or its last message was written by hand by the owner; then the
+  pending state ends. Otherwise images still lacking a description go to
+  `spawn_descriptions` (reply follows on `ImagesDescribed`), or `start_reply`
+  runs at once.
+- **Descriptions** — `handle_images_described` (`src/app.rs:474-497`) stores
+  each description whose message still exists with
+  `History::save_image_description`, sets one status for failures
+  (`Descrizione immagine fallita (N): <first error>`) and, when `then_reply`
+  holds and the chat is still pending, starts the reply.
+- **Start** — `start_reply` (`src/app.rs:654-673`) records the last context
+  message in `answered_up_to`, marks own mentions, reads the chat name and the
+  stored image descriptions, builds the turns and calls `spawn_ai_request`.
+- **Finish** — `finish_reply` (`src/app.rs:505-519`) clears the pending state
+  and `answered_up_to`; if a newer message that passes `should_schedule`
+  arrived after the answered one, it schedules a new reply (keeping its
+  images), otherwise it drops the kept images.
+- **Startup catch-up** — `on_offline_sync_completed` (`src/app.rs:571-577`)
   runs `catch_up` once per process (`catch_up_done`). `catch_up`
-  (`src/app.rs:393-409`) asks `catch_up_chats` (`src/app.rs:416-428`) for the
+  (`src/app.rs:587-607`) asks `catch_up_chats` (`src/app.rs:614-626`) 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.
+  marks each pending and calls `prepare_reply` with its offline images (no
+  reply delay); the status becomes `Risposte di recupero: N` when N > 0.
 
 ## Key bindings
 
-Commands use function keys (`src/app.rs:22-29`); `handle_key`
-(`src/app.rs:573-603`) dispatches them:
+Commands use function keys (`src/app.rs:23-32`); `handle_key`
+(`src/app.rs:774-804`) dispatches them:
 
 | Key | Where | Action |
 |---|---|---|
@@ -131,7 +157,9 @@ Commands use function keys (`src/app.rs:22-29`); `handle_key`
 | 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. |
-| Up / Down | list focus | Move selection, clamped to the filtered list. |
+| Up / Down | list focus | Move selection by one row, clamped to the filtered list. |
+| PageUp / PageDown | list focus | Move selection by 10 rows (`LIST_PAGE_ROWS`), clamped. |
+| Home / End | list focus | Jump to the first / last row. |
 | 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). |
@@ -142,7 +170,7 @@ Commands use function keys (`src/app.rs:22-29`); `handle_key`
 
 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
+the input or the filter; `is_command` (`src/app.rs:1434-1441`) 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
@@ -151,59 +179,59 @@ mode with F2 also makes the event loop clear the terminal (see
 ### 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,
+`edit_filter`, `src/app.rs:812-848`). `visible_chats` / `visible_contacts`
+(`src/app.rs:923-933`) keep the entries whose name contains the filter,
+case-insensitively (`matches_filter`, `src/app.rs:1148-1150`); 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
+`request_delete` (`src/app.rs:867-884`) 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`).
+the same chat calls `confirm_delete` (`src/app.rs:891-906`), which removes
+the chat from the history (messages, image descriptions and row; no WhatsApp
+call), drops its pending reply, kept images and answered snapshot, 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:854-860`).
 
 ### Opening and toggling
 
-`open_selected` (`src/app.rs:862-878`) loads display name, per-chat
+`open_selected` (`src/app.rs:1075-1091`) 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.
+(`src/app.rs:1108-1133`) 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
+`toggle_autoreply` (F3, `src/app.rs:981-986`) delegates to
+`toggle_chat_autoreply` (`src/app.rs:994-1007`), 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
+(`src/app.rs:1408-1416`), 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>`.
 
-`toggle_global_autoreply` (F4, `src/app.rs:845-856`) refuses to enable
+`toggle_global_autoreply` (F4, `src/app.rs:1058-1069`) 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.
 
 ## Web panel requests
 
-`handle_web_request` (`src/app.rs:801-812`) applies a `WebCommand` and then
+`handle_web_request` (`src/app.rs:1014-1025`) 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
+- `ToggleChat(jid)` → `toggle_web_chat` (`src/app.rs:1032-1038`): 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,
+`web_state` (`src/app.rs:1044-1050`) returns the global flag, the status line
+and `web_chats` (`src/app.rs:1422-1432`): the TUI chat list in the same order,
 restricted to chats that support autoreply. See
 [Web control panel](../integrations/web-control-panel.md).
 
@@ -211,32 +239,38 @@ restricted to chats that support autoreply. See
 
 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:981-984`).
+shutdown (`src/app.rs:1194-1197`).
 
-- `submit_input` (`src/app.rs:742-750`) ignores blank input or no open chat,
+- `submit_input` (`src/app.rs:955-963`) ignores blank input or no open chat,
   clears the input and calls `spawn_send` with `is_ai: false`.
-- `spawn_send` (`src/app.rs:1101-1110`) calls `WaHandle::send_text` and emits
+- `spawn_send` (`src/app.rs:1377-1386`) calls `WaHandle::send_text` and emits
   `Sent`.
-- `handle_sent` (`src/app.rs:540-564`) stores a successful send as a
+- `handle_sent` (`src/app.rs:741-765`) 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:1004-1014`) runs a text completion and emits
-  `AiReply`.
-- `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: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.
+- `spawn_ai_request` (`src/app.rs:1218-1229`) runs the text completion in one
+  task and waits for it in another, so `AiReply` is always emitted — even when
+  the request task panics — and the chat never stays pending.
+- `spawn_image_descriptions` (`src/app.rs:1240-1263`) runs one task per image
+  in parallel (download with up to 3 attempts and a 60 s timeout each, then
+  `AiClient::describe_image`) and a collector task that emits a single
+  `ImagesDescribed`; a failed image does not stop the others and the bytes
+  stay in memory.
+- `handle_ai_reply` (`src/app.rs:695-701`) shows
+  `AI: nessuna risposta necessaria` for `Reply::NoReply`, `Errore AI: …` for
+  an error, and passes text to `send_ai_reply_if_enabled`
+  (`src/app.rs:709-722`), which sends it only if the chat is still enabled and
+  the owner did not answer by hand meanwhile (`owner_answered_meanwhile`);
+  otherwise it shows `Risposta AI scartata: autoreply disattivato` or
+  `Risposta AI scartata: hai già risposto`.
 
 ## Tests
 
-Unit tests at `src/app.rs:1167` onward cover `can_enable_autoreply` (private,
+Unit tests at `src/app.rs:1443` onward cover `can_enable_autoreply` (private,
 self-chat LID, groups allowed; broadcasts/newsletters and missing AI refused),
-`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
+`web_chats` filtering and order, `vision_applies`,
+`select_images_to_describe` (last five, skip known and out-of-context),
+`arrived_after`, `manual_reply_after`, `description_failure_status`,
+`matches_filter`, selection clamping/stepping and page steps, 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 (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.
+- [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 replies, image descriptions, reply 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 (including the --version flag and the build version embedded by build.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 with key help and build version.

+ 45 - 23
openwiki/architecture/runtime-and-event-loop.md

@@ -1,15 +1,17 @@
 ---
 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, the optional web panel thread and the draw/recv TUI loop.
-tags: [runtime, tokio, event-loop, ratatui, crossterm, web-panel]
+description: Process startup in src/main.rs (including the --version flag and the build version embedded by build.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, version]
+verified:
+  - by: openwiki/0.7.2
+    at: 2026-10-21T21:49:12.291Z
 sources:
+  - id: openwiki-source-36f054f1f38f2197a6af5bca
+    resource: repo://build.rs
   - id: openwiki-source-b55a21a31ede1b56cd31a6a6
     resource: repo://src/main.rs
-generated: { by: "claude-code", at: "2026-10-20T20:27:06.515Z" }
-verified:
-  - by: openwiki/0.7.2
-    at: 2026-10-20T20:27:06.515Z
+generated: { by: "claude-code", at: "2026-10-21T21:49:12.291Z" }
 ---
 
 # Runtime and event loop
@@ -18,13 +20,27 @@ The binary (`wh`) is a single-process terminal app. `src/main.rs` wires the
 modules together and runs one serialised event loop: every state change goes
 through [`App::handle_event`](app-state.md), so `App` needs no locking.
 
+## Build version
+
+`build.rs` runs `git rev-parse --short HEAD` at compile time and exports the
+result as the `WH_GIT_HASH` environment variable (`unknown` when git is
+missing, fails or prints nothing); it asks cargo to rebuild when `.git/HEAD`,
+`.git/refs` or `.git/packed-refs` change (`build.rs:14-33`). `main.rs`
+combines it into `BUILD_VERSION` = `<CARGO_PKG_VERSION> (<hash>)`
+(`src/main.rs:24-25`), which the [terminal UI](terminal-ui.md) shows at the
+bottom right.
+
 ## Startup sequence
 
-`main` (`src/main.rs:30-54`) runs on the multi-threaded tokio runtime
-(`#[tokio::main]`) and:
+`main` (`src/main.rs:36-64`) runs on the multi-threaded tokio runtime
+(`#[tokio::main]`). If any command-line argument is `--version` or `-V`
+(`wants_version`, `src/main.rs:66-72`) it only prints `wh <BUILD_VERSION>`
+and exits, without reading the configuration or touching the terminal.
+Otherwise it:
 
-1. Loads the configuration with `Config::load` and creates the data directory
-   (`ensure_data_dir`). See [Configuration and data](../operations/configuration-and-data.md).
+1. Loads the configuration with `Config::load` (`config.toml`, `SOUL.md`,
+   `vision.md`) and creates the data directory (`ensure_data_dir`). See
+   [Configuration and data](../operations/configuration-and-data.md).
 2. Opens the SQLite [history store](../concepts/history-store.md) at
    `history_db_path()`.
 3. Creates two unbounded `tokio::sync::mpsc` channels: one for `AppEvent`
@@ -33,7 +49,7 @@ through [`App::handle_event`](app-state.md), so `App` needs no locking.
    `whatsapp::start(session_db_path, wa_tx)`.
 5. Spawns the WhatsApp forwarder.
 6. Starts the optional [web control panel](../integrations/web-control-panel.md)
-   with `start_web_panel` (`src/main.rs:74-79`): nothing happens when
+   with `start_web_panel` (`src/main.rs:88-97`): 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.
@@ -48,40 +64,40 @@ Any failure before the TUI starts aborts with an `anyhow` context such as
 
 ```
  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)
+   "wh-input" thread       Translator task         "wh-web" thread       (AI, image description,
+        |                        |                       |                send, reply timers)
         | Key/Redraw             | WaEvent               | Web(WebRequest)      |
         v                        v                       v                      | AiReply / Sent /
-   app_tx  <------  WhatsApp forwarder (Wa(..))  <-------+                      | CooldownElapsed
-        ^                                                                       |
+   app_tx  <------  WhatsApp forwarder (Wa(..))  <-------+                      | ImagesDescribed /
+        ^                                                                       | ReplyDue
         +-----------------------------------------------------------------------+
         |
      app_rx  -->  event_loop: draw ui::render -> recv -> app.handle_event
 ```
 
-- **WhatsApp forwarder** (`src/main.rs:60-68`): a tokio task that wraps each
+- **WhatsApp forwarder** (`src/main.rs:74-86`): a tokio task that wraps each
   `WaEvent` in `AppEvent::Wa` and stops when either channel side closes.
 - **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`)
+- **Input thread** (`src/main.rs:99-134`): 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:123-129`): only `KeyEventKind::Press` keys
+- **Event mapping** (`src/main.rs:136-147`): 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, cooldown timers) hold a clone of the sender and report with
-  `AiReply` / `Sent` / `CooldownElapsed`.
+- **Background tasks** spawned by `App` (AI completions, image downloads and
+  descriptions, sends, reply-delay timers) hold a clone of the sender and
+  report with `AiReply` / `ImagesDescribed` / `Sent` / `ReplyDue`.
 
 ## TUI loop
 
-`run_tui` (`src/main.rs:135-140`) calls `ratatui::init`, runs the loop and
+`run_tui` (`src/main.rs:149-158`) 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:147-166`) repeats while `!app.should_quit`:
+`event_loop` (`src/main.rs:160-184`) 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.
@@ -95,3 +111,9 @@ list cannot leave stale cells behind.
 Because rendering happens only after an event, the screen is redrawn on key
 presses, resizes, WhatsApp events, web panel requests and background task
 results — there is no timer tick.
+
+## Tests
+
+`src/main.rs` tests check that only `--version` / `-V` request the version
+and that `BUILD_VERSION` starts with the package version and names a commit
+in parentheses. See [Unit tests](../testing/unit-tests.md).

+ 32 - 28
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 the filtered list with global autoreply marker, message formatting, wrapping, the input box and the footer key help.
+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 with key help and build version.
 tags: [ui, ratatui, tui, qr-code, rendering]
 sources:
   - id: openwiki-source-6bddea306b9b87f180f1fc02
     resource: repo://src/ui.rs
-generated: { by: "claude-code", at: "2026-10-20T20:27:06.515Z" }
+generated: { by: "claude-code", at: "2026-10-21T21:49:12.291Z" }
 verified:
   - by: openwiki/0.7.2
-    at: 2026-10-20T20:27:06.515Z
+    at: 2026-10-21T21:49:12.291Z
 ---
 
 # Terminal UI rendering
@@ -22,7 +22,7 @@ All user-facing strings are Italian constants at the top of the file
 
 ## Screens
 
-`render` (`src/ui.rs:105-112`) dispatches on `app.screen`:
+`render` (`src/ui.rs:107-114`) 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:150-163`) centers a block of lines vertically
+`render_centered` (`src/ui.rs:152-165`) 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:190-199`) encodes it with the
+(`WaEvent::Qr`). `qr_to_text` (`src/ui.rs:192-201`) 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:169-183`) replaces the QR with a single line when:
+`qr_block_lines` (`src/ui.rs:171-185`) 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,7 +50,7 @@ dark terminal background.
 
 ## Main screen layout
 
-`render_main` (`src/ui.rs:205-220`):
+`render_main` (`src/ui.rs:207-222`):
 
 ```
 +---------------------------------+--------------------------------------+
@@ -62,67 +62,71 @@ dark terminal background.
 |                                 | Messaggio                             |
 |                                 | > typed text_                         |
 +---------------------------------+--------------------------------------+
- <status>   Tab:focus  Enter:apri/invia  F2:chat/contatti  Canc:elimina  F3:AI chat  F4:AI tutte  F10:esci
+ <status>   Tab:focus  Enter:apri/invia  F2:chat/contatti  Canc:elimina  F3:AI chat  F4:AI tutte  F10:esci      v0.1.0 (e147fe6)
 ```
 
 (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
+- **List pane** (30% width, `src/ui.rs:242-261`): the filtered chats or
   contacts (`App::visible_chats` / `visible_contacts`) depending on
-  `list_mode`. The title (`list_title`, `src/ui.rs:266-275`) is
+  `list_mode`. The title (`list_title`, `src/ui.rs:268-277`) 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
+  `src/ui.rs:291-298`).
+- **Messages pane** (`src/ui.rs:312-348`): 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:492-504`): shows `"> " + input`; when
+- **Input pane** (3 rows, `src/ui.rs:494-506`): 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: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`).
+- **Footer** (1 row, `src/ui.rs:530-573`): split into a left area with the
+  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`,
+  and a right area, exactly as wide as needed, with the dimmed build version
+  `v<version> (<git hash>) ` (`version_label`, from `BUILD_VERSION`, see
+  [Runtime and event loop](runtime-and-event-loop.md)). 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:226-234`).
+`src/ui.rs:228-236`).
 
 ## Message formatting
 
-`format_message_line` (`src/ui.rs:377-397`) renders a
+`format_message_line` (`src/ui.rs:379-399`) renders a
 [stored message](../concepts/history-store.md) as `HH:MM <sender>: <text>` in
 the local time zone; out-of-range timestamps show `--:--`
-(`format_clock_time`, `src/ui.rs:399-408`).
+(`format_clock_time`, `src/ui.rs:401-410`).
 
-`sender_label` (`src/ui.rs:414-431`) picks:
+`sender_label` (`src/ui.rs:416-433`) 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:365-371`): AI messages cyan, own messages
+Colors (`message_style`, `src/ui.rs:367-373`): 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:441-453`).
-`wrap_text` (`src/ui.rs:459-486`) splits on `\n`, wraps each paragraph by
+wide characters (CJK, emoji) count as two cells (`src/ui.rs:443-455`).
+`wrap_text` (`src/ui.rs:461-488`) 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:558-730` cover the footer key help (F4 highlighted
-only when the global autoreply is on), sender labels and fallbacks, clock
+Unit tests in `src/ui.rs:575` onward cover the footer key help (F4
+highlighted only when the global autoreply is on), the version label, 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, the list title (global marker and filter) and selection

+ 46 - 26
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, the per-chat AI autoreply flag and the global autoreply setting, and supports local chat deletion.
+description: The local SQLite database in src/history.rs that stores messages, chat summaries, contacts, the per-chat AI autoreply flag, the global autoreply setting and the AI descriptions of images, 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-20T20:27:06.515Z" }
+generated: { by: "claude-code", at: "2026-10-21T21:49:12.291Z" }
 verified:
   - by: openwiki/0.7.2
-    at: 2026-10-20T20:27:06.515Z
+    at: 2026-10-21T21:49:12.291Z
 ---
 
 # Message history store
@@ -23,13 +23,14 @@ synchronously from the event loop.
 
 ## Opening
 
-- `History::open(path)` (`src/history.rs:65-74`) creates missing parent
+- `History::open(path)` (`src/history.rs:67-76`) creates missing parent
   directories, opens the file and runs the schema script.
-- `History::open_in_memory()` (`src/history.rs:80-86`) exists only under
+- `History::open_in_memory()` (`src/history.rs:82-88`) exists only under
   `#[cfg(test)]`.
 - The schema is applied with `CREATE TABLE/INDEX IF NOT EXISTS`
-  (`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_sql`, `src/history.rs:397-436`); there are no migrations, so an
+  existing `history.db` simply gains new tables (such as `settings` and
+  `image_descriptions`) on next start.
 
 ## Schema
 
@@ -39,62 +40,63 @@ synchronously from the event loop.
 | `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` |
+| `image_descriptions` | `message_id` (PK), `chat_jid`, `description` | AI description of an image message, written once per image |
 
-Rust types (`src/history.rs:26-53`): `StoredMessage`, `ChatSummary`
+Rust types (`src/history.rs:28-55`): `StoredMessage`, `ChatSummary`
 (`jid`, `name`, `last_timestamp`, `autoreply`) and `Contact` (`jid`, `name`).
 
 ## Saving messages and deduplication
 
-`save_message` (`src/history.rs:105-116`) runs in one transaction:
+`save_message` (`src/history.rs:107-118`) 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:380-402`).
+   primary key, so redelivered messages are ignored (`src/history.rs:438-463`).
 2. Only if the row was new, upsert the `chats` row
-   (`upsert_chat_for_message`, `src/history.rs:408-421`): `last_timestamp`
+   (`upsert_chat_for_message`, `src/history.rs:465-483`): `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 (`chat_name_candidate`,
-`src/history.rs:427-437`).
+`src/history.rs:485-499`).
 
 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:122-140`): the latest
+- `recent_messages(chat_jid, limit)` (`src/history.rs:124-142`): 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:148-166`): the
+- `messages_since(chat_jid, since, limit)` (`src/history.rs:150-168`): 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:173-189`): all chats ordered by
+- `list_chats()` (`src/history.rs:175-191`): 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:215-234`): ordered by name,
+- `list_contacts()` (`src/history.rs:217-236`): ordered by name,
   case-insensitive.
-- `display_name(jid)` (`src/history.rs:240-251`): same contact → chat → JID
+- `display_name(jid)` (`src/history.rs:242-253`): same contact → chat → JID
   user part fallback, used for the open chat title.
 
 ## Contacts
 
-`upsert_contact` (`src/history.rs:195-209`) inserts or renames a contact and
+`upsert_contact` (`src/history.rs:197-211`) inserts or renames a contact and
 ignores blank names, so an empty push name never erases a known name.
 
 ## Autoreply flags
 
-- `set_autoreply(chat_jid, enabled)` (`src/history.rs:257-268`) upserts the
+- `set_autoreply(chat_jid, enabled)` (`src/history.rs:259-270`) upserts the
   `chats` row, creating it with defaults if the chat has no messages yet.
-- `is_autoreply(chat_jid)` (`src/history.rs:274-283`) returns the stored
+- `is_autoreply(chat_jid)` (`src/history.rs:276-285`) 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
+  (`src/history.rs:291-317`) 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.
@@ -102,22 +104,40 @@ ignores blank names, so an empty push name never erases a known name.
 New messages never reset the per-chat flag, because the message upsert only
 touches `last_timestamp` and `name`.
 
+## Image descriptions
+
+The [AI autoreply](../workflows/ai-autoreply.md) asks the vision model to
+describe each image once and stores the text, so later replies reuse it
+instead of downloading and describing the image again:
+
+- `message_exists(message_id)` (`src/history.rs:319-332`) tells whether a
+  message is still stored; the app checks it before saving a description, so
+  a chat deleted while its images were being described gets no orphan rows.
+- `save_image_description(message_id, chat_jid, description)`
+  (`src/history.rs:334-349`) upserts the row, replacing a previous
+  description of the same message.
+- `image_descriptions(chat_jid)` (`src/history.rs:351-367`) returns the
+  chat's descriptions keyed by message id; the app uses it to skip images
+  already described and to turn image messages into text when building the
+  AI context.
+
 ## 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
+`delete_chat(chat_jid)` (`src/history.rs:369-394`) removes, in one
+transaction, all messages of the chat, their image descriptions 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:481-763` covers duplicate ids, limit and ordering,
+`src/history.rs:543` onward 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
+`message_exists`, image descriptions (per chat, replaced, deleted with the
+chat), `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, the per-chat AI autoreply flag and the global autoreply setting, and supports local chat deletion.
+- [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, the global autoreply setting and the AI descriptions of images, 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 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.
+- [Quickstart](quickstart.md) - What the wh WhatsApp terminal client with per-chat and global AI autoreply (system prompt in SOUL.md, reply delay, image descriptions) and optional web control panel does, how to build and run it, and where to find each topic in this wiki.
 
 # Directories
 

+ 88 - 57
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, retries with linear backoff, error handling, the optional request log and reply parsing.
-tags: [ai, openai, reqwest, vision, http, retry]
+description: The OpenAI-compatible HTTP client in src/ai.rs — text completions offering the no_reply tool, image descriptions with the vision model (request text from vision.md), image validation, timeouts, retries with linear backoff, error handling, the optional request log and reply parsing.
+tags: [ai, openai, reqwest, vision, http, retry, tools]
+verified:
+  - by: openwiki/0.7.2
+    at: 2026-10-21T21:49:12.291Z
 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-20T20:27:06.515Z" }
-verified:
-  - by: openwiki/0.7.2
-    at: 2026-10-20T20:27:06.515Z
+generated: { by: "claude-code", at: "2026-10-21T21:49:12.291Z" }
 ---
 
 # AI chat-completions client
@@ -23,23 +23,26 @@ turns themselves are built elsewhere.
 
 ## Types
 
-- `Role` (`src/ai.rs:42-58`): `System`, `User`, `Assistant`, serialised as
+- `Reply` (`src/ai.rs:51-58`): the successful outcome of a completion —
+  `Text(String)` to send, or `NoReply` when the model called the `no_reply`
+  tool.
+- `Role` (`src/ai.rs:60-80`): `System`, `User`, `Assistant`, serialised as
   `"system"`, `"user"`, `"assistant"`.
-- `ChatTurn { role, content }` (`src/ai.rs:62-67`): one message of the
+- `ChatTurn { role, content }` (`src/ai.rs:82-87`): one message of the
   conversation.
-- `ImageInput { mime, bytes }` (`src/ai.rs:119-124`): an image for vision
-  requests.
-- `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
+- `ImageInput { mime, bytes }` (`src/ai.rs:141-146`): an image to describe.
+- `AiClient` (`src/ai.rs:89-103`): `reqwest::Client`, endpoint, API key,
+  model, optional vision model, the image description request, optional log
+  path and the retry count.
+- `StatusError { status, body }` (`src/ai.rs:105-122`): 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:102-138`).
+API key and the image bytes (`src/ai.rs:124-160`).
 
 ## Construction
 
-`AiClient::new(&Config)` (`src/ai.rs:147-171`):
+`AiClient::new(&Config)` (`src/ai.rs:169-197`):
 
 - requires `openai_api_key`; otherwise fails with
   `openai_api_key mancante in config.toml`;
@@ -48,67 +51,84 @@ API key and the image bytes (`src/ai.rs:102-138`).
 - builds the endpoint as `openai_base_url` (trailing `/` trimmed) +
   `/chat/completions`;
 - copies `openai_model`, `openai_vision_model` and `ai_retries`;
+- takes the image description request from `vision.md`
+  (`Config::image_description_prompt`), falling back to the built-in
+  `DEFAULT_IMAGE_DESCRIPTION_REQUEST` (`src/ai.rs:22-23`), an Italian request
+  for an objective, concise description of subjects, actions, readable text
+  and context;
 - 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
+No network traffic happens until a request is made. 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`, default `ai_retries` 2, `src/config.rs:34-45`).
+`deepseek-v4.1-flash`, default `ai_retries` 2, `src/config.rs:34-43`).
 
-## Text completions
+## Text completions and the `no_reply` tool
 
-`complete(&turns)` (`src/ai.rs:180-182`) sends
+`complete(&turns)` (`src/ai.rs:199-208`) sends
 
 ```json
-{ "model": "<model>", "messages": [{ "role": "...", "content": "..." }] }
+{ "model": "<model>",
+  "messages": [{ "role": "...", "content": "..." }],
+  "tools": [{ "type": "function",
+              "function": { "name": "no_reply", "description": "...",
+                            "parameters": { "type": "object", "properties": {} } } }] }
 ```
 
-built by `build_request_body` / `build_messages` (`src/ai.rs:325-346`).
+built by `build_request_body`, `build_tools` and `build_messages`
+(`src/ai.rs:343-379`). The only tool offered is the parameterless `no_reply`
+function; its description is neutral ("does not send any message in the
+chat: call it instead of writing a reply") and never says *when* to use it —
+that decision belongs to the system prompt in `SOUL.md`
+(`src/ai.rs:42-49`). The client never executes the tool: calling it simply
+means "send nothing".
 
-## Vision completions
+## Image descriptions
 
-`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`):
+`has_vision()` (`src/ai.rs:210-216`) is true when a vision model is
+configured. `describe_image(&image)` (`src/ai.rs:218-236`) asks the vision
+model for a description of **one** image, independently of any
+conversation:
 
 1. fails with `modello vision non configurato` without a vision model;
-2. validates the image (`validate_image`, `src/ai.rs:385-399`): empty →
+2. validates the image (`validate_image`, `src/ai.rs:408-427`): 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
-   (`build_vision_request_body`, `src/ai.rs:348-369`).
+3. sends a single user message `[{"type":"text", <description request>},
+   {"type":"image_url", ...}]` to the vision model, **without tools**
+   (`build_description_body`, `src/ai.rs:381-392`);
+4. returns the description text; a `NoReply` outcome is turned into the
+   `risposta del servizio AI vuota` error.
 
 The image is embedded as a `data:<mime>;base64,<...>` URL
-(`build_image_part`, `src/ai.rs:371-379`). Validation errors happen before any
-request and are not retried.
+(`build_image_part`, `src/ai.rs:394-406`). Validation errors happen before any
+request and are not retried. The caller stores the description in the
+[history store](../concepts/history-store.md), so each image is described
+once and later replies see only the text.
 
 ## Retries
 
-Both completion methods go through `send_with_retry` (`src/ai.rs:222-236`),
+Both request kinds go through `send_with_retry` (`src/ai.rs:238-259`),
 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
+  `is_retryable` (`src/ai.rs:317-332`) 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);
+  (`src/ai.rs:334-341`), 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.
+and, when enabled, a separate log record.
 
 ## Sending and error handling
 
-`send_body` (`src/ai.rs:248-257`) delegates to `request_reply`
-(`src/ai.rs:265-291`), which posts JSON with bearer auth and returns
+`send_body` (`src/ai.rs:261-280`) delegates to `request_reply`
+(`src/ai.rs:282-314`), which posts JSON with bearer auth and returns
 Italian `anyhow` errors:
 
 | Failure | Message |
@@ -119,42 +139,53 @@ Italian `anyhow` errors:
 | 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:522-524`); the API key is never
+boundary (`truncate_chars`, `src/ai.rs:568-574`); the API key is never
 included.
 
 ## Optional request log
 
 When `ai_log = true` in `config.toml` (default `false`), `send_body` appends
 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
+(`Config::ai_log_path`, `src/config.rs:333-339`) after the attempt completes.
+`format_log_record` (`src/ai.rs:521-547`) writes a header
 `===== <local timestamp> | <endpoint> | modello: <model> =====`, the
-pretty-printed request body and the outcome labelled `RISPOSTA` or `ERRORE`.
+pretty-printed request body and the outcome labelled `RISPOSTA` or `ERRORE`;
+a `NoReply` outcome is logged as `RISPOSTA` with the text
+`(tool no_reply: nessun messaggio inviato)`.
 
 - Image data URLs are replaced by `data:<mime>;base64,<omessi N byte>`
-  (`redact_images` / `redact_data_url`, `src/ai.rs:430-472`); text parts and
+  (`redact_images` / `redact_data_url`, `src/ai.rs:473-519`); 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:510-516`); write failures are ignored so
+  (`append_to_file`, `src/ai.rs:556-566`); write failures are ignored so
   logging never makes a reply fail and never writes over the TUI.
 
 ## Reply parsing
 
-`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.
+`parse_reply` (`src/ai.rs:429-477`) looks at `choices[0]`:
+
+- if any of its `message.tool_calls` is a function call named `no_reply`
+  (`calls_no_reply`), the result is `Reply::NoReply`, even when the message
+  also carries text;
+- otherwise it takes `message.content`, trims it and returns `Reply::Text`;
+  calls to other tool names are ignored;
+- it 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:526-850` covers vision body shapes, missing vision model,
+`src/ai.rs:576` onward covers the description body (request text plus image,
+no tools), the `vision.md` request and its default, 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, the
-request log (image redaction, no key or base64 in records, logging only when
-enabled, append-only writes) and the retry policy (`is_retryable` refuses
-4xx except 429 and accepts generic errors, the `StatusError` message is
+`has_vision`, `Debug` redaction, request body shape including `tools`, the
+`no_reply` tool definition, reply parsing (trimmed text, `no_reply` with or
+without text, other tool calls ignored, missing choices/content), the
+missing-key error, endpoint construction, truncation, the request log (image
+redaction, no key or base64 in records, `no_reply` records, logging only when
+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).

+ 1 - 1
openwiki/integrations/index.md

@@ -1,5 +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, retries with linear backoff, error handling, the optional request log and reply parsing.
+- [AI chat-completions client](ai-client.md) - The OpenAI-compatible HTTP client in src/ai.rs — text completions offering the no_reply tool, image descriptions with the vision model (request text from vision.md), image 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.

+ 3 - 3
openwiki/integrations/web-control-panel.md

@@ -3,9 +3,6 @@ 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
@@ -16,6 +13,9 @@ sources:
   - id: openwiki-source-7b75006986d8b0337adc6ca0
     resource: repo://src/web.rs
 generated: { by: "claude-code", at: "2026-10-20T20:27:06.515Z" }
+verified:
+  - by: openwiki/0.7.2
+    at: 2026-10-21T21:49:12.291Z
 ---
 
 # Web control panel

+ 35 - 43
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-20T20:27:06.515Z" }
+generated: { by: "claude-code", at: "2026-10-21T21:49:12.291Z" }
 verified:
   - by: openwiki/0.7.2
-    at: 2026-10-20T20:27:06.515Z
+    at: 2026-10-21T21:49:12.291Z
 ---
 
 # WhatsApp client integration
@@ -31,7 +31,7 @@ backend of `whatsapp-rust-sqlite-storage` (`Cargo.toml:14-15`).
 
 ## Startup
 
-`start(session_db, &events)` (`src/whatsapp.rs:211-244`):
+`start(session_db, &events)` (`src/whatsapp.rs:208-241`):
 
 1. requires a UTF-8 session path and creates its parent directory;
 2. opens the session store with `SqliteStore::new` (`session.db`, see
@@ -40,7 +40,7 @@ backend of `whatsapp-rust-sqlite-storage` (`Cargo.toml:14-15`).
 4. spawns the `Translator` task and the bot;
 5. when the bot task ends, emits `Disconnected("client stopped")`.
 
-`EventForwarder` (`src/whatsapp.rs:247-268`) only pushes raw events, in
+`EventForwarder` (`src/whatsapp.rs:244-265`) 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` and `AppStateSyncFailed`. All translation happens in
@@ -52,7 +52,7 @@ whatsapp-rust Bot --raw events--> EventForwarder --chan--> Translator --WaEvent-
 
 ## WaEvent
 
-`WaEvent` (`src/whatsapp.rs:81-111`):
+`WaEvent` (`src/whatsapp.rs:81-108`):
 
 | Variant | Meaning |
 |---|---|
@@ -61,23 +61,23 @@ whatsapp-rust Bot --raw events--> EventForwarder --chan--> Translator --WaEvent-
 | `Disconnected(String)` | Connection lost, with reason |
 | `LoggedOut` | Device unlinked; pairing must restart |
 | `OfflineSyncCompleted` | All messages received while the app was offline have been delivered (and, being sequential, already emitted as `Message`); triggers the startup [catch-up](../workflows/ai-autoreply.md) |
-| `Message { message, image, addressed_to }` | A `StoredMessage` (live or history sync); `image` is `Some` only for live image messages; `addressed_to` lists the raw JIDs the message directly addresses (empty for history sync) |
+| `Message { message, image }` | A `StoredMessage` (live or history sync); `image` is `Some` only for live image messages |
 | `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-462`) maps raw events to
+`Translator::handle_event` (`src/whatsapp.rs:413-459`) 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: <code> (expires in Ns)`,
 `connection failure: <reason> (<message>)` (`describe_connect_failure`,
-`src/whatsapp.rs:801-806`), `stream error: ...` and
+`src/whatsapp.rs:796-801`), `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
+**fatally** (`is_fatal_sync_failure`, `src/whatsapp.rs:808-810`), as
 `app state sync incomplete (fatal: [...], retryable: [...])`; retryable or
 skipped collections are retried by the library on a forced reconnect and are
 not shown.
@@ -85,43 +85,37 @@ not shown.
 ## JIDs
 
 - JIDs are stored as `user@server` without device suffix
-  (`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)
+  (`jid_to_string` uses `to_non_ad_string`, `src/whatsapp.rs:825-827`).
+- `canonical_jid` (`src/whatsapp.rs:366-386`) 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.
-- `WaHandle::own_jids` (`src/whatsapp.rs:194-200`) returns this account's PN
+- `WaHandle::own_jids` (`src/whatsapp.rs:191-197`) returns this account's PN
   and LID; it is empty before pairing.
 
 ## Live messages
 
-`handle_live_message` (`src/whatsapp.rs:478-525`):
+`handle_live_message` (`src/whatsapp.rs:475-521`):
 
 - 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:822-825`);
+  and empty names, `src/whatsapp.rs:817-820`);
 - 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:927-934`) reads the `ContextInfo` of the
-first content kind that carries one (extended text, image, video, audio,
-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.
+- emits `StoredMessage` with the WhatsApp timestamp and `is_ai = false`.
+
+Mentions and quotes are not extracted: the message text is passed on as
+rendered, and the [AI autoreply](../workflows/ai-autoreply.md) only rewrites
+`@<own number>` mentions in the text before building the AI context.
 
 ## Contact names
 
 Names come from two sources ranked `Push < Saved` (`NameSource`,
-`src/whatsapp.rs:270-278`): push names from messages and `PushNameUpdate`,
+`src/whatsapp.rs:267-275`): push names from messages and `PushNameUpdate`,
 saved names from `ContactUpdate` (full name, else first 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
+`src/whatsapp.rs:526-540`) and from history sync. `note_name`
+(`src/whatsapp.rs:329-351`) 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.
@@ -129,50 +123,48 @@ persists them.
 ## History sync
 
 Each `HistorySync` chunk is processed in its own task
-(`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
+(`handle_history`, `src/whatsapp.rs:555-575`). Decoding (inflate + protobuf)
+runs in `spawn_blocking` via `extract_history` (`src/whatsapp.rs:704-745`).
+`emit_history` (`src/whatsapp.rs:581-648`) 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
+History messages never carry an image reference (so their images are never
+described); 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:841-853`) turns a protobuf message into
+`describe_message` (`src/whatsapp.rs:836-848`) 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]` (`describe_image_or_audio`,
-   `src/whatsapp.rs:861-873`);
+   `src/whatsapp.rs:856-868`);
 3. caption of other media;
-4. `describe_other` (`src/whatsapp.rs:881-899`): protocol, reaction,
+4. `describe_other` (`src/whatsapp.rs:876-894`): 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:951-1003`): `is_placeholder` (only `[media]` and
+(`src/whatsapp.rs:915-963`): `is_placeholder` (only `[media]` and
 `[messaggio non supportato]`), `is_voice_note`, `is_image` and
 `image_caption`.
 
 ## Sending and downloading
 
-- `WaHandle::send_text(chat_jid, text)` (`src/whatsapp.rs:153-166`) rejects
+- `WaHandle::send_text(chat_jid, text)` (`src/whatsapp.rs:150-163`) rejects
   empty text, parses the JID and returns the new message id.
-- `WaHandle::download_image(&ImageRef)` (`src/whatsapp.rs:173-186`) downloads
+- `WaHandle::download_image(&ImageRef)` (`src/whatsapp.rs:170-183`) 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-131`).
+  and `ImageRef`'s `Debug` omits key material (`src/whatsapp.rs:109-128`).
 
 ## Tests
 
-`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
+`src/whatsapp.rs:965+` covers `is_fatal_sync_failure`, 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).

+ 108 - 65
openwiki/operations/configuration-and-data.md

@@ -1,8 +1,11 @@
 ---
 type: operations
 title: Configuration and data files
-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]
+description: How src/config.rs loads config.toml, the system prompt from SOUL.md and the optional image description request from vision.md, the supported keys and defaults (reply_delay_seconds, catch_up_seconds, ai_retries, web_port), the template files, where the data directory and databases live, the build version, and operational and security caveats.
+tags: [configuration, toml, data-directory, sqlite, security, soul-md]
+verified:
+  - by: openwiki/0.7.2
+    at: 2026-10-21T21:49:12.291Z
 sources:
   - id: openwiki-source-ea70eb6c045047448e446296
     resource: repo://.gitignore
@@ -12,97 +15,132 @@ sources:
     resource: repo://src/ai.rs
   - id: openwiki-source-2a737474d86fc75cc9d9694f
     resource: repo://src/config.rs
-generated: { by: "claude-code", at: "2026-10-20T20:27:06.515Z" }
-verified:
-  - by: openwiki/0.7.2
-    at: 2026-10-20T20:27:06.515Z
+generated: { by: "claude-code", at: "2026-10-21T21:49:12.291Z" }
 ---
 
 # Configuration and data files
 
-All runtime settings come from one TOML file loaded by `Config::load`
-(`src/config.rs:259-263`). No environment variable is ever read
-(`src/config.rs:1-25`).
+All runtime settings come from files in the data directory, loaded once at
+startup by `Config::load` (`src/config.rs:265-281`): `config.toml` for the
+settings, `SOUL.md` for the system prompt and the optional `vision.md` for
+the image description request. No environment variable is ever read
+(`src/config.rs:1-25`). Editing any of them requires restarting `wh`.
 
 ## Data directory
 
 The data directory is fixed: `<platform config dir>/wh`, resolved with
-`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:293-296`).
+`dirs::config_dir()` (`src/config.rs:160-168`). On Windows this is
+`%APPDATA%\wh`, on Linux `~/.config/wh` (file names are case-sensitive
+there). It cannot be changed from the config file. At startup
+`ensure_data_dir` creates it if missing (`src/config.rs:308-315`).
 
 | File | Purpose | Defined by |
 |---|---|---|
-| `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` |
+| `config.toml` | Optional settings | `CONFIG_FILE`, `src/config.rs:54` |
+| `SOUL.md` | System prompt: the responder's identity and rules; required for the AI | `SOUL_FILE`, `src/config.rs:56` |
+| `vision.md` | Optional request sent with each image to obtain its description | `VISION_FILE`, `src/config.rs:58` |
+| `session.db` | WhatsApp session store used by `whatsapp-rust` ([WhatsApp client](../integrations/whatsapp-client.md)) | `session_db_path`, `src/config.rs:317-323` |
+| `history.db` | Local messages, chats, contacts, per-chat autoreply flags, the global autoreply setting and image descriptions ([History store](../concepts/history-store.md)) | `history_db_path`, `src/config.rs:325-331` |
+| `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:333-339` |
 
 The two databases are separate by design: `session.db` belongs to the
 WhatsApp library (linked-device session and keys, schema owned by
-`whatsapp-rust`), while `history.db` belongs to the app (chats, messages,
-contacts, autoreply flags).
+`whatsapp-rust`), while `history.db` belongs to the app.
 
-## Keys
+## Keys in `config.toml`
 
 | Key | Purpose | Default when missing or blank |
 |---|---|---|
 | `openai_api_key` | Bearer token for the AI endpoint (trimmed) | none — the [AI client](../integrations/ai-client.md) cannot be built |
 | `openai_base_url` | Base URL of the OpenAI-compatible endpoint (trimmed, trailing `/` removed) | `https://ollama.com/v1` |
 | `openai_model` | Model for text replies (trimmed) | `deepseek-v4.1-flash` |
-| `openai_vision_model` | Model for image replies (trimmed) | none — image understanding disabled |
-| `system_prompt` | Persona/instructions for autoreplies, kept as written (not trimmed, multiline allowed) | none — AI not configured |
+| `openai_vision_model` | Model that describes images (trimmed) | none — image understanding disabled |
 | `ai_log` | Boolean; when `true` every request sent to the AI model is appended to `ai_requests.log` | `false` |
-| `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` |
+| `reply_delay_seconds` | Seconds the autoreply waits after the first unanswered message of a chat; messages received meanwhile are answered in the same reply; `0` answers at once | `30` |
 | `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` |
 | `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.
+Mapping and defaults: `Config::from_file` (`src/config.rs:284-306`) and the
+constants at `src/config.rs:34-43`. `reply_delay_seconds`,
+`catch_up_seconds` and `ai_retries` are unsigned integers in the file
+(`Option<u32>`, `src/config.rs:121-127`), so a negative value is rejected as
+an invalid value type. `web_port` is `Option<u16>` (`src/config.rs:128-129`),
+so negatives and values above 65535 fail TOML parsing, and
+`resolve_web_port` (`src/config.rs:254-263`) 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).
+
+Unknown keys are ignored. In particular the keys of older versions are no
+longer read: `system_prompt` (move the prompt to `SOUL.md`) and
+`cooldown_seconds` / `group_cooldown_seconds` (replaced by
+`reply_delay_seconds`); a leftover value is silently ignored, not an error.
 
 `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 (`read_config_file`, `src/config.rs:202-212`).
+its default and a comment (with `web_port` commented out) and points to
+`SOUL.md` for the prompt; copy it to the data directory as `config.toml` and
+fill in `openai_api_key`.
+
+## Prompt files: `SOUL.md` and `vision.md`
+
+`read_prompt_file` (`src/config.rs:212-223`) reads both files:
+
+- the content is kept **as written** (not trimmed, multiline, Markdown
+  allowed); only a leading UTF-8 BOM is removed;
+- a missing file or one that is blank (only whitespace) yields `None`;
+- an unreadable or non-UTF-8 file is an error naming the path
+  (`cannot read prompt file <path>`), which aborts startup.
+
+`SOUL.md` becomes `Config::system_prompt`. The binary embeds no prompt text:
+without `SOUL.md` (or with a blank one) the AI is not configured, every
+autoreply path stops and the status line says
+`AI non configurata: imposta openai_api_key in config.toml e il prompt in SOUL.md`
+(see [Application state](../architecture/app-state.md)). `soul.md.template`
+in the repository root is a minimal starting point. The app appends only
+factual context to the prompt (chat kind and name, who writes the `user` and
+`assistant` messages, see [AI autoreply](../workflows/ai-autoreply.md));
+behaviour — including when to call the `no_reply` tool and stay silent —
+belongs in `SOUL.md`.
+
+`vision.md` becomes `Config::image_description_prompt`; when it is missing or
+blank the [AI client](../integrations/ai-client.md) uses its built-in Italian
+description request.
+
+## Loading rules for `config.toml`
+
+- Missing file → all defaults (`read_config_file`, `src/config.rs:196-210`).
 - 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 (`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`.
+  never echoes file content (`parse_config_file`, `src/config.rs:181-194`).
+  This also covers negative or non-integer `reply_delay_seconds`,
+  `catch_up_seconds` 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:234-241`).
+  (`src/config.rs:241-252`).
 - `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)).
 
+## Build version
+
+`wh --version` (or `-V`) prints `wh <version> (<short git commit>)` and
+exits without reading any file; the same text is shown at the bottom right
+of the main screen. The commit hash is embedded at build time by `build.rs`
+and is `unknown` when built without git (see
+[Runtime and event loop](../architecture/runtime-and-event-loop.md)).
+
 ## Security and operational notes
 
 - The API key is never printed: `Config`'s `Debug` shows `***` or `<unset>`
-  (`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.
+  (`src/config.rs:132-158`), and the raw `FileConfig` has no `Debug` impl at
+  all (`src/config.rs:107-130`). 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, one record per
@@ -113,23 +151,28 @@ Startup aborts with `cannot load configuration` on any of these errors
   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.
+- Keep `config.toml` and `SOUL.md` in the data directory, outside the
+  repository. `.gitignore` ignores a root-level `/config.toml`, `.env`,
+  any `SOUL.md` and `*.log` files (`.gitignore:4-18`), 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 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.
+  QR pairing (copying it to another machine reuses the linked session).
+  `history.db` can be deleted to reset local history, image descriptions and
+  the per-chat and global autoreply flags; it is recreated on start.
+- Images downloaded for description are kept in memory only and never
+  written to the data directory; only their text description is stored in
+  `history.db`.
 
 ## Tests
 
-`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, 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).
+`src/config.rs:342+` covers defaults, full files, the ignored legacy
+`system_prompt` key, blank values, trimming, base URL normalisation and
+errors, `Debug` masking, vision model, DB and AI log paths, `ai_log`,
+`reply_delay_seconds` (default, custom values including `0`, negatives
+rejected, legacy cooldown keys ignored), catch-up and `ai_retries` defaults
+and 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, and `SOUL.md` reading (missing, blank, kept as written
+without BOM, non-UTF-8 error naming the path). 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 (including ai_retries and web_port), the template file, 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 system prompt from SOUL.md and the optional image description request from vision.md, the supported keys and defaults (reply_delay_seconds, catch_up_seconds, ai_retries, web_port), the template files, where the data directory and databases live, the build version, and operational and security caveats.

+ 46 - 25
openwiki/quickstart.md

@@ -1,7 +1,7 @@
 ---
 type: overview
 title: Quickstart
-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.
+description: What the wh WhatsApp terminal client with per-chat and global AI autoreply (system prompt in SOUL.md, reply delay, image descriptions) 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
@@ -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-20T20:27:06.515Z" }
+generated: { by: "claude-code", at: "2026-10-21T21:49:12.291Z" }
 verified:
   - by: openwiki/0.7.2
-    at: 2026-10-20T20:27:06.515Z
+    at: 2026-10-21T21:49:12.291Z
 ---
 
 # Quickstart
@@ -46,10 +46,15 @@ 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.
 
+`wh --version` (or `-V`) prints `wh <version> (<git commit>)` and exits; the
+same version is shown at the bottom right of the main screen
+(`src/main.rs:24-41`, `build.rs`).
+
 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, `Enter` opens a chat or
-sends, typing in the list filters it, `F2` chats/contacts, `F3` toggles
+sends, typing in the list filters it, `PgUp`/`PgDn`/`Home`/`End` move
+through the list, `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)).
@@ -57,37 +62,53 @@ autoreply for the open chat, `F4` toggles the global autoreply ("AI tutte"),
 ## 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: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`), AI retries (`ai_retries`), an AI
+`<platform config dir>/wh` (on Windows `%APPDATA%\wh`, on Linux
+`~/.config/wh`), next to `session.db` and `history.db`
+(`src/config.rs:46-58`, `src/config.rs:160-168`); the repository's
+`config.toml.global.template` lists every key. The system prompt is not a
+key: it is the file `SOUL.md` in the same folder (start from
+`soul.md.template`), and the optional `vision.md` replaces the built-in
+request used to describe images. 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, autoreply flags and image descriptions.
+Without `openai_api_key` and a non-blank `SOUL.md` the app still runs as a
+plain WhatsApp client. Optional keys tune the reply delay
+(`reply_delay_seconds`), the startup catch-up (`catch_up_seconds`), AI
+retries (`ai_retries`), the vision model (`openai_vision_model`), 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).
+the web panel port (`web_port`, unauthenticated, all interfaces). The old
+keys `system_prompt`, `cooldown_seconds` and `group_cooldown_seconds` are
+ignored. Keys and defaults:
+[Configuration and data](operations/configuration-and-data.md).
+
+## Autoreply in one paragraph
+
+The first unanswered message of an enabled chat starts a
+`reply_delay_seconds` wait; messages arriving meanwhile join the same reply.
+Images are described once by the vision model and only their descriptions
+reach the AI. The reply is dropped if the owner answered by hand, and the
+model can stay silent by calling the `no_reply` tool, whose use is decided
+by `SOUL.md`. Details: [AI auto-reply workflow](workflows/ai-autoreply.md).
 
 ## Startup in one line
 
-`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
+`main` prints the version and exits for `--version`; otherwise it 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:36-64`, 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, 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` |
+| Startup, version flag, channels, threads, main loop | [Runtime and event loop](architecture/runtime-and-event-loop.md) | `src/main.rs`, `build.rs` |
+| Events, state, keys, filter, chat deletion, reply orchestration, background tasks | [Application state](architecture/app-state.md) | `src/app.rs` |
+| Screens, rendering, message formatting, footer version | [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, retries, errors | [AI client](integrations/ai-client.md) | `src/ai.rs` |
+| AI HTTP requests, `no_reply` tool, image descriptions, 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` |
+| When and how the AI replies (global/per-chat, groups, reply delay, owner-answered drop, images, catch-up, context) | [AI auto-reply workflow](workflows/ai-autoreply.md) | `src/autoreply.rs`, `src/app.rs` |
+| Local SQLite messages, chats, contacts, settings, image descriptions | [History store](concepts/history-store.md) | `src/history.rs` |
+| Config keys, `SOUL.md` / `vision.md`, 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 (including the web panel), 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 and the version flag), what each one covers, how they isolate files, and how to run them.

+ 27 - 20
openwiki/testing/unit-tests.md

@@ -1,8 +1,11 @@
 ---
 type: testing
 title: Unit tests
-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.
+description: Overview of the in-module unit test suites in src/*.rs (including the web panel and the version flag), what each one covers, how they isolate files, and how to run them.
 tags: [testing, cargo, unit-tests]
+verified:
+  - by: openwiki/0.7.2
+    at: 2026-10-21T21:49:12.291Z
 sources:
   - id: openwiki-source-651d1fb6c9e49916a916ab51
     resource: repo://Cargo.toml
@@ -16,16 +19,15 @@ sources:
     resource: repo://src/config.rs
   - id: openwiki-source-ce0c4ea8cd3b15431fa691ce
     resource: repo://src/history.rs
+  - id: openwiki-source-b55a21a31ede1b56cd31a6a6
+    resource: repo://src/main.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-20T20:27:06.515Z" }
-verified:
-  - by: openwiki/0.7.2
-    at: 2026-10-20T20:27:06.515Z
+generated: { by: "claude-code", at: "2026-10-21T21:49:12.291Z" }
 ---
 
 # Unit tests
@@ -38,34 +40,38 @@ touches the network, binds a port or uses a real WhatsApp session.
 ## Running
 
 ```sh
-cargo test                      # whole suite
-cargo test autoreply::          # one module
-cargo test should_reply_false   # by name filter
+cargo test                        # whole suite
+cargo test autoreply::            # one module
+cargo test should_schedule        # by name filter
 ```
 
 ## Suites
 
 | 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` | 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 |
+| `autoreply` | 55 | `src/autoreply.rs:505+` | private/group/self-chat detection, `supports_autoreply` (groups yes, broadcasts and newsletters no), `ReplyGate::should_schedule` rules (disabled, own messages, self-chat, backlog, blank/placeholder text, voice notes and images trigger, pending chat), pending tracking and `take_pending`, `answered_by_owner`, `catch_up_due` rules, `build_turns` (system turn naming private chat, group or self-chat, roles, `is_ai` in the self-chat, context cap, blank texts skipped, voice notes, stored image descriptions vs. unseen images, assistant turns untouched, group sender prefixes), `context_start`, `mark_own_mentions` |
+| `config` | 37 | `src/config.rs:342+` | defaults, full files, ignored legacy `system_prompt` key, blank values, trimming, base URL normalisation, `Debug` masking, DB and AI log paths, `ai_log`, `reply_delay_seconds` (default, custom and `0`, negatives rejected, legacy cooldown keys ignored), 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, `SOUL.md` reading (missing, blank, as written without BOM, non-UTF-8 error naming the path) |
+| `ai` | 27 | `src/ai.rs:576+` | text request body with the `no_reply` tool, image description body (no tools) and its `vision.md`/default request, image validation limits, `describe_image` errors, `Debug` redaction, reply parsing (`no_reply` with or without text, other tool calls ignored, missing choices/content), endpoint construction, AI log records (no key, images redacted, `no_reply` record), logging only when enabled, append-only log writes, retry policy (`is_retryable`, `StatusError` message, linear `retry_delay`), truncation |
+| `whatsapp` | 22 | `src/whatsapp.rs:965+` | fatal-only app state sync reporting, message rendering and placeholders, captions, image references, voice/image detection, silent messages, JID device suffix, push-name filtering |
+| `app` | 20 | `src/app.rs:1443+` | autoreply enable rules (private, self-chat LID, groups; broadcasts, newsletters and missing AI refused), `web_chats` filtering and order, vision applicability, `select_images_to_describe` (last five in arrival order, known and out-of-context images skipped, nothing without vision), `arrived_after`, `manual_reply_after`, `description_failure_status`, list filter matching, selection, page steps and toggle helpers, AltGr handling, fixed function-key codes |
+| `history` | 18 | `src/history.rs:543+` | 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, `message_exists`, image descriptions (per chat, replaced, deleted with the chat), `delete_chat`, contacts, on-disk open |
+| `ui` | 15 | `src/ui.rs:575+` | footer key help (F4 highlight), version label, sender labels, clock time, QR text, wrapping, visible lines, input tail, messages title, list title (global marker, filter), selection clamp |
 | `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 |
+| `main` | 2 | `src/main.rs:186+` | only `--version` / `-V` request the version; `BUILD_VERSION` starts with the package version and names a commit |
 
-Total: 214 `#[test]` functions.
+Total: 205 `#[test]` functions.
 
 ## Test isolation
 
 - History tests use `History::open_in_memory()` (only compiled for tests,
-  `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`).
+  `src/history.rs:78-89`); the on-disk test writes under
+  `std::env::temp_dir()` in a per-process directory (`src/history.rs:837-852`).
 - 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`).
+  `std::env::temp_dir()` and removes it (`src/ai.rs:856-868`).
+- Config tests that read `SOUL.md`-style prompt files write per-process
+  scratch files under the `target` directory and remove them
+  (`scratch_path`, `src/config.rs:349-375`).
 - Web tests exercise the pure routing and parsing helpers; the HTTP server
   itself is never started.
 
@@ -76,4 +82,5 @@ Pages with module details: [AI autoreply](../workflows/ai-autoreply.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).
+[Application state](../architecture/app-state.md),
+[Runtime and event loop](../architecture/runtime-and-event-loop.md).

+ 189 - 164
openwiki/workflows/ai-autoreply.md

@@ -1,19 +1,21 @@
 ---
 type: workflow
 title: AI auto-reply workflow
-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]
+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, the reply delay that batches messages into one reply, the drop when the owner answered by hand, image descriptions stored once and reused, the startup catch-up, building the AI context from SOUL.md, the no_reply tool, retries and sending the reply.
+tags: [ai, autoreply, workflow, vision, reply-gate, reply-delay, catch-up, image-description]
+verified:
+  - by: openwiki/0.7.2
+    at: 2026-10-21T21:49:12.291Z
 sources:
   - id: openwiki-source-9fec9d1af408501c9c455161
     resource: repo://src/app.rs
   - id: openwiki-source-043e6252badc57a1ee6bfb48
     resource: repo://src/autoreply.rs
+  - id: openwiki-source-2a737474d86fc75cc9d9694f
+    resource: repo://src/config.rs
   - 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-20T20:27:06.515Z
+generated: { by: "claude-code", at: "2026-10-21T21:49:12.291Z" }
 ---
 
 # AI auto-reply workflow
@@ -22,154 +24,145 @@ The autoreply answers incoming WhatsApp messages with an AI-generated text,
 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
+[`App`](../architecture/app-state.md); the HTTP calls are done by the
 [AI client](../integrations/ai-client.md).
 
 ```
-WaEvent::Message { message, image, addressed_to }
+WaEvent::Message { message, image }
   -> History::save_message (new only)
   -> 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 && enabled? -> start_reply (text-only)
-       else stop
-WaEvent::OfflineSyncCompleted (first time only)
-  -> App::catch_up -> start_reply (text-only) per due chat
-
-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         (each with up to ai_retries retries)
+       ReplyGate::should_schedule? --yes--> mark_pending + timer(reply_delay_seconds)
+       image from an allowed sender?  ----> keep_image (pending_images / backlog_images)
+  ... AppEvent::ReplyDue
+  -> prepare_reply: reply_context (still enabled? AI? owner not answered last?)
+       --no--> finish_reply (drop)
+       images without description? --yes--> download + describe_image (parallel, max 5)
+                                             -> AppEvent::ImagesDescribed -> store descriptions
+  -> start_reply: messages_since(yesterday 00:00, 200) -> mark @[te] -> build_turns
+  -> AiClient::complete (tools: [no_reply], up to ai_retries retries)
   -> AppEvent::AiReply
-  -> still enabled (global OR per-chat)? --no--> discard
-  -> WaHandle::send_text -> AppEvent::Sent -> stored with is_ai = true
+       NoReply -> status "AI: nessuna risposta necessaria"
+       Text    -> still enabled AND owner did not answer meanwhile? -> send_text -> stored is_ai = true
+  -> finish_reply: newer message arrived meanwhile? -> schedule one new reply
+WaEvent::OfflineSyncCompleted (first time only)
+  -> catch_up -> mark_pending + prepare_reply (no delay) per due private chat
 ```
 
 ## 1. Enabling
 
 There are two independent switches, and a chat is **enabled** when either is
-on (`autoreply_enabled`, `src/app.rs:366-372`):
+on (`autoreply_enabled`, `src/app.rs:521-527`):
 
 - **Per chat** — F3 toggles the flag of the open chat (`toggle_autoreply` /
-  `toggle_chat_autoreply`, `src/app.rs:763-794`); the
+  `toggle_chat_autoreply`, `src/app.rs:976-1007`); 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
+  requires a configured AI (API key in `config.toml` and a non-blank
+  `SOUL.md`, see [Configuration](../operations/configuration-and-data.md))
+  and a chat that supports autoreply (`can_enable_autoreply`,
+  `src/app.rs:1403-1416`), 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`);
+  `global_autoreply` (`toggle_global_autoreply`, `src/app.rs:1052-1069`);
   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.
+The global switch does not bypass the chat-type rule: `meets_content_rules`
+still requires a supported chat, so broadcasts, status and newsletters are
+never answered.
 
-`supports_autoreply` (`src/autoreply.rs:89-97`) accepts private chats and
+`supports_autoreply` (`src/autoreply.rs:99-107`) accepts private chats and
 groups:
 
 - private: JID server `s.whatsapp.net` or `lid` (`is_private_chat`,
-  `src/autoreply.rs:66-76`);
-- group: JID server `g.us` (`is_group_chat`, `src/autoreply.rs:78-87`);
+  `src/autoreply.rs:76-86`);
+- group: JID server `g.us` (`is_group_chat`, `src/autoreply.rs:88-97`);
 - broadcasts (including `status@broadcast`), newsletters and malformed JIDs
   are excluded.
 
-## 2. Trigger
+## 2. Trigger and ReplyGate rules
 
 Only messages that `History::save_message` reports as new reach
-`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: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
+`maybe_autoreply` (`on_message`, `src/app.rs:333-342`; `maybe_autoreply`,
+`src/app.rs:344-366`), 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:628-634`), refreshes the own JIDs if
+unknown and computes `enabled` (global or per chat).
 
-`ReplyGate::should_reply(message, enabled, addressed_to, now)`
-(`src/autoreply.rs:176-194`) returns `true` only when **all** hold
-(`is_reply_candidate` and `meets_content_rules`,
-`src/autoreply.rs:258-282`):
+`ReplyGate::should_schedule(message, enabled)` (`src/autoreply.rs:182-191`)
+returns `true` only when **all** hold (`is_reply_candidate` and
+`meets_content_rules`, `src/autoreply.rs:241-265`):
 
 | Rule | Detail |
 |---|---|
+| Not pending | the chat has no reply already scheduled or in progress |
 | 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`) |
+| Sender allowed | not sent by the user — except human-written messages in the self-chat ("Messaggi a te stesso"); AI messages never trigger (`sender_allowed`, `src/autoreply.rs:172-180`) |
 | Supported chat | private chat or group (`supports_autoreply`) |
 | No backlog | `timestamp >= started_at`, the process start time |
 | Has content | text not blank and not `[media]`/`[messaggio non supportato]`; voice notes and images do trigger |
-| Cooldown | enough time since the last reply in that chat (see below) |
-
-### Cooldowns
-
-`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: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;
-- `private` for private chats and for group messages addressed to the user.
-
-A group message is addressed to the user when one of its `addressed_to`
-JIDs — an `@`-mention or the author of a quoted message — equals one of the
-own JIDs, ignoring a `:N` device suffix (`is_addressed`,
-`src/autoreply.rs:322-332`). Mentioning someone else does not shorten the
-cooldown. In private chats mentions make no difference.
 
 The self-chat is detected by comparing the chat JID with the account's own
-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: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`).
-
-## 4. Deferred reply (private chats)
-
-When a message in a private chat (including the self-chat) passes every
-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:986-997`), a tokio task that sleeps
-until that instant and sends `AppEvent::CooldownElapsed { chat_jid }`.
-
-`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.
-
-## 5. Startup catch-up
+PN/LID JIDs, ignoring a `:N` device suffix (`is_self_chat`,
+`src/autoreply.rs:148-157`; `strip_device_suffix`,
+`src/autoreply.rs:293-309`). Groups follow exactly the same rules as private
+chats; mentions and quotes no longer change anything.
+
+## 3. Reply delay and batching
+
+When `should_schedule` passes, `schedule_reply` (`src/app.rs:368-376`) marks
+the chat pending in the gate (`mark_pending`, `src/autoreply.rs:202-210`)
+and `spawn_reply_timer` (`src/app.rs:1199-1209`) emits
+`AppEvent::ReplyDue { chat_jid }` after `reply_delay_seconds` (default 30;
+`0` answers at once). The chat stays pending until `finish_reply`, so every
+message arriving during the wait — and while the AI is generating — gets no
+timer of its own: the reply is built from the context loaded when it starts,
+so it covers everything received during the wait.
+
+`handle_reply_due` (`src/app.rs:417-428`) takes the images kept for the chat
+and, if the chat is still pending (a local deletion removes the pending
+mark), calls `prepare_reply` (`src/app.rs:430-451`). Its `reply_context`
+(`src/app.rs:529-547`) drops the reply — and `finish_reply` ends the pending
+state — when:
+
+- the autoreply was turned off (both switches) during the wait;
+- the AI is not configured;
+- the chat has no message in the context window;
+- the latest message was written by hand by the owner outside the self-chat
+  (`answered_by_owner`, `src/autoreply.rs:231-239`): the owner already
+  answered, so the AI stays silent.
+
+Otherwise it describes the new images first (see §5) or calls
+`start_reply` at once.
+
+### After the reply
+
+`AiReply` is always followed by `finish_reply` (`src/app.rs:499-519`), which
+clears the pending state and the `answered_up_to` snapshot. If a reply was
+started and the latest stored message is a different one
+(`arrived_after`, `src/app.rs:1307-1314`) that itself passes
+`should_schedule`, a **new** reply is scheduled (with a fresh delay, keeping
+the images received meanwhile); otherwise the kept images are dropped. So
+messages that arrive while a reply is being generated are answered once,
+afterwards, rather than being lost or triggering parallel replies.
+
+## 4. Startup catch-up
 
 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:374-384`) runs `catch_up` once per process (`catch_up_done`;
+(`src/app.rs:567-577`) runs `catch_up` once per process (`catch_up_done`;
 later reconnections skip it).
 
-`catch_up` / `catch_up_chats` (`src/app.rs:386-428`) look at every enabled
+`catch_up` / `catch_up_chats` (`src/app.rs:579-626`) 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:
+`ReplyGate::catch_up_due` (`src/autoreply.rs:267-290`) accepts it only when:
 
 - `catch_up_seconds` (default 7200) is positive — `0` disables the catch-up;
 - the chat is not a group;
@@ -178,94 +171,126 @@ per-chat flag — and take its last stored message.
 - it passes `meets_content_rules`, so a last message written by the user
   (already answered) never qualifies.
 
-Each due chat gets one text-only reply through `start_reply`; when at least
-one was started the status shows `Risposte di recupero: N`.
+Each due chat is marked pending and goes through `prepare_reply` **without**
+the reply delay, together with the images of its offline messages
+(`backlog_images`); offline images of other chats are dropped. When at least
+one reply went ahead the status shows `Risposte di recupero: N`. Offline
+images that arrive after the catch-up already ran are described on their own,
+without a reply (`describe_late_image`, `src/app.rs:400-415`).
+
+## 5. Image descriptions
+
+Images are never sent to the text model. Each image sent by an allowed
+sender in an enabled chat is kept by `keep_image` (`src/app.rs:378-398`) and,
+just before the reply that covers it, described **once** by the vision model
+(`AiClient::describe_image`, request text from `vision.md` or the built-in
+one, see [AI client](../integrations/ai-client.md)). The description is
+stored in the `image_descriptions` table and reused in every later context.
+
+- `select_images_to_describe` (`src/app.rs:1265-1292`) keeps only the images
+  whose message is in the reply context and that have neither a stored
+  description nor one in progress (`describing`), and at most the **5 most
+  recent** (`MAX_IMAGES_PER_REPLY`); older images of the same batch are
+  dropped for good, to cap the cost of albums. Without `openai_vision_model`
+  nothing is described (`vision_applies`).
+- `spawn_image_descriptions` (`src/app.rs:1231-1263`) describes all selected
+  images in parallel. Each download (`download_image_with_retry`,
+  `src/app.rs:1336-1362`) makes up to 3 attempts, each abandoned after 60 s,
+  waiting 2 s × attempt between them; the bytes stay in memory.
+- `handle_images_described` (`src/app.rs:465-497`) stores each description
+  whose message still exists (`History::message_exists`), so a chat deleted
+  meanwhile gets no orphan rows. Failed images stay without description, the
+  others are unaffected, and the status shows
+  `Descrizione immagine fallita (N): <first error>`. If the chat is still
+  pending, the reply then starts (re-checking `reply_context`).
 
 ## 6. Building the context
 
-`start_reply` loads the context with
+`start_reply` (`src/app.rs:645-673`) loads the context with
 `History::messages_since(chat_jid, context_start(now), CONTEXT_MESSAGES)`:
 
-- `context_start` (`src/autoreply.rs:476-497`) is local midnight of
+- `context_start` (`src/autoreply.rs:459-468`) is local midnight of
   **yesterday** (falls back to `now` minus two days if local time cannot be
   resolved);
 - `CONTEXT_MESSAGES` = 200 (`src/autoreply.rs:13-15`) caps the window,
   keeping the most recent messages in ascending order
   ([History store](../concepts/history-store.md)).
 
-`build_turns(system_prompt, messages, context)`
-(`src/autoreply.rs:499-526`):
-
-- first a `System` turn: the configured prompt followed by a role note
-  (`role_note`, `src/autoreply.rs:20-27`, `src/autoreply.rs:467-474`).
-  `ROLE_NOTE_CHAT` tells the model that `assistant` messages were written by
-  the account owner it answers for and `user` messages by the contact or,
-  in groups, by the participants named in front; `ROLE_NOTE_SELF_CHAT`
-  says that in the self-chat `user` is the owner and `assistant` its own
-  previous replies;
-- then the last `CONTEXT_MESSAGES` messages, skipping blank texts; the last
-  one is the newest message of the chat;
-- roles (`turn_role`, `src/autoreply.rs:367-377`): in normal chats and
+It records the last context message in `answered_up_to`, replaces every
+`@<own number or LID>` in the texts with `@[te]` (`mark_own_mentions` /
+`replace_mention`, `src/autoreply.rs:159-170`, `src/autoreply.rs:438-457`;
+a longer number is never matched) and calls
+`build_turns(system_prompt, messages, TurnContext)`
+(`src/autoreply.rs:482-503`) with the chat name and the stored image
+descriptions:
+
+- first a `System` turn (`system_turn_content`,
+  `src/autoreply.rs:420-436`): the `SOUL.md` prompt as written, then — in
+  normal chats and groups — a line `Chat privata con «name».` or
+  `Chat di gruppo «name».` and `ROLE_NOTE_CHAT` (`assistant` messages were
+  written by the account owner it answers for, `user` messages by the
+  contact or, in groups, by the participants named in front); in the
+  self-chat only `ROLE_NOTE_SELF_CHAT` (`user` is the owner, `assistant` its
+  own previous replies). Parts are separated by a blank line;
+- then the last `CONTEXT_MESSAGES` messages, skipping blank texts;
+- roles (`turn_role`, `src/autoreply.rs:311-321`): in normal chats and
   groups own messages are `Assistant`; in the self-chat, where every message
   is `from_me`, only `is_ai` messages are `Assistant`;
 - in groups every `User` turn is prefixed with `"<sender>: "`, where the
   sender is the trimmed push name or else the user part of the sender JID
-  (`sender_label`, `message_to_turn`, `src/autoreply.rs:427-465`);
-- only `User` turns are rewritten (`src/autoreply.rs:388-425`):
-  - voice note: the triggering one becomes an instruction to reply
-    courteously that it cannot be listened to and ask to write it; older ones
-    become a short history note;
-  - image: the triggering one becomes "reply to the attached image" when
-    vision is used, otherwise an instruction to say it cannot be seen and
-    ask for a description; older ones become a history note; a caption is
-    appended as `Didascalia: ...`;
+  (`sender_label`, `message_to_turn`, `src/autoreply.rs:381-418`);
+- only `User` turns are rewritten (`user_content`,
+  `src/autoreply.rs:364-379`), with plain facts — how to react is left to
+  `SOUL.md`:
+  - voice note → `[Il contatto ha inviato un messaggio vocale che non puoi ascoltare]`;
+  - image with a stored description →
+    `[Il contatto ha inviato un'immagine. Descrizione: <text>]`, without one →
+    `[Il contatto ha inviato un'immagine che non puoi vedere]`; a caption is
+    appended as `\nDidascalia: <caption>`;
   - any other text is kept as is.
 
-`TurnContext { self_chat, vision }` (`src/autoreply.rs:379-386`) carries
-the two switches.
+## 7. Request, `no_reply` and retries
 
-## 7. Vision path, fallback and retries
+`spawn_ai_request` (`src/app.rs:1211-1229`) calls `AiClient::complete`, which
+offers the model a single parameterless tool, `no_reply`. When the model
+calls it, the outcome is `Reply::NoReply`: nothing is sent and the status
+shows `AI: nessuna risposta necessaria`. The app never says when to use it —
+the cases belong in `SOUL.md`.
 
-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: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, 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: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
+Every AI call (text completion and image description) 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`.
+the checks in §8 happen after all retries. The request task is awaited by a
+second task, so `AiReply` is always delivered and the chat never stays
+pending. 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: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: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: ...`).
+A text reply is handled by `send_ai_reply_if_enabled`
+(`src/app.rs:703-722`), which re-checks two things:
+
+- `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`;
+- `owner_answered_meanwhile` (`src/app.rs:724-735`): if a hand-written owner
+  message follows the last message of the reply context
+  (`manual_reply_after`), the reply is discarded with
+  `Risposta AI scartata: hai già risposto`.
+
+Otherwise the text is sent with `spawn_send(..., is_ai = true)`; on success
+`handle_sent` (`src/app.rs:737-765`) stores it as an own message with
+`is_ai = true`, which keeps it from re-triggering the self-chat and from
+counting as a manual answer. 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, `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).
+The gate rules, pending tracking, catch-up, mention marking and turn building
+are covered by `src/autoreply.rs:505+`; enable rules, `web_chats`, image
+selection, `arrived_after`, `manual_reply_after` and the description failure
+status by `src/app.rs:1443+`; image descriptions and the global flag storage
+by `src/history.rs`; the `no_reply` tool, descriptions and 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 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.
+- [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, the reply delay that batches messages into one reply, the drop when the owner answered by hand, image descriptions stored once and reused, the startup catch-up, building the AI context from SOUL.md, the no_reply tool, retries and sending the reply.