Browse Source

Enhance documentation and update metadata for workflows

- Added verification metadata to configuration and quickstart documents.
- Updated generated metadata in unit tests and AI auto-reply workflow.
- Improved descriptions in AI auto-reply workflow to include today's date context.
- Adjusted references and line numbers in the relink and pairing workflow for clarity.
- Corrected test function counts and descriptions in unit tests documentation.
softword 3 days ago
parent
commit
fa84abea28

+ 111 - 69
openwiki/.claims/architecture/app-state.json

@@ -1,14 +1,14 @@
 {
   "schemaVersion": 1,
-  "pageVersion": "sha256:37292ba995a3a70e902fd75c163ea0039a8724324712200d9fa529205b98e321",
+  "pageVersion": "sha256:147e2ba13ec4a1f2727c1e580a0935503d6213e3aabb9bce1cf520e31c348e78",
   "claims": [
     {
       "id": "claim_daef343b267c45a987738a3daf7e404d",
-      "statement": "AppEvent has variants Key, Redraw, Wa, AiReply, ImagesDescribed, AudiosTranscribed, ReplyDue, Sent, Web, ConfigReloaded and Relinked; AiReply carries a Reply or an error, ImagesDescribed carries a DescriptionTarget plus one result per image, AudiosTranscribed carries the chat JID, a then_reply flag, one result per audio and the postponed images, Web carries a WebRequest from the web control panel, ConfigReloaded carries the reloaded Config or the load error, and Relinked carries the relink outcome.",
+      "statement": "AppEvent has variants Key, Redraw, Wa, AiReply, ImagesDescribed, AudiosTranscribed, ReplyDue, Sent, Web, ConfigReloaded, Relinked, AvatarFetched and AvatarFetchFinished; AiReply carries a Reply or an error, ImagesDescribed carries a DescriptionTarget plus one result per image, AudiosTranscribed carries the chat JID, a then_reply flag, one result per audio and the postponed images, Web carries a WebRequest from the web control panel, ConfigReloaded carries the reloaded Config or the load error, Relinked carries the relink outcome, AvatarFetched carries a chat JID and an optional picture URL, and AvatarFetchFinished carries an optional error.",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L117-L169",
-          "version": "repo-lines-v1:sha256:4e60f6fe2040a9712b61baea9f6be8ae53ca7b98d9ec4ad1f2fac5320b51dd4b:eyJzZWxlY3RlZExpbmVDb3VudCI6NTMsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImI4OWMyOWE3NjJmOGZhMmEzZmY0NmY5MGZiZDY3N2QyYTI3MjViMjM3NTYxNjIwODdkOTRmMWI2MDg2Yzk5YzEiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjZmMGExNjA2YWZlMjU3OTU2NWMyMWNkOGU1ZjUyNzljMmY0ZWMwZWMyMjZmZjM5YjljNmY2MjllZTNkYjk0NTEiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjJlYzJiMDUyMWI2NDA2YzM3MGE3MzdmMTZhYTU0NDBkZDcyZDE1YWMxY2E2OGY5MmI1ZDc5NzUxM2I0NDk0NGQifQ"
+          "resource": "repo://src/app.rs#L123-L171",
+          "version": "repo-lines-v1:sha256:c9b28bdffc1670acc97aacd9dee4367dbf482cfee5caaa5272d6e3db226a9905:eyJzZWxlY3RlZExpbmVDb3VudCI6NDksImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImI4OWMyOWE3NjJmOGZhMmEzZmY0NmY5MGZiZDY3N2QyYTI3MjViMjM3NTYxNjIwODdkOTRmMWI2MDg2Yzk5YzEiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImU2ZDg5NzY3YjlhZDdmZmE1ZmFlZWRkZjYwYTdiNDE4NmUzNTk1YzVkOWY3NDNiNDlmNTk4MzZiMTYwYjBkYjEiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImJlMzFiMTJlM2U2OTYyZDc0YjRiYzMwNTNkYmM2MTZjZmI3MWM0Y2YzNjllYTc0MjYyM2Y4ZDliZTVmNzJlODYifQ"
         }
       ]
     },
@@ -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#L171-L191",
+          "resource": "repo://src/app.rs#L190-L210",
           "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, no open chat, no pending deletion or relink, empty image and audio bookkeeping, and a ReplyGate created at the current time.",
+      "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, no open chat, no pending deletion or relink, empty image and audio bookkeeping, an empty avatar cache with no fetch running and wa_connected false, and a ReplyGate created at the current time.",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L269-L317",
-          "version": "repo-lines-v1:sha256:9c70d97117149450bc5590493b9c129d6759a93c1d30ddfc08f8f3cf47891eec:eyJzZWxlY3RlZExpbmVDb3VudCI6NDksImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImRjOTk1ZDBmZjIyZDRiZjUyYjZjNDM1YjMxY2Q0ZGFhM2U1OGE4ZjVkMzAxMzgwMjk1MDAyZWY2MDMzOWIwZjYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjQ5NWU2MTMyNDM5Mjg3ZWRkNTY5ZGNmZDI3Y2JkODAyYTBjOWE4ZjYyZWUyNzM0NGQ0NDc5Y2U5YzJkM2IzN2UiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImU3M2U3ZTFjNDIxZDk5NzQ5ZWVmNzNjNjJiNmE4ODAyODhjNGNhZDM4ZTYwZWQyMTNjNmM1ZDI1ZjAyMDU3OGMifQ"
+          "resource": "repo://src/app.rs#L299-L345",
+          "version": "repo-lines-v1:sha256:b5ce83438d3242f2cb2bb5c0f88fb0f64f79cdf6a06bb35eb67d2d314b00e2e6:eyJzZWxlY3RlZExpbmVDb3VudCI6NDcsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjdmNTYzZjU4ZTBlOWQyYjgxMjgwMTZhOWEyZGZhMTU2YjgzNTM0NWYzOTkzYTVhYjdkNjVhNWU4ODFmMDg0Y2UiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjBhOGJlZTA0NjIzODBmZjdmN2Y0ZDZhYzE2MGViODRmYjk3YjgwZGRjMjM2ZGFmZWY4NTQ4OTYxYjcwZDQ4ZmQiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImU3M2U3ZTFjNDIxZDk5NzQ5ZWVmNzNjNjJiNmE4ODAyODhjNGNhZDM4ZTYwZWQyMTNjNmM1ZDI1ZjAyMDU3OGMifQ"
         }
       ]
     },
     {
       "id": "claim_bf08f07b96ed49c2b688852a35a263f5",
-      "statement": "handle_wa_event maps Qr to Pairing, Connected to Main with status Connesso and an own-JID refresh, LoggedOut to Connecting with the QR cleared and status Sessione scollegata, Disconnected and Error to a status text only, OfflineSyncCompleted to the startup catch-up, and routes Contact and Message { message, image, audio } events.",
+      "statement": "handle_wa_event maps Qr to Pairing, Connected to Main with wa_connected set, status Connesso and an own-JID refresh, LoggedOut to Connecting with wa_connected cleared, the QR cleared and status Sessione scollegata, Disconnected to a cleared wa_connected plus status text, Error to a status text only, OfflineSyncCompleted to the startup catch-up, and routes Contact and Message { message, image, audio } events.",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L436-L463",
-          "version": "repo-lines-v1:sha256:6cb99e0ed4d9cc3d67855b607fc8f10d28c57e26bd5863778dfad3aa44c3ce88:eyJzZWxlY3RlZExpbmVDb3VudCI6MjgsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjgwNjYyNjdlNzM4MTkwMDlkOWYwMTZiM2IwMmY0YWEwMTM4MzI1MDUyZjg2ODA4M2VlZmZlMGI4MTQzNGI1YzgiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjdkNTdjYjhjOGU1NTg3MTQ3MDZiYTY4NDc1YTBiNTMzOGQwYzYyM2VjOTBhOWEyMDk1OTg1ZDAxMmIyZGZmZjkiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImI2ZDczY2I1NTUwODdmZGQwZTc4MzhjZjM4MWZmZmRmMzljMzk2M2VmZWJhMGE0ZjQyNzg4OWE0MGUwODYwZWQifQ"
+          "resource": "repo://src/app.rs#L480-L509",
+          "version": "repo-lines-v1:sha256:8a4974b7c3714117e1408346a1807bdca9e5aaaf4fd30c7c9b99b19cb1e8f212:eyJzZWxlY3RlZExpbmVDb3VudCI6MzAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjZkOGNmMDJkODU2OWY1YTQ3ZjRiNmY0Zjc5MzBmNzkzMTczM2VmNGE4NDMwODNmYmZjMzAzNmNiM2JiMjc5MGQiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImM5NDkyZWM5YTM2ZThkODU0NjE1MmI4MjJkNTdmZDIwMDk4OTE1MDMwYTIwZDliZGExZDVmYWQ0NGY4ZjFiNWYiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImI2ZDczY2I1NTUwODdmZGQwZTc4MzhjZjM4MWZmZmRmMzljMzk2M2VmZWJhMGE0ZjQyNzg4OWE0MGUwODYwZWQifQ"
         }
       ]
     },
@@ -47,7 +47,7 @@
       "statement": "on_message returns early when History::save_message reports the message was not new.",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L485-L499",
+          "resource": "repo://src/app.rs#L531-L545",
           "version": "repo-lines-v1:sha256:a70d770a3f3dc3ed4a57db8f2b971da1b9f50afa36fc45185c4f5c34bcf1eba0:eyJzZWxlY3RlZExpbmVDb3VudCI6MTUsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjBlYWIwYmIzYTMyYTBlZGUzMGY0ZTk1MmQ1MjcxYzRhMTgwMTZjMGU4Y2M4N2VhYmE1ODA1OTYyZDNjOGE4NTYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjgyODRmZjk5YWRlYTg2MmRlOTFhNTQwZTNjOTcwNzc4NWU0ODY1ODg2YjZlOTFmMzFiNzdmNzdlOGQzNzI4OGMiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjFjMGQxZmNhMmFmODQ5N2RiMDdiZDk4NWM2YjJkNmQwOGM2MjA2ZDNiN2E4ODY3MjcyNGUwM2I4YzI3YTU5OWUifQ"
         }
       ]
@@ -61,7 +61,7 @@
           "version": "repo-lines-v1:sha256:683c16567f4074fbe88d85ef8581c16739d5ca0531826b0fb49c5df95ac18f20:eyJzZWxlY3RlZExpbmVDb3VudCI6MTAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImJkMTkzOTVkMDA1MWQ2YTA4MzBkMmQ5OWZhNTk4ZDU4N2EyOWZkNTA0OTkwNGQ1OWNiNTI2YmQ3Yzc4ODBiMGUiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6ImFmNjIzNzA4OWI5MWQxMDdiNjY0NTc5NzNhOGJhMmE3MWQ4NzA0NGVhOTFlOTE4Y2RlMjA1Zjg2YWNkNDZjOTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjFlZmRkMDA4YTE2MzY2YTNmMTdiNjY1M2IyNzcyMThkYzQ2NTRiMGFjYTE5MGI1YmFiZDcwNmIyM2IxMDQwNzciLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImI2ZmI3YjljNTEzODFiZmNlNmIwYzU4ZTY0ZmZhMjhjODMyNjU5NTkyOWQ1MDNlYTUwYjBlMDRiNTk0N2QwNTMifQ"
         },
         {
-          "resource": "repo://src/app.rs#L1122-L1167",
+          "resource": "repo://src/app.rs#L1171-L1216",
           "version": "repo-lines-v1:sha256:6e737f1163f7fc1e0f873a9756a9a2c32d247edaf022b24c9d62e4f5d3ecdc71:eyJzZWxlY3RlZExpbmVDb3VudCI6NDYsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjlhZGRmNjgzMjdhYTA4ZWZjNDYxNGFmZDIwOTkzZTFkNzZiZjUxNmQ3NzRjM2QyMmIzOTUyOTEwNmNkOTBhZmMiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjE2MmVlNTdlNDFhOGMyMGFlMDE2NGRmMjYxNDM5ZGNjNDk5ZDM1MjA4MzQxNTE1MTZkMDQ3ZThkZWFlOGE0NjYiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6Ijk2ZWU5ZDllODg2ODFmN2JmM2M4ZmZiZjEzZDgyODk2ODM4MGY2Y2E0M2Y1ZGE4ODI5MmU1MjY3ZDRjMDhlOWYifQ"
         }
       ]
@@ -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#L1305-L1329",
+          "resource": "repo://src/app.rs#L1354-L1378",
           "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#L1894-L1902",
+          "resource": "repo://src/app.rs#L1968-L1976",
           "version": "repo-lines-v1:sha256:ba5bea3063970ee3b66b2d54fc0529a98bf3972bf7e9a5cab37c6d0bf4659ff7:eyJzZWxlY3RlZExpbmVDb3VudCI6OSwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiZmFmNzM0NDVmZTU0ZTJjMzIyMDBiMGRiNDhlM2MyOGZlZDgyZDk3NWY0Mzg4MjM0NjA5NjhlZmM4ZmU1ZjJmOSIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDEyY2EzNDVjY2Y3NWJmOWMwODA2YmNlNjk1YmU4ZGU4MDhiNzk5ODQyNTFhN2E1NGQyMDJjZjYxMDFkZDQ1MSIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiOWM5Yjc1NjM5OGI3MTI0NGNmYThkOTk4ZGRlOTQwZWRmODk1MmNkMDEwMTYyMzM1YmEwMzRjYjkxZGZmMjRiZSIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiNDQwM2Q5ZmY0OGI3ZmI2ZDcyM2RlNWM5YjMxODkzMTNlMjQ2NzBhMWE5NjJhYWUxZTYxODRhMjllZmM3MWM1ZCJ9"
         },
         {
-          "resource": "repo://src/app.rs#L1365-L1378",
+          "resource": "repo://src/app.rs#L1414-L1427",
           "version": "repo-lines-v1:sha256:cbcb0d80e707fedd21b3de7081436677468c63c025abb23dab5e5a6ab0ad90cc:eyJzZWxlY3RlZExpbmVDb3VudCI6MTQsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjA0ZTRlYmRmYjA5YmI0NTM3YzBhZDM0MWUxNGY5NTkzYjEwYWE5ZWJiOTM0ZTNiOGJhMGViY2ZiYjlhYjBkZmYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjI1Zjg5YTg3NjBkYWQ3NjUxYmZiZGQ4ZGFkYTM4OGRmZDUwZmU2NDczOGUwMzM4YjIzZTM5NzM4Y2Y1NTRmOWUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImUzZWNmNGIzNzRlN2M0ODFlZjUzODQ1Y2Y1YjlmNDI5MDk3NmQxYTlmNDlkOWY5NWJjNjIxNWEyY2VhMDYyOTAifQ"
         }
       ]
@@ -95,7 +95,7 @@
       "statement": "Opening the selected entry of the filtered list (resolved by selected_jid) loads its display name, autoreply flag and the last 200 messages and moves focus to the input.",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L1446-L1473",
+          "resource": "repo://src/app.rs#L1520-L1547",
           "version": "repo-lines-v1:sha256:e52bd5fba06c8fa9c17319299e938b8922b2b431f8015138d79e1dca4d4c26d6:eyJzZWxlY3RlZExpbmVDb3VudCI6MjgsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQwZDJhY2QwNjZiMWU0MDM4ZGMyYzllOTI0YTAwNzlkZDNmOGNlNjllZjY4OTZlMTYzYzYyZThhMDlmYTQzYjAiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjE2MmVlNTdlNDFhOGMyMGFlMDE2NGRmMjYxNDM5ZGNjNDk5ZDM1MjA4MzQxNTE1MTZkMDQ3ZThkZWFlOGE0NjYiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImNjYzVlMzg4ZThmOTY0ZGY1OTFjZDM1Y2FlZjk5ODM0MmRjNTllNGU5OTMwMmMyYmI0MjMyZmMxMzdhM2Q1ZjkifQ"
         },
         {
@@ -109,7 +109,7 @@
       "statement": "spawn_send sends the text via WaHandle::send_text and emits AppEvent::Sent.",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L1863-L1872",
+          "resource": "repo://src/app.rs#L1937-L1946",
           "version": "repo-lines-v1:sha256:946a66552037dc4ae7eff9cf6c602520fd019e2700d8d6cc4dc93c7db36bb837:eyJzZWxlY3RlZExpbmVDb3VudCI6MTAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImFlOWMwM2FlM2RiZTlkZDA3YjYxNTZkZWFkNzBhNmRhYjc4MzI3M2I2Y2E0Y2JlZGI0M2E0MTI4YmJlNTdiZjgiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjMwZDEyZTUxMmY3N2FlNTEwYWQwZTk5M2I3NGNmNjdmY2ExYmRiOTllYmE3OWY3OTdjZDkxMWYyN2Y2MWQ1ODQiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjUyM2IxZmMxYjIzYjFmMWZhZWVhY2E4MTM2Yzc1YmFkOGEwNGEzMGQ5Nzk0MTc5ZDNjZjUxYjJkNGVhMDI1NTIifQ"
         }
       ]
@@ -119,7 +119,7 @@
       "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#L1064-L1090",
+          "resource": "repo://src/app.rs#L1113-L1139",
           "version": "repo-lines-v1:sha256:a05110c5a383be3518256a8f574d7c2b6dfa7ab90b8f3a2d19fbed62b9ec6e14:eyJzZWxlY3RlZExpbmVDb3VudCI6MjcsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImVlODZiMzQ5MjhkN2ZlZWQzNTRiNmY1OGM3MjJlNTk5YzI3ODhhNWE0Yzc2MjUxN2YzYzExYzIzMDM0OGVjOTUiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6Ijc3ODkyYWI4MWM1MjU3OGQ5NjU4YWE4MmNmNDU0NjAyY2VmYTA4MDJiNmMzYzRkYWE4OWM4MjUwZDBlZDBjMjUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImVlYjQ1ODA1ZTI5MzcwMzMyZTY2ZDA3NzAwZjlhMDg3MDgyNWE3ZDM2NWFmYmJmNzYxOWNiYjNlMjNiNTViMWQifQ"
         }
       ]
@@ -129,7 +129,7 @@
       "statement": "build_ai returns no client when the system prompt is missing.",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L1879-L1887",
+          "resource": "repo://src/app.rs#L1953-L1961",
           "version": "repo-lines-v1:sha256:02a6b90c8547969f5229ba908e64ec2f9fd8d0547f3a7b34404429320b337fe4:eyJzZWxlY3RlZExpbmVDb3VudCI6OSwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiNzcyNTRhNjcwYWJhNGU4ZWFiNDlmM2U1OWYzNjc4NTk0OWM2ZDk1OTFlN2ZkMWY1NjA4NGU3NzBlOWM3NDJlNCIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDEyY2EzNDVjY2Y3NWJmOWMwODA2YmNlNjk1YmU4ZGU4MDhiNzk5ODQyNTFhN2E1NGQyMDJjZjYxMDFkZDQ1MSIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiMGQ3OWM4ZDU3OGFjNjM3NDFmOTgwMTlmMzlmOWRmMDA4M2RkN2YzM2U0ZDNlOWU3ODU1YzZmNmQ2MWQzNjg0MCIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiNTA2ZDU0NTM1OGJmMGQyNTFiNDhiNWZhZTE2YzIyZjkzYzgzNDM5ZTQwNzA0YzRkYzg1MzgyN2ZmNWFiNmYyNiJ9"
         }
       ]
@@ -139,18 +139,18 @@
       "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#L1932-L1939",
+          "resource": "repo://src/app.rs#L2032-L2039",
           "version": "repo-lines-v1:sha256:a7e4215d1055bcf132d529dd3c057ef20525fbb22d5a1301c6980d41107b5124:eyJzZWxlY3RlZExpbmVDb3VudCI6OCwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiYzllOWIxZTI5MGI4MzMyZDI5YTMwZjc1N2JjN2E3N2ZlYjZmMjAxYjNjMmM5M2ZiNTQ4MDU1YTFhODVmN2MyZSIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDEyY2EzNDVjY2Y3NWJmOWMwODA2YmNlNjk1YmU4ZGU4MDhiNzk5ODQyNTFhN2E1NGQyMDJjZjYxMDFkZDQ1MSIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiZDg4NTkyMTBlYmNiMTJjZjY4YzYxNjIyMmZjNTYwYzFmNjhlMmZhMzFmNjMwNDkzZGY4MDk1ODg4N2Q1N2FmOSIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiN2JjOThkNmIwZGM0OTJjNTAxNmRmMTA4MDVhM2E1NGViOWMxODRhNWE2MTM3ZTdmNGJlY2ZhMGMyMjA1NTBkMCJ9"
         }
       ]
     },
     {
       "id": "claim_bc67b9414f7d4100abf86082fa0b5681",
-      "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.",
+      "statement": "start_reply records the last context message as answered, marks own mentions, reads the chat name and stored image descriptions, formats today's local date with today_label, builds the turns with that date in the TurnContext and spawns the AI request.",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L1009-L1028",
-          "version": "repo-lines-v1:sha256:0a937f62450210fb1b4a45c2eb392c8533cab8fc5e7f2ca88caec3ba64669dbb:eyJzZWxlY3RlZExpbmVDb3VudCI6MjAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImZkMjA2ZWQ4NGMzNGNhMWE0ZmNmZmQ1NmFlOTBiNDgyZDIyNGM2OWQ0ZmRlMDY4MmI2MTllODNlMDRlNWJhYjYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjEyZTA5MzVlNDEzNWQ5MWEyZGFlMDUxOGE3ZTY5NjQ0MmIxYmM5ZGYxOThiNDA1ZmVmMGQzMjUwY2VmOWU1ZWQiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImQwMjI2ZGQ4YmZkY2JkMDhlNjUxMjUzOGY4MmU5YWNiOTEyNzg2NTgwNDEwMmQ2YjRlMzQ4NmRmMjU4YjMxNmYifQ"
+          "resource": "repo://src/app.rs#L1046-L1077",
+          "version": "repo-lines-v1:sha256:9754bc4dd70e320cf8ab979a40761a07ff1ea98f33092b8c9a74a5d6309f8bda:eyJzZWxlY3RlZExpbmVDb3VudCI6MzIsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImFiNzVjM2VmNWFmYTk2NDEwYWI1MGM2YWYwYmM5ZTQ2OWNiNmIxZmViYWM1NmNkYmU1ZDFlMmI3NDdiOWFiYzAiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjNlNTg3NTIyOTQ0NGRkYzY4MDUyNGYwZWYyYTBkMWEwZDkxOTkyMDllMWEyYmJhYzA2NzVkYzgwYjA2MTk3YmQiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImQwMjI2ZGQ4YmZkY2JkMDhlNjUxMjUzOGY4MmU5YWNiOTEyNzg2NTgwNDEwMmQ2YjRlMzQ4NmRmMjU4YjMxNmYifQ"
         }
       ]
     },
@@ -159,7 +159,7 @@
       "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#L501-L542",
+          "resource": "repo://src/app.rs#L547-L588",
           "version": "repo-lines-v1:sha256:182b47e758726e40d650a8411663c9043000bdb61933958b51b7e604c5899fe7:eyJzZWxlY3RlZExpbmVDb3VudCI6NDIsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImI3YzcyYzFlOTMzNjgyYzYyY2MyOGVmNzQ1NDMxNDM5OTZiYWVlNTg4NmFkM2I0YzEyNmNkNDI0OGU5MWY4MTAiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImNmMTVlOGU1NjQ1ZDM5YWE4YzRjNmIwZjMwYjAxYjEwYmNmYjYzMWFlOWI3ZDBjNGNkMjFjMzAwZjQ3ODVhNWMiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjBmZDk1MWYzMzMxMmIwZGIxOTkwNzNjZDIzNDA4MWM3MzMyNDIxZjYwZjRjNmQ4MDA3YmU2YjM1YzMwODYxNGUifQ"
         }
       ]
@@ -169,7 +169,7 @@
       "statement": "On ReplyDue the kept images and audios 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 transcription of audios still lacking one (postponing the images, the reply follows AudiosTranscribed), otherwise spawns the description of images still lacking one (the reply follows ImagesDescribed), or starts the reply at once.",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L628-L679",
+          "resource": "repo://src/app.rs#L674-L725",
           "version": "repo-lines-v1:sha256:c98f9109631c043cce400a98fdab67562e0b19285fc5165cca7a94c260fee718:eyJzZWxlY3RlZExpbmVDb3VudCI6NTIsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjhhMGZkOGQ4YmVjYTA3ZDNkNGZkZDViOGFiMWI4NmQ1Y2MwNGVlYzdkNzY3ZTc1Y2Y3ODgxMDk2OGI2N2Q3NDUiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjE2MmVlNTdlNDFhOGMyMGFlMDE2NGRmMjYxNDM5ZGNjNDk5ZDM1MjA4MzQxNTE1MTZkMDQ3ZThkZWFlOGE0NjYiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjM5YzEwY2I0YTNhZmI3NWE4N2VmNmE4MmZmYzVhMjBiZDZiN2U0ZmQ0NDRhMmNjMWRlMGZmZDg3MjljODQzZGUifQ"
         }
       ]
@@ -179,7 +179,7 @@
       "statement": "The startup catch-up runs once per process on OfflineSyncCompleted, drains the backlog image and audio maps, and 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, setting the status 'Risposte di recupero: N' when N > 0 and dropping the backlog media of the other chats.",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L920-L981",
+          "resource": "repo://src/app.rs#L966-L1027",
           "version": "repo-lines-v1:sha256:42232cc062a5b6425e02103fe331cbe869e08340278bde2cd954474e46206045:eyJzZWxlY3RlZExpbmVDb3VudCI6NjIsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImM5ZmJjMjYxZDk1MDVkNTBjZTU1MWY4ZDI4OTQ2MTZjMWM2MGY3NTlhZTAwYTE1OWY4Yzg3OGVmMWJjM2Y2ZWEiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjUwOTcxZjZhZWVlMGNhYTY2MWUzOGJkMDMzMDYzODk4NjdkOTc1ZjM3MGVhNWE4YWE3N2NmY2E2MzFiYTU0YzkiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6Ijc4OTU5NGE5NzIzN2Y2NjVkMmU0Y2RhMTIyZmY1NTIyNzJjMjVmZTFhMTI0NmZjMGNkNjllMDQ4MjE2ZTNlYWMifQ"
         }
       ]
@@ -189,7 +189,7 @@
       "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#L854-L860",
+          "resource": "repo://src/app.rs#L900-L906",
           "version": "repo-lines-v1:sha256:9190cc2d950eb45e132eae01c1ae01b5fb4af82470bb42fae8b4e0227a315710:eyJzZWxlY3RlZExpbmVDb3VudCI6NywiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiYmMzZGZhMjZjNzdiNTNkZDcyZWZmZGI1MmEyNWFlNGY5YzMwNTdiZTk2NWI0MzRlMTMyOWIwMTM2MGZmNGViNyIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDg5M2Y0MGUzNzgxZWI4OTZjODJkMDQ2YTA4MDY3YTExZTM1Y2RmMmYxZGZhZjI2ZmVhOTBiOGRiNjY3NjQxMyIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiMTYyZWU1N2U0MWE4YzIwYWUwMTY0ZGYyNjE0MzlkY2M0OTlkMzUyMDgzNDE1MTUxNmQwNDdlOGRlYWU4YTQ2NiIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiOTEyYzE5ODI4NjExZDJhZjdmZWFmNjQyN2ZmNzE0MjFiN2JhMTU4MzM5OWY0YWU5ODBiMzJkNThlNDUzYTJiYSJ9"
         }
       ]
@@ -199,15 +199,15 @@
       "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#L1530-L1532",
+          "resource": "repo://src/app.rs#L1604-L1606",
           "version": "repo-lines-v1:sha256:fa265165a7dbaf620c22011a0c3c22d3351ef4c563f2f54f8fcb163c57bd1734:eyJzZWxlY3RlZExpbmVDb3VudCI6MywiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiOTBhZGVkYjQzODU5N2ZkNWM0ZTVlZTAxMmE5Y2RiYzY1Yjk4Y2VkYTcxMDc2YWU5YjE4MmFjMDNmMmQ5YWRlNyIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDEyY2EzNDVjY2Y3NWJmOWMwODA2YmNlNjk1YmU4ZGU4MDhiNzk5ODQyNTFhN2E1NGQyMDJjZjYxMDFkZDQ1MSIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiOWI5ODAxZjkzZGZiYTU3OWNmZTAzOWRiMGFlYTMwNzc1YWRhYTU2ZDM2NGUxZjE3NWJmNjYxYzhhZjg4MDRkYSIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiMmFiZDA1MjI3NzQyOTJhOTE3MDdjZDg0ZjRmMWExYTExNmQ3MWQzNDA3Yjg1Y2NkNDY4OWU1ZDM1MDBiYjAyNiJ9"
         },
         {
-          "resource": "repo://src/app.rs#L1175-L1211",
+          "resource": "repo://src/app.rs#L1224-L1260",
           "version": "repo-lines-v1:sha256:ebbdd1522a95030cb283c058e7c767edf7b3297cdfe336683fa94151dd1114b4:eyJzZWxlY3RlZExpbmVDb3VudCI6MzcsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjExNTZkYzNmM2MyODVjOTI5NmU1NWNlY2FiOGQzMzczNzg1YTgyOTVkYTZkZjU4Nzk1ZDUyNTZlNDc5MjRiMzMiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjU3NmVkYmFmNjIyMDAzYjdkMGYzOWE0YjE1ZGNkZjJmYTQ0NGNhY2Q5ZWRlMWRmNjc2OGI0ZWZiMzQ3OTVjNGIiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjdkZWE3NDZmZTI4YTEwOGI2ZDUyNmVkOWJlNzE1NjU3MmI2YjIwYTdmODgxYTI0NGU0OTViN2YzMzg4MWFiMDUifQ"
         },
         {
-          "resource": "repo://src/app.rs#L1289-L1299",
+          "resource": "repo://src/app.rs#L1338-L1348",
           "version": "repo-lines-v1:sha256:3f82b1885fd37c0a7f8cddf8a1e57280241400b28724250c318d147c0b73a56a:eyJzZWxlY3RlZExpbmVDb3VudCI6MTEsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImM4NDJlYTc1OTQ1NzNkNTFkY2MyY2YzMTUyNDRkODE2Nzc4NWQwYjA1ODUzZDc3NzAzMWQ4ODU5NmVmNzRlZTIiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImY5OTI3ZWJiYWIzOWI4Zjc4ZGM3NzcyYTY3Y2YxNzhjZmY4Y2I5OTBiYWJlZjVjMTc1ZDQwNDRlZTllOWZmMWQiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImQzMzk2MDA5NzYxYzQ4YWY3MDRkZDRmZDQzNWM1NTNhOTg2ZGQ5ZWIzMmZiNTYyZmY0MGFkODI0NGIwMWU4ZDQifQ"
         }
       ]
@@ -217,7 +217,7 @@
       "statement": "Deleting a chat requires two Delete presses on the same chat in chat mode; confirmation removes the chat from the local history only (messages, image descriptions and chat row, keeping the contact and making no WhatsApp call), drops its pending reply, pending and backlog images and audios and answered snapshot, and closes it if open; any other key cancels the pending deletion.",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L1225-L1272",
+          "resource": "repo://src/app.rs#L1274-L1321",
           "version": "repo-lines-v1:sha256:ad391df25f8e04ff219ae608b732e73d9e13df87da14ad068cae689778491925:eyJzZWxlY3RlZExpbmVDb3VudCI6NDgsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjZhMTdmZjJmNjhhNGFkMzg5M2JmNTgwZjljZmVjNmUyNTI4ZjJjODhjZjJhMjNlNTg2MjcxYjgyMTkyOTBjMzciLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjVmMjAxZDg2ZGM3NzY0YWFmOTA5MGNhNDRkODcxNDM3Y2JiODk4ZGE5ZWJlMGU0ZDhlNjVlNDZiZDJkZGVkYjQiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjgyNTEwNTY0NjA2ZWRmNDUyODU1YTA4OTFlYmRlN2ZhOGZjYjIyMTBlODQyZjU4NDNjNzlhMmRmZDcyZjU5OTAifQ"
         },
         {
@@ -231,28 +231,32 @@
       "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#L1427-L1444",
+          "resource": "repo://src/app.rs#L1501-L1518",
           "version": "repo-lines-v1:sha256:05cde78eab7927a8b5c33ce971f85c0e210ce60936eada88f8c9481585c1519e:eyJzZWxlY3RlZExpbmVDb3VudCI6MTgsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjlhMjY2MjVlMDNiMmE0MDg1NDg0YjQ2NjUzOTllOWRmOWFhZDQzYmVkNGQxNWFkODEwODMzMGIyMDhjNmM5ZTYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6Ijk5OWJkZWZlZWFjYTYzNDBlMDI5MGRjODZlODg5Yzg5ZDVlMTg5YWE1YmE0NGI1ODViM2I3MDczNGY0YTYyZDgiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImU2ZWQ1MmZlYjhjZTliMjNiYmNiYTkwMGFkYjk0MzE3MjA4ODNhZTk0ODY4NmQyNjhlNmRkMjFlNjljYTU3YjkifQ"
         }
       ]
     },
     {
       "id": "claim_5447e09be26b40afa6cf039ca7dee031",
-      "statement": "handle_web_request applies State, ToggleGlobal, ToggleChat, Relink 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'.",
+      "statement": "handle_web_request applies State, ToggleGlobal, ToggleChat, Relink or Quit, then calls maybe_start_avatar_fetch 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#L1380-L1410",
-          "version": "repo-lines-v1:sha256:64fdd9a3310b49def09cbd68a0103e07c41050b652d90eece3d0e2323f063a71:eyJzZWxlY3RlZExpbmVDb3VudCI6MzEsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjA3YTEzOTMxODdkZWNmZmM0NzMzYzYwY2FiYjA2MTkwYjdkYjAxMjJjYWEyYWM3YjRjNGUwNzBiNzIxNTk1NWQiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjM2MGUyOWExOGZkNDE5NDVjMjM3NTgxMzM4ZGI3NWFjOTIzYzYxYjVmYmFiOTNlOTM3ODdlNWJmMGMyMjk4OTciLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjVhOGY4ZTE1N2Y4ODViMmUzM2NmZjkyMzAzNTIyY2VlYzYzZTlhZDcwZmI5NDgyMjg2MGI4NmJiZmJlNjc2ZmEifQ"
+          "resource": "repo://src/app.rs#L1434-L1447",
+          "version": "repo-lines-v1:sha256:95ccc404cb44631061c7ce41c398bebcd048da051c5235b86d9599d5d4b19096:eyJzZWxlY3RlZExpbmVDb3VudCI6MTQsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjhmODZlNzYwNTJhYmUwM2IyNzhmNDYwNDMzZjkyYTYxZTlhNmZjNmEwNjcwNDI4ODRkNzIyYzZlYzZjMDVhMDIiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjkwM2FjNDE5MWFjMTU4NzhjMTg5N2Y3OTdjMDU3MGM5MzZkOWUzODQ4Mjg4MTkzZDFjNmQ4YTdjMzAwZDUwNDMiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImNjODZlNGU1ZWEzODcxYTk3ZDc5ZGFkOTAzNTM1ZDc5ZTcwOGIzMjBiZTViZjA3NDM4MTEzMTk0MWEyNzRiZDcifQ"
+        },
+        {
+          "resource": "repo://src/app.rs#L1478-L1484",
+          "version": "repo-lines-v1:sha256:eca06e6d7e2e4c49b822bfd026a82eeceb05ee7352e6baff4e94e122cba8ce49:eyJzZWxlY3RlZExpbmVDb3VudCI6NywiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiYzc2MDM1ZTk0MmI5NTYwYjBkZTM2NmU0NjViOGVjNDdiODlmYjE4YzY1ZWRiMmRiNjFiYTIwYTlmMmMxNTM0NSIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDg5M2Y0MGUzNzgxZWI4OTZjODJkMDQ2YTA4MDY3YTExZTM1Y2RmMmYxZGZhZjI2ZmVhOTBiOGRiNjY3NjQxMyIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiNGM5MmE1M2NkZmI5ZThhZGRlOTMxNjZmZjgwM2UwZjI4MTMyYTJiNGRhM2JhOTQ5YmMxMTFiMDVhNDJjNDc5YyIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiNWE4ZjhlMTU3Zjg4NWIyZTMzY2ZmOTIzMDM1MjJjZWVjNjNlOWFkNzBmYjk0ODIyODYwYjg2YmJmYmU2NzZmYSJ9"
         }
       ]
     },
     {
       "id": "claim_42f6281320234303b6f272644df4e74f",
-      "statement": "web_chats exposes the TUI chat list in the same order, restricted to chats that support autoreply.",
+      "statement": "web_chats exposes the TUI chat list in the same order, restricted to chats that support autoreply, attaching each chat's cached avatar URL (None when not fetched or without picture).",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L1916-L1930",
-          "version": "repo-lines-v1:sha256:fef040ce667500067e70dc91e838a73c35752821bb041d4463b4361b4445b2c0:eyJzZWxlY3RlZExpbmVDb3VudCI6MTUsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImYxMTFhMDk4Y2E0M2I2Zjg1OTYwYTUxMjFiOGVmY2Y5MDVlZDZmNjdmZjE5MjY0NjcxZDhkYTFiYzhjZTEwNWQiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjM1ZjZmNDY2ZTU5Y2ZjMzA3MDNjNDVmNzJhZjY1ZGU1NGFjMzYzZDM1NGNjMWEzMzMwMzUwYWNkNDUyNDM0YzQiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6Ijg5NGUwY2FkODFjNTBiOWY4MWE5ODI2ZjA1YzUyZjY0MGE0ZGM3NzdiYWQyNGM2MGEwMjQ5NzhlZWJmMWRjMGMifQ"
+          "resource": "repo://src/app.rs#L1990-L2006",
+          "version": "repo-lines-v1:sha256:c2d9c9a91b384e368f619e50dfb7b5c40a6791f1e5936afb8ff8eeddeb228dad:eyJzZWxlY3RlZExpbmVDb3VudCI6MTcsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImYxMTFhMDk4Y2E0M2I2Zjg1OTYwYTUxMjFiOGVmY2Y5MDVlZDZmNjdmZjE5MjY0NjcxZDhkYTFiYzhjZTEwNWQiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjE5ZTc2MzU1Y2YxMTk5NDgwYzNmOTk4MzZiMTQ4NmM4NDFkODkxZmFhZDkwNGM2MDhlMDkzMDIyZDM3NTdhZWUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjZmZWE5NWYzZGNkZjE2MDA3NzQxOTRlMGQxYzQ4MmM0YTFmZTYwNjcyYjNkZDAwNTk5ZGRjNjg4YTg2YTFlYWUifQ"
         }
       ]
     },
@@ -261,11 +265,11 @@
       "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#L1705-L1708",
+          "resource": "repo://src/app.rs#L1779-L1782",
           "version": "repo-lines-v1:sha256:22bd4378ebfd0c259aac4089a03d61912cbb17abca836659a5d9c8fc9cde7b15:eyJzZWxlY3RlZExpbmVDb3VudCI6NCwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiYjA0MjdkNzcxY2Q5ZTlmY2YxZTMyODc5YWNlMDkzZWY5ODk3NjRiZjE4ZjE3OWY2YzFkZGFjM2M3Nzc1MDMyMiIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDEyY2EzNDVjY2Y3NWJmOWMwODA2YmNlNjk1YmU4ZGU4MDhiNzk5ODQyNTFhN2E1NGQyMDJjZjYxMDFkZDQ1MSIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiYTQzMjhiMGQzNWY1YmVlOTEzODE3NWRlZTE2ZDRmNmZhM2FmMzc1NDM0ZmIwOTMwYWU4Nzk5MDg2OTk2MTgxYyIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiOTViMzliZDc5NmMyYjVjYThiYjhjMWEzNjUzODk5NzY5MGE0OTE5ZDY5NzVlNGMxMGZiMWE1ZGE3MGU1Mjc3OSJ9"
         },
         {
-          "resource": "repo://src/app.rs#L747-L770",
+          "resource": "repo://src/app.rs#L793-L816",
           "version": "repo-lines-v1:sha256:5736af4a9574d77087b2de7f56f7b77e2d96d8861d19b9c42159d91fa560f0f1:eyJzZWxlY3RlZExpbmVDb3VudCI6MjQsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImJmNTc4ODJlNGRmYjJmODViYWQyOGRhYWU0Njc1ZjIwODYxYzc2N2E1ZjIxNzg3OTIyYzYyZDgxOGQ5YjJiYjUiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjdjZWI1ZWFiOTZmZDM0MzEzNTI3ZjczMDk2ZjYwMWYzZDZiNWUxYmY1MzE3YTRmMzg0YWM0MTY4OWE0YWI1NTIiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjBhN2YyNDA4YTExOTJiY2VmZTI0ODJhNTg2MGM1MTVhMmQ4NGIxMzQzZTE1ZDdmMGVmNGRhMTA0YzY1ZTJiM2EifQ"
         }
       ]
@@ -275,7 +279,7 @@
       "statement": "finish_reply clears the pending state and the answered snapshot; if a newer message that passes should_schedule arrived after the answered one it schedules a new reply and keeps the images and audios received meanwhile, otherwise it drops the kept images and audios of the chat.",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L831-L852",
+          "resource": "repo://src/app.rs#L877-L898",
           "version": "repo-lines-v1:sha256:50173ca19bcacd64331b57d37896a266693fbcf0d59be90d1ec263c47339b0ad:eyJzZWxlY3RlZExpbmVDb3VudCI6MjIsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ1NDc2YjZjNjU4MzI4NGViNzUyMTA1Mzg3NGFjZmU3NDY1MGQ0NTJhOTRhMjYxYzg4YTFhYzBjYTYzNWYwY2MiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6Ijk5OWJkZWZlZWFjYTYzNDBlMDI5MGRjODZlODg5Yzg5ZDVlMTg5YWE1YmE0NGI1ODViM2I3MDczNGY0YTYyZDgiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImNmMGY4ZWU3NGMyYmNhMjZlNTlmY2NmYjUzZmQyYWI2OGJjNzIwZTY2MGZiZjM3M2IwZjFhNTY4ZmIyNjBjOTUifQ"
         }
       ]
@@ -285,7 +289,7 @@
       "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#L1600-L1611",
+          "resource": "repo://src/app.rs#L1674-L1685",
           "version": "repo-lines-v1:sha256:5c8be8846fd13dfd0c92193083af62f733a451585be051143592b7b033237591:eyJzZWxlY3RlZExpbmVDb3VudCI6MTIsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjY2OWRlZjlmNmNmMDJlMjJjNGFhMmU4NTllMWY1MWZjYjdhOWUyYzc1NzRmZmFhY2IxNDNmOWIzZmUxMWY1ZmMiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6Ijc0ZmM5YzZmMDQxZWE4YTkyNjlhMGIzYmYzMTJjNjYwYjk4OTEyNWI5MDhmN2I1NjE2Y2IwZmZmMjM0NTI2YjUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjE5NTA2Nzc1NGYwNDQ1NDFmZmVlZDA3N2Y3OGMyZjg1ZTY3NjNiM2YzOTZkYjBlYTM5OTJmZDgxMmZjYWRmNDAifQ"
         }
       ]
@@ -295,7 +299,7 @@
       "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#L1050-L1056",
+          "resource": "repo://src/app.rs#L1099-L1105",
           "version": "repo-lines-v1:sha256:f27e2fa9423ba6f3c9fb6381dd2ec60346c3c15ed204b861c320fac53ba2a473:eyJzZWxlY3RlZExpbmVDb3VudCI6NywiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiMWY1MGM3ZGZhNzg3MTk0NDViMjRiNDI2NjAyNjQxZDk0MmI1NjFmN2U1MmQ3Y2M2MGNkN2M5YzNiYjI2NWQzNCIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDg5M2Y0MGUzNzgxZWI4OTZjODJkMDQ2YTA4MDY3YTExZTM1Y2RmMmYxZGZhZjI2ZmVhOTBiOGRiNjY3NjQxMyIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiNzk0ZmJmY2NiNjQ2MWJhYjM5ZWE3YTVmZTJiZDJjMzU3YTQyY2I1NTUxYjE3NzJjNTc2YThjOTVjODkyMTY2ZiIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiZGVkOTlkNjVkMzhkZjFiODYxZTExYTc1MzdlZDk4YWI1NjA5NzEyYTVhZjgzNjdlNWY1Y2FkNjM4MGYyNWZiMSJ9"
         }
       ]
@@ -309,7 +313,7 @@
           "version": "repo-lines-v1:sha256:7825efb1211acd5d025f243b3bb470aeb24c370f07856a47d71779b8de576ab3:eyJzZWxlY3RlZExpbmVDb3VudCI6MiwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiMTVlZWU2ODI2YzIzMDYxMTIxNjY4N2UzNjYyM2E1YjA0NThhMGRjNGFjMGIxOTk3ZDZjYmVlNDNhZGJkMDgyYiIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNGEwYTBlYmUxMGIzNDk4NjVhMzlmY2Y3ZjJlMzY5MDM2MTA2NjdiNWQwYmU5MGU2NWZhYzUwNGJiNmZkNTM5MyIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiZmZiMmY1ZDFiZmZhMTlmNWYxODFhNjBkYTg2NWU5YmY0ODk4MDBkYjUzNDgwMjZjNjE4MGJmNjBkZDk4MjhmZiIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiZjM1MGFjYmZhYzdlNDEzZmFkNWYxMzk0NTQwYWE3MWM3YWY3ZWZmMDA4MDliYmMxYTlhNzQxZGRhNDFiOGYwZCJ9"
         },
         {
-          "resource": "repo://src/app.rs#L1175-L1202",
+          "resource": "repo://src/app.rs#L1224-L1251",
           "version": "repo-lines-v1:sha256:acf06960921a82cc9b2dfbc4cd8be73f5ce9d8945326f358e29f727a80dd79f0:eyJzZWxlY3RlZExpbmVDb3VudCI6MjgsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjExNTZkYzNmM2MyODVjOTI5NmU1NWNlY2FiOGQzMzczNzg1YTgyOTVkYTZkZjU4Nzk1ZDUyNTZlNDc5MjRiMzMiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjU3NmVkYmFmNjIyMDAzYjdkMGYzOWE0YjE1ZGNkZjJmYTQ0NGNhY2Q5ZWRlMWRmNjc2OGI0ZWZiMzQ3OTVjNGIiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjFlYjhjMzVjMzllMzMxMTJhMGUzZTVlMDg1NjlmOWVkYWJkNDM5ZmNkM2FkZmVkNjQ5MzAwNjcyZWRiYjYwNjEifQ"
         }
       ]
@@ -319,22 +323,22 @@
       "statement": "F3 toggles the autoreply of the selected row of the filtered list when the list has focus and of the open chat when the input has focus.",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L1342-L1357",
+          "resource": "repo://src/app.rs#L1391-L1406",
           "version": "repo-lines-v1:sha256:f3a02114e65f3b57e1e9609cdb5227848e7d4a8d0d2d3abc6b38c40f5286461f:eyJzZWxlY3RlZExpbmVDb3VudCI6MTYsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjBjMWZhZjI0NjQzNTZlZjY0NzAyNjI4YzM0MGYyOGNhZDljMjY3OGYxMTViNTAxNDZkMGMyNTBmNzhmZDk2NWMiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjkzNzNhMTgxNzI2YzQ3ZjdjNzlmOTI3ZDI2NzhhYzQ3NjYzY2VhMzI1OGYxOGEwNTM0ZTg2NDVlYmFlOTlkMGUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6Ijc1YjdhNGU5MWJmNTc3ZDQwNjIyM2IzMjg2ZTFhYmI0NGRkNzUwYmViNmU1MzIwYTMyMGZlZDJkOTBjOWQ3ZDQifQ"
         },
         {
-          "resource": "repo://src/app.rs#L1462-L1473",
+          "resource": "repo://src/app.rs#L1536-L1547",
           "version": "repo-lines-v1:sha256:b46e0c6074aa70a6a18b87a08237023dd69de8b989ccfb3ce3c5796148164d3b:eyJzZWxlY3RlZExpbmVDb3VudCI6MTIsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjRjZGFkMjlkZWMwNzg5N2ZlNTVmMTZlNGRiNTEzOWEwMzEyZmU3M2M5Mzc5OTk0NTU5ZDg2ZmQyNjVlZWU2NjciLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjE2MmVlNTdlNDFhOGMyMGFlMDE2NGRmMjYxNDM5ZGNjNDk5ZDM1MjA4MzQxNTE1MTZkMDQ3ZThkZWFlOGE0NjYiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImNjYzVlMzg4ZThmOTY0ZGY1OTFjZDM1Y2FlZjk5ODM0MmRjNTllNGU5OTMwMmMyYmI0MjMyZmMxMzdhM2Q1ZjkifQ"
         }
       ]
     },
     {
       "id": "claim_36c6c5a030ac405b87bbe3051c9b7d52",
-      "statement": "A relink needs two F8 presses: the first sets pending_relink and asks for confirmation, the second starts it; start_relink is ignored while a relink is running, otherwise it switches to the Connecting screen, clears the QR, resets own_jids_known and spawns a task running WaHandle::relink that sends AppEvent::Relinked.",
+      "statement": "A relink needs two F8 presses: the first sets pending_relink and asks for confirmation, the second starts it; start_relink is ignored while a relink is running, otherwise it clears wa_connected, switches to the Connecting screen, clears the QR, resets own_jids_known and spawns a task running WaHandle::relink that sends AppEvent::Relinked.",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L360-L415",
-          "version": "repo-lines-v1:sha256:1874e8b5ab58c3a5e271332f88ac81403b0acf03851aaef445bbd477d21f76c4:eyJzZWxlY3RlZExpbmVDb3VudCI6NTYsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjIxZGUyN2ZmNmY2ZmRjOWQxMWZhMDVjOGM5ZTVjNmI1NTc2NjU3NDhhMWRmM2I3YzFkOTVhZTQwNDY3MmJkNDEiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImM3ZWE1ZDExNGEyZjkyZThlZjQwMzQxN2JhMDkxZWRjODhiNTYwODM5OWY2YTFiNmM0ZWVmNTAzZmI1ZTdjZjIiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjBlZjIyYTA2NDVkNDAwY2EyNWVmYzI2Mjg2ZTdjMDZmOWUzZmZhZmQxY2UwMzVjMmZjOTkyYWZhNGI2NGIxY2EifQ"
+          "resource": "repo://src/app.rs#L399-L443",
+          "version": "repo-lines-v1:sha256:ea63ea5ea5d7d0a0fcd464bd7da7e690e5205cf67e4ee7bf9b63d96c04f81f9d:eyJzZWxlY3RlZExpbmVDb3VudCI6NDUsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjIxZGUyN2ZmNmY2ZmRjOWQxMWZhMDVjOGM5ZTVjNmI1NTc2NjU3NDhhMWRmM2I3YzFkOTVhZTQwNDY3MmJkNDEiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImM3ZWE1ZDExNGEyZjkyZThlZjQwMzQxN2JhMDkxZWRjODhiNTYwODM5OWY2YTFiNmM0ZWVmNTAzZmI1ZTdjZjIiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImU2YWVlOWU5ZGVmNjBmOTE4NTk0NGUwZDJmMTM4NzIwNDE1MmM3ZGIwM2I3MWYyYjJhMjNlMDRiMGI1NzY5ZjgifQ"
         }
       ]
     },
@@ -343,7 +347,7 @@
       "statement": "finish_relink clears the relinking flag and shows either 'Sessione cancellata: scansiona il nuovo QR' or 'Nuovo abbinamento fallito: <error>'; the screen then follows the new session's events.",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L377-L388",
+          "resource": "repo://src/app.rs#L417-L428",
           "version": "repo-lines-v1:sha256:741e9b7ade558dddd72ab34aa017552f645c8cc476274a1266dc2b4f27a5860d:eyJzZWxlY3RlZExpbmVDb3VudCI6MTIsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjYzZjY3YjMxMGI5MjEzMzcwMjdhMWY2YzdiYjA0YjkzNDM5MWZjOWM1ZDAzYTJlNjY1NWI2ZmVjNDE4ZjljMjkiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImEyY2MwZDQ0ZGRlMzkxMDE4YTQ1OWMwNmIwOTBlZWVjOGRmNTA0MTg4Mjk3MzIwMmQwYTc2NWZjOTM1NjRjYTYiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImU1ZTg0YjQyYzc5NDMxZTNkOWI4Mjk4MGQwN2Y5NWQxMTBiYjQ1ZGUxZTI0NjlkYjcxMDJlNzJmODRiM2FjOWIifQ"
         }
       ]
@@ -353,22 +357,22 @@
       "statement": "apply_reloaded_config keeps the current configuration and shows 'Configurazione non ricaricata: <error>' on a load error; on success it replaces config, rebuilds the AI client with build_ai (running replies keep their previous client) and shows 'Configurazione ricaricata' or the build_ai status.",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L417-L434",
+          "resource": "repo://src/app.rs#L457-L474",
           "version": "repo-lines-v1:sha256:11076b2b498b6d19b7a0629886f2cdb37229105049de848490f4e1d9faead710:eyJzZWxlY3RlZExpbmVDb3VudCI6MTgsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjVhYTEzNjY2OGNiMzc5NzgyYTUwMTY4ZDNlOWM4Njg4MmM5YTBhZTAxOGQwNTZiNTEzZWU2ODA5OWYxNjc3YzYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjVmMjAxZDg2ZGM3NzY0YWFmOTA5MGNhNDRkODcxNDM3Y2JiODk4ZGE5ZWJlMGU0ZDhlNjVlNDZiZDJkZGVkYjQiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjAyNjE2ZDk0ZDJmZDg4NWViMDZhMGFmZjVkYjQ1YWUyOTdiMWQ5NzViYzU0MjRlZGIyYWUwZmE2NzZkMzE0ZTYifQ"
         }
       ]
     },
     {
       "id": "claim_1e2cad36bb074b0d9d9976128dd97003",
-      "statement": "web_state returns the global flag, the toggleable chats, the status line, the current screen named connecting/pairing/main and, while a QR payload is stored, the QR rendered as SVG.",
+      "statement": "web_state returns the global flag, the toggleable chats with their cached avatar URLs, the status line, the current screen named connecting/pairing/main and, while a QR payload is stored, the QR rendered as SVG.",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L1412-L1425",
-          "version": "repo-lines-v1:sha256:643e53e7eee86239879c37d679254899bc317cdbf3cd55b8314e04ba1657043c:eyJzZWxlY3RlZExpbmVDb3VudCI6MTQsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImIzN2M0MzFhZmQ5NGMyNzU0NzJkNjliODNlMGM0MmRmMjU0NmQ2MDQ3NmM4ODg3Yzk3MDRmZWM2Y2U0MDkyMDMiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImRkZDFkZTJkMmJiNWE5ODAxZDE4MTAyYTQwYzlmOTc4NjRkNjNkYmEzMzkzMzdiZDVlYTU3Y2FiOGU4YTYwYzgiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImJlODM0YjUzNzA2ODgzYmQzMmMxMmViNmQ0OGEyMmFjZjlkNTIyYzIzOWJlNTE5NDZmNzA5YjIxZDNkNjIwMzIifQ"
+          "resource": "repo://src/app.rs#L1486-L1499",
+          "version": "repo-lines-v1:sha256:3aa43674de3e4e7b89e84724d3034c2a649dcbb71fccfc1ecd34eecf63c78af9:eyJzZWxlY3RlZExpbmVDb3VudCI6MTQsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImIzN2M0MzFhZmQ5NGMyNzU0NzJkNjliODNlMGM0MmRmMjU0NmQ2MDQ3NmM4ODg3Yzk3MDRmZWM2Y2U0MDkyMDMiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImRkZDFkZTJkMmJiNWE5ODAxZDE4MTAyYTQwYzlmOTc4NjRkNjNkYmEzMzkzMzdiZDVlYTU3Y2FiOGU4YTYwYzgiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImJlODM0YjUzNzA2ODgzYmQzMmMxMmViNmQ0OGEyMmFjZjlkNTIyYzIzOWJlNTE5NDZmNzA5YjIxZDNkNjIwMzIifQ"
         },
         {
-          "resource": "repo://src/app.rs#L1908-L1914",
-          "version": "repo-lines-v1:sha256:aeccbfa28dab16bc5a14f9e771c54b3c780209ad3837bba0672ad94aaf49bd5a:eyJzZWxlY3RlZExpbmVDb3VudCI6NywiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiZjYzMWVjNWFhMWFjMTI1MDk0ODNlODM1MTA5ZDBkNjMwMDY0YTBhYmU5MjU5NGUzOWUxZjUwOGIxZDUxNzVjNCIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDEyY2EzNDVjY2Y3NWJmOWMwODA2YmNlNjk1YmU4ZGU4MDhiNzk5ODQyNTFhN2E1NGQyMDJjZjYxMDFkZDQ1MSIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiYWNjYzYzZTY1NWZjNmMxODhkMDhhMzMyMTUyYzY1MTZiNzVlZDdkNTIzYmJmMGFhMTg2MzBkMTRkMjA0NDczOSIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiNDQwM2Q5ZmY0OGI3ZmI2ZDcyM2RlNWM5YjMxODkzMTNlMjQ2NzBhMWE5NjJhYWUxZTYxODRhMjllZmM3MWM1ZCJ9"
+          "resource": "repo://src/app.rs#L1978-L1988",
+          "version": "repo-lines-v1:sha256:f54a96c0669fd127778e7c598ce1151cbf0a2fb8e12f8fec066bfbfab1224636:eyJzZWxlY3RlZExpbmVDb3VudCI6MTEsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjExNzIxMmJmNWJiNzM1M2Q3ZjM0NTZmNzk4ZjU0N2EwZDMyMmU2ODk1M2JmNzczMmI3NTNkNDJhN2EwYjZkNmYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjM1ZjZmNDY2ZTU5Y2ZjMzA3MDNjNDVmNzJhZjY1ZGU1NGFjMzYzZDM1NGNjMWEzMzMwMzUwYWNkNDUyNDM0YzQiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjY1ZmYzZmUxYjJmY2RmMjgyMjdhZGZjY2MyZDk3ZGY5YzhmMzcwY2Q5MzRiOWIwZGUxNjVlN2ViYmEzZjA5YWEifQ"
         }
       ]
     },
@@ -377,11 +381,11 @@
       "statement": "App keeps the audio half of the reply bookkeeping in pending_audios and backlog_audios (chat JID to audios with their message ids, in arrival order) plus a transcribing set of message ids; keep_audio stores an audio for a pending chat, in the backlog maps for an offline message before the catch-up, or transcribes it at once without replying after the catch-up, and does nothing at all when transcription is not configured.",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L245-L252",
+          "resource": "repo://src/app.rs#L264-L271",
           "version": "repo-lines-v1:sha256:624a4ce8841071737337c985666fbac63835cc6490d2f2eb42a0f686ebfc5729:eyJzZWxlY3RlZExpbmVDb3VudCI6OCwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiOGUyMDU5YTZhNWEzYzU1YTNkYzg3NzhlMzUyMWI4NjM3NDExOWJiOGRlZDViNmY5NTg0OTNjY2VmOWJiY2U4NiIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiMzI0NjljOGEwODk5OGMwYmE3ZDUxN2FiYjcyYTQyMTI1YjRlZjc0YzFkNWMwNmNmZDU0ZDdmNWRjMWEwMDQ1NiIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiZjM3Y2IzMGQ4MjVjMTlhYTU3ZWFmY2E5MTg4ZmRjNTU1MTRmY2VhZjE1NmMzNTY2NTIwMDIxMjc0MzkyNjcxMiIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiYTg4MjZkZWMyMTEyZDgzMjYwMjQzMzU4MzRiMTI3YTBmYWRkYzIxMGYwMzUwMWEyZDI5NzVhMzE5YTdjY2EwYiJ9"
         },
         {
-          "resource": "repo://src/app.rs#L583-L626",
+          "resource": "repo://src/app.rs#L629-L672",
           "version": "repo-lines-v1:sha256:1631b6874f5880f4290a306f619b45026e63622c4f480dc289aeddb04599171b:eyJzZWxlY3RlZExpbmVDb3VudCI6NDQsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImY4ZTgwYjQzNzA2MWU4ZDc4M2IzZDBhNmYxZThiMmI4YmNhZTYyYTIxNmQxZTA3ZDZlYzQ2ODZlNjMxZjE5ZTgiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjE2MmVlNTdlNDFhOGMyMGFlMDE2NGRmMjYxNDM5ZGNjNDk5ZDM1MjA4MzQxNTE1MTZkMDQ3ZThkZWFlOGE0NjYiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6Ijk0YjU3MDVlNmJlMTRmMGZkMjBhZTgxYTYzZDNlZWQzZjJhMTFmMWJhNGNlMWUyYzEzMjBhODVlOTdlYzJmYTUifQ"
         }
       ]
@@ -391,11 +395,11 @@
       "statement": "Audio transcription is enabled only when transcribe_key is set, and the number of audios transcribed before one reply is capped at MAX_AUDIOS_PER_REPLY (3, most recent kept, older ones dropped for good), while images use MAX_IMAGES_PER_REPLY (5); audios_to_transcribe shares select_media_to_describe with images and skips audios being transcribed or whose message text is no longer the plain voice placeholder.",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L1647-L1676",
+          "resource": "repo://src/app.rs#L1721-L1750",
           "version": "repo-lines-v1:sha256:cbfda29e4579b9e3f15b7065e6b8f90aa77722d3009289a42486f3f85d5f5e74:eyJzZWxlY3RlZExpbmVDb3VudCI6MzAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjA1ZGMzYmMyMzYxYWY2MWZiOGM1N2YyZGZiYTExMWVkN2QyNTZhNzBhMTBmYWRhNDFkMzg2ODgwZDQ3YTc5OGYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImRmYWI3MTBkZGY3ODU5ZTU3NTc2NGE5MTNhMDAyYzc0NTg1ZmZhNzAwMTUxMGNiODgxMWRhODNkMmNmNzRiMzQiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjU0MDAyNGFmZmIxODk1NGEyZjYxODAzYjk5ODBiOGY1NmVkNTlmZjJjN2FiODAyZDU5NDI2NGZiMDFkNmMyOGUifQ"
         },
         {
-          "resource": "repo://src/app.rs#L717-L724",
+          "resource": "repo://src/app.rs#L763-L770",
           "version": "repo-lines-v1:sha256:fa39de81b9c45f8ac3d33bc79d12c507e895c348f454fe7b20bb5e77d0bd40de:eyJzZWxlY3RlZExpbmVDb3VudCI6OCwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiOGVlMDgxMTQxMWY5NDM0NzM0MzhlNDI3YTdiN2ZkZWVmYjMzN2ViNDAzYzE5NDU2ZTMyOTZlNzgzMTFlNmNiZCIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDg5M2Y0MGUzNzgxZWI4OTZjODJkMDQ2YTA4MDY3YTExZTM1Y2RmMmYxZGZhZjI2ZmVhOTBiOGRiNjY3NjQxMyIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiZjJkODI4YTAzN2FjMWM4MTUzNWIxZDU1MDA4MzIzMjNiZmJiODY2YTNhM2Y4N2U3YTY4YzEzYTJiYWU1NmZmZiIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiNTc4NDUxMGM1NmY1YTlhM2IyZjRlN2Q2ZmMwMWM0Yzg1MTI5YmNjNTMzZTVhNmNlMDYxNjJjYWIyYjA0ZjhjMiJ9"
         },
         {
@@ -403,7 +407,7 @@
           "version": "repo-lines-v1:sha256:c9d722762ee24413b4f692e17e9d10b9430f82db1b1c0cfecc1c9b9e33e0876a:eyJzZWxlY3RlZExpbmVDb3VudCI6NCwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiYjRkMDBmOGEwMmYwM2JkMGI0MDQ1MmI5NDM4ZDgyZTgyNDZkOTZiMTc5ZjllOGM4ZDY1ZDJiODg5YmQzM2U4YiIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiYWUwMTNhNDJhODRhNGUxM2UyNzBhOTM2ODdkMzEzMmE5N2UxZThmY2FkYWU3MDkxYjQxZTFiYTZjZDhjNjBhZiIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiN2UxZjg4N2FlOTZlZTg0ODQ3ZjZiNTExZWY4MDU5ODY2ZGExZjY3NDJhYzk4N2Y1YzE4NWRhYTcxODFhNmE4YiIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiNGI3M2NiMGJjNjJiMWRjODdhZjkyNjE5YWQwMGFkOTg5NGIzYTkwZmQ0MjY5YzkzZmQzODM3MzE5ZDgzNWEyZiJ9"
         },
         {
-          "resource": "repo://src/app.rs#L900-L918",
+          "resource": "repo://src/app.rs#L946-L964",
           "version": "repo-lines-v1:sha256:aca8ef54d074bdd9a35c6228ea67532c04a9bf55079ae6ed368a56dfc7f87db1:eyJzZWxlY3RlZExpbmVDb3VudCI6MTksImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjI3MGQwY2JkODBmOGZhNDZkN2EzOGQxNDMyMzZjODMzYmViZGE4Mzg2ZGNmZTFkZTBjYmNjNWM2M2IzNGE1MzQiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjRiNDI5OGJkNmI1YjQyZmVjNDI0NDIxMjBlNTkzZjAxZDFiYTU0YmFhODg2NTUwMzE0MWE0YmQxYmViNjQxZjgiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjIyMGUyNjkzMzFhZTU2NTJiYTMxNWViYTdmYzRlNWMxNWFhNDlhMzdiMjE2NGY3NjRlYmNiYTA1ODE5ZGMyNDAifQ"
         }
       ]
@@ -413,11 +417,11 @@
       "statement": "spawn_transcriptions builds a TranscriptionJob from transcribe_url, transcribe_key, transcribe_model and transcribe_language with an empty MIME type, adds the message ids to transcribing and calls spawn_audio_transcriptions, which runs one task per audio in parallel (download with up to 3 attempts, a 60 s timeout each and growing retry waits, then the transcription with the downloaded MIME type) and one collector task that emits a single AudiosTranscribed carrying then_reply and the postponed images.",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L1758-L1848",
+          "resource": "repo://src/app.rs#L1832-L1922",
           "version": "repo-lines-v1:sha256:450b2c863867076ae29a98f6b2df94b3dc43936844327c15ec52c2a5f4b2ec09:eyJzZWxlY3RlZExpbmVDb3VudCI6OTEsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjUwMzZlNzExZDhiYjhjNjMwNzQ2NTEwNGIwZDYwZTI0Y2M0N2IwZDY4ZDRlOTMyNWM3MGM2N2JkNDRkZTU3ZDQiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjE5ZTc2MzU1Y2YxMTk5NDgwYzNmOTk4MzZiMTQ4NmM4NDFkODkxZmFhZDkwNGM2MDhlMDkzMDIyZDM3NTdhZWUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjViYzQ0ZWIzMGI2OWE4MGNjYTQzMGFhMjIxNTg0MGU4ZTNkNTlhOWU4OTk2NDk5ZTlhZmM3NmQ3ZjY1OWUxMmEifQ"
         },
         {
-          "resource": "repo://src/app.rs#L681-L715",
+          "resource": "repo://src/app.rs#L727-L761",
           "version": "repo-lines-v1:sha256:13c9ebb313a3bcdb8ca212f0ecedf91359ab8b7346f0a159add0c503cdc53a51:eyJzZWxlY3RlZExpbmVDb3VudCI6MzUsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImM1YjQ5M2Y3OGMzOGVhMGFhNGY2YzlmODE4ZWI3ZTc3ZTIxMDJiNzNjZDhhMWU3ZDc1MzAzOWExZWQ1ZTI4YjAiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjA4ODU4MjU5NDc5YmI1ZmYzY2Y0YjE2MDhjNTA0MzYzNzM5NThhMWQ3NWY2NDZlMDZiNGUyOWQ5MzVkNGMzN2MiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjBjOTYzNmU4NWU4NzBhN2VmZjYyMWM2ODNhN2RkODZhYzA3YTdiNmNhYTNhOTNlYzgwN2FjOWQzYTg4MzFmOGUifQ"
         }
       ]
@@ -427,7 +431,7 @@
       "statement": "handle_audios_transcribed removes the ids from transcribing, appends each transcription to its message text as '[messaggio vocale] <text>' when the message still exists, reports failures with one status 'Trascrizione vocale fallita (N): <first error>' (download failures carry the prefix 'Download audio fallito'), reloads the open chat, and then, while the chat is pending, describes the postponed images or starts the reply when then_reply holds.",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L1710-L1718",
+          "resource": "repo://src/app.rs#L1784-L1792",
           "version": "repo-lines-v1:sha256:b58e50269905a224c205da0c87a432f759e7992b40a8e0e8c3dae0872b66dbcc:eyJzZWxlY3RlZExpbmVDb3VudCI6OSwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiMDlhZGUzMDdjOThiYmRmZTE1NTJjZDJhY2E4ZTY4NTA5YTE5NGZhOTQ5NjkyNTdhMGZlYjFmYjM2NTFlM2E3MiIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDEyY2EzNDVjY2Y3NWJmOWMwODA2YmNlNjk1YmU4ZGU4MDhiNzk5ODQyNTFhN2E1NGQyMDJjZjYxMDFkZDQ1MSIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiMjcxOTg2YjExOGI3ZmNmMzEzNTE1MmVlODA0NDMwMmE5Zjc3YmE5ZTc1NmM3NWIxOGI4N2IzMjZjNjQ4OTI3YiIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiOTViMzliZDc5NmMyYjVjYThiYjhjMWEzNjUzODk5NzY5MGE0OTE5ZDY5NzVlNGMxMGZiMWE1ZGE3MGU1Mjc3OSJ9"
         },
         {
@@ -439,7 +443,7 @@
           "version": "repo-lines-v1:sha256:e8ad9bf94a9667abf25ac1ea023b45a46e983fa19f2693ad61572aa62b8dce37:eyJzZWxlY3RlZExpbmVDb3VudCI6NCwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiN2Q3YWEwNTI1MjIzNTNlZGExNGNjMGM5ZTg0ZTE3NWNlMDBlMjNmMWY1M2M0MDZlZGYwYmE4OTA4NTg2MjMyYiIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiOGRlMDQ3NmFjZmJlNjk5MzE1ZDU4YTBlNGMwZDIwZGZhM2FkMTlmMDg3MDYzM2Q4NmQyNzZkZjI1MGY0MTJhNyIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiZjQxYzA4ZTkxMzgyNzAzMTIzZDJkYWQ5Nzk0NWU1OGNlMDQ1NmU5NGVmMGYzZjFlNTFkMDFhYzlmN2M5OTQ1MyIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiZDQzOGUwNWQwNzYwZjY5ZjFjMTMwYTA4ZWVhYTc4ZWI5Y2MzNDFmYzMyMjI5MmYzZGNjYmI0ZTBiZmE2NWUzYiJ9"
         },
         {
-          "resource": "repo://src/app.rs#L772-L829",
+          "resource": "repo://src/app.rs#L818-L875",
           "version": "repo-lines-v1:sha256:d5ad7a2c920ec3c326e04645e1ce43b9fb4ce906a211a835b056903dbcdf35d5:eyJzZWxlY3RlZExpbmVDb3VudCI6NTgsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjMwZjE0YmRhOTU2YjU3M2JhNzk1NTJhNTEwNmIyMzMzNjNlMWFlNGY3ZDMyZmE3YWU4ODk2MDRlMDMyOGI2MGUiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6Ijk5OWJkZWZlZWFjYTYzNDBlMDI5MGRjODZlODg5Yzg5ZDVlMTg5YWE1YmE0NGI1ODViM2I3MDczNGY0YTYyZDgiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjBhN2YyNDA4YTExOTJiY2VmZTI0ODJhNTg2MGM1MTVhMmQ4NGIxMzQzZTE1ZDdmMGVmNGRhMTA0YzY1ZTJiM2EifQ"
         }
       ]
@@ -449,18 +453,56 @@
       "statement": "For one reply the media work is strictly ordered: transcriptions run before image descriptions and the AI request, because prepare_reply hands the images to spawn_transcriptions instead of describing them while an audio still needs a transcription, and only the following AudiosTranscribed event describes those postponed images.",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L643-L715",
+          "resource": "repo://src/app.rs#L689-L761",
           "version": "repo-lines-v1:sha256:ec755f9e1aadd0ddfa4bcb7f40ceb61ebadb3024ee9727315ad63fcae7620f83:eyJzZWxlY3RlZExpbmVDb3VudCI6NzMsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImMyMWI5OTAxNjZkNDU5NjI1ZDk3MTdjZThkNTMzOTMzNTFhNzI5YTA5ZDY5ODBmN2UxYTJhOWJiNDkyYjVhNjMiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjE0OWI3NmEwN2ZiN2VjODVjZmJhNmNjNmQ1OTUzOTFlNDU5MjAzY2NmZDMwNTdmNmY3MzczYzNlNzZjYWE1N2UiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjBjOTYzNmU4NWU4NzBhN2VmZjYyMWM2ODNhN2RkODZhYzA3YTdiNmNhYTNhOTNlYzgwN2FjOWQzYTg4MzFmOGUifQ"
         },
         {
-          "resource": "repo://src/app.rs#L772-L829",
+          "resource": "repo://src/app.rs#L818-L875",
           "version": "repo-lines-v1:sha256:d5ad7a2c920ec3c326e04645e1ce43b9fb4ce906a211a835b056903dbcdf35d5:eyJzZWxlY3RlZExpbmVDb3VudCI6NTgsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjMwZjE0YmRhOTU2YjU3M2JhNzk1NTJhNTEwNmIyMzMzNjNlMWFlNGY3ZDMyZmE3YWU4ODk2MDRlMDMyOGI2MGUiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6Ijk5OWJkZWZlZWFjYTYzNDBlMDI5MGRjODZlODg5Yzg5ZDVlMTg5YWE1YmE0NGI1ODViM2I3MDczNGY0YTYyZDgiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjBhN2YyNDA4YTExOTJiY2VmZTI0ODJhNTg2MGM1MTVhMmQ4NGIxMzQzZTE1ZDdmMGVmNGRhMTA0YzY1ZTJiM2EifQ"
         }
       ]
+    },
+    {
+      "id": "claim_b8d5262a046e4303970190161263dea9",
+      "statement": "maybe_start_avatar_fetch starts a fetch only on the Main screen while WhatsApp is connected and no fetch is running, selecting the web chats whose cached avatar is missing or older than AVATAR_TTL_SECONDS (86400) and marking the fetch as running before spawning fetch_avatars.",
+      "evidence": [
+        {
+          "resource": "repo://src/app.rs#L116-L119",
+          "version": "repo-lines-v1:sha256:8ac2cc801ab683eec324c271fae8e3fda2bd483ba0d4fb80465f310dc39af43d:eyJzZWxlY3RlZExpbmVDb3VudCI6NCwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiYjM5OGZlNjM4OWZlMTgwNzM2YWNjM2YyMWU0NDFlNDkxMjZhMGIxODkzMGI0ZDQ1MzBhMDNmNWU2Y2ZkZDdjNyIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiYmZkNmI5ZjFmMmVjNDZiMjU5MzUxZTg3OGIzYjY4N2VlMDhlOWNjMTgyODg3Zjg1MTI1ODI1NDhmYWY0MmFiMyIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiMWY3YTczOGU5MGIyNDIxNmVhZWYzOTNiMDUwNTI1YzBjZDdlOWI1OGE1NzIzMjUzMWM2OTBkMmIzODAzOWExNiIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiZTZkODk3NjdiOWFkN2ZmYTVmYWVlZGRmNjBhN2I0MTg2ZTM1OTVjNWQ5Zjc0M2I0OWY1OTgzNmIxNjBiMGRiMSJ9"
+        },
+        {
+          "resource": "repo://src/app.rs#L1449-L1471",
+          "version": "repo-lines-v1:sha256:b500d32399004dfd66cba09d876f635c883129b0ce1856400c9ec3704b5b1f9a:eyJzZWxlY3RlZExpbmVDb3VudCI6MjMsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImY4MDJlMDA2NmM3ZGUyOGE2ZTg4NWQ1OThiOTYwNDJlNzc2NDIyNjA0YWJmZTUzYzkwZGY4ZDlhMDAwOTE2OWUiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjE2MmVlNTdlNDFhOGMyMGFlMDE2NGRmMjYxNDM5ZGNjNDk5ZDM1MjA4MzQxNTE1MTZkMDQ3ZThkZWFlOGE0NjYiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjQ0NmM2OGRiNzFjZDkwMmMyZDMxY2QyYjQ5Y2MyM2ZjYzFmNjVlYWRlY2JlZGRmNDFhOWEwZjVhNGIxMzE1MmQifQ"
+        }
+      ]
+    },
+    {
+      "id": "claim_af34a472366147e790590a6454ba2198",
+      "statement": "fetch_avatars requests profile pictures sequentially with an AVATAR_FETCH_INTERVAL_SECONDS (2 s) pause between requests, emits AvatarFetched per chat, and on the first error caches that chat without picture, stops, and reports the error through AvatarFetchFinished; a completed batch ends with AvatarFetchFinished without error.",
+      "evidence": [
+        {
+          "resource": "repo://src/app.rs#L2008-L2030",
+          "version": "repo-lines-v1:sha256:dd13940dc14cd062e2bd6ce52abd9ce733256c2c6d45fc84adb1e94c5ded7a0c:eyJzZWxlY3RlZExpbmVDb3VudCI6MjMsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjU3NmE0NzFhMmM0NDRiN2ZkMzdkYzY0YmE1YjYzYjBjMTg5MDhjM2U1NTZmNGE5MDAyMGVhNDU5NDg4ODkxYzciLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImQ4ODU5MjEwZWJjYjEyY2Y2OGM2MTYyMjJmYzU2MGMxZjY4ZTJmYTMxZjYzMDQ5M2RmODA5NTg4ODdkNTdhZjkiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6Ijg5NGUwY2FkODFjNTBiOWY4MWE5ODI2ZjA1YzUyZjY0MGE0ZGM3NzdiYWQyNGM2MGEwMjQ5NzhlZWJmMWRjMGMifQ"
+        }
+      ]
+    },
+    {
+      "id": "claim_212396518fde4f57985c96bf0bc0575b",
+      "statement": "AvatarFetched stores a CachedAvatar with the current Instant in the in-memory avatars map, and AvatarFetchFinished clears avatar_fetch_running and, on error, sets the status 'Foto profilo non caricate: <error>'.",
+      "evidence": [
+        {
+          "resource": "repo://src/app.rs#L173-L180",
+          "version": "repo-lines-v1:sha256:c886361ce21f5d3f88eee41ed6770acb3886153edfdad0468881cea11c4422e6:eyJzZWxlY3RlZExpbmVDb3VudCI6OCwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiNWQxMjM2OGNhNmI0NDI2Y2ExNTM0ZGQ4YWNhMDNkM2MxZjVhZWJhOTAyNWU5YmU3NTZiY2M5NDgzYTRiYjcxYSIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDEyY2EzNDVjY2Y3NWJmOWMwODA2YmNlNjk1YmU4ZGU4MDhiNzk5ODQyNTFhN2E1NGQyMDJjZjYxMDFkZDQ1MSIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiMzAxZDMyMWZjZGI4NjBkNTQxZDRlNGZmMzU0Y2QzYzkyMDMzNWZjYmQ5MDc3ZDAxZTkzMjgxOTNkZmM0OWE3ZSIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiMmQ1NDNkM2RmNThmYmY4N2RmNWJiNmU4Y2ZkNDBjMGQ2NjkyNjcxZmYxMDI5ZGZkMzg5ZjQxYjI0NjY5NmJjYSJ9"
+        },
+        {
+          "resource": "repo://src/app.rs#L374-L384",
+          "version": "repo-lines-v1:sha256:d49f75443dcde9c8c8fb3160c7b80131fe0b6ab74bd8000854cefb643920cd89:eyJzZWxlY3RlZExpbmVDb3VudCI6MTEsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImFhNzJiYjk2NjkzMzlmMjVjOTJlYjY0YmMwYTc3MDFjZGFhMTU5MDk2NGY4OTE3Y2Q0YzI1N2M5NThjMTdiZmUiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjU1MjJjNDJjZDE5YzZkMDNkODA4YzQ1MjcyMDU1MjgyNWI3ZWExYzViNDkxYjIxODUxMWNhYzc0MTVjZjlkOWIiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjllYTI0OWFkZjc4YjM5M2UxYmMxYmM5MTRhMWQwMTlmNGI5NTI5Njc4NmQzNGY1OTE2YWIwYTY1ODdjMmUwZTUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImE4YjBlZWFjMWM4ODgwZTg0ZGRlNGIzZTA3ZTEzOWQ2MGNmYjhjMzlmZGQ5NTE1ZmU5YjI3MWQ5MjM3YjZiODcifQ"
+        }
+      ]
     }
   ],
   "verification": {
     "by": "openwiki/0.7.2",
-    "at": "2026-10-24T16:42:08.550Z"
+    "at": "2026-10-25T12:54:53.684Z"
   }
 }

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

@@ -1,13 +1,13 @@
 {
   "schemaVersion": 1,
-  "pageVersion": "sha256:1c12bcb75120e45d7940b31833bbe96fe727146b35af7e2e965eff9da4c51980",
+  "pageVersion": "sha256:73780810fe3cd1b10a3d3d3a8fb56a8c9f3079e97a27a9bffda63c40a108fef4",
   "claims": [
     {
       "id": "claim_a384a56f0ebf4add8c0ab7722b040888",
       "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, spawns the configuration watcher, builds App, spawns the input thread and runs the TUI.",
       "evidence": [
         {
-          "resource": "repo://src/main.rs#L42-L71",
+          "resource": "repo://src/main.rs#L53-L82",
           "version": "repo-lines-v1:sha256:1f8efd5e596ab7a7d1da0bfd9bc62005e6be9c40accb54842253ec733e4abfe3:eyJzZWxlY3RlZExpbmVDb3VudCI6MzAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImExZDA2ZTA0OGNkMjcyNzM4ZDgzN2IwYzBlYzZhYWIwYjAzMTQ1N2U0NzVmZDQ5YWJiY2JhZmM1ZjczYzAyMGQiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImE5NjlkZGFlYmE5MjRhNThlYTY2OGUyMDQzMDFiZWE5ZThjMzZjNjg5NTU3NTc3NmYzZDkzZWNiZmJlNzdjMzUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjAxZmI2ZDM1MThiYTRjMzUwOWRiMWM5MTAyMTk2NjQwMzI4ZjgzM2M3MDJmMzgwNmNmZjFlZGI5MzU4Y2Q5YTgifQ"
         }
       ]
@@ -17,7 +17,7 @@
       "statement": "The WhatsApp forwarder wraps each WaEvent in AppEvent::Wa and stops when a channel side closes.",
       "evidence": [
         {
-          "resource": "repo://src/main.rs#L81-L93",
+          "resource": "repo://src/main.rs#L92-L104",
           "version": "repo-lines-v1:sha256:376729d914bfe5462e810010c4e827fb7fc331182e173bf42c14ec815be9c5eb:eyJzZWxlY3RlZExpbmVDb3VudCI6MTMsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImRhNjQ2NjQ1ZDg1OTU4N2VjNWRjNmMyZWVjMzJhNjFkODI1ZDVmZjM0MWQzMmVlMmRhNGNhMWQwNzYzNmQ3ZjkiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImFhOGNhMzU1ZmQ1NzA2YTk1OTI3N2EwNDFlM2IxNTIzM2I4YzI0M2M0Njc4MGM4M2Y1ZjkyY2M2ZTU0MjhjN2QiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjNiMWI0ZGU5MmNiYmMwMmUxN2UyYWJmYjc3OGQyZDA0MjJmNjUzODM5YWZiNWE2MTdmNDhjNmE4MTBiZGU3YzIifQ"
         }
       ]
@@ -27,11 +27,11 @@
       "statement": "Terminal input is read on a dedicated OS thread named wh-input; a read error is reported as WaEvent::Error and stops the thread.",
       "evidence": [
         {
-          "resource": "repo://src/main.rs#L29-L29",
+          "resource": "repo://src/main.rs#L30-L30",
           "version": "repo-lines-v1:sha256:1d14980ebaa2f31e4144fe6b04613dfc3285892428a59dc526be0808a55c1eff:eyJzZWxlY3RlZExpbmVDb3VudCI6MSwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiMWQxNDk4MGViYWEyZjMxZTQxNDRmZTZiMDQ2MTNkZmMzMjg1ODkyNDI4YTU5ZGM1MjZiZTA4MDhhNTVjMWVmZiIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiMWQxNDk4MGViYWEyZjMxZTQxNDRmZTZiMDQ2MTNkZmMzMjg1ODkyNDI4YTU5ZGM1MjZiZTA4MDhhNTVjMWVmZiIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiNDZmZmE0OWU5ZTQyNmZkYjkwM2I2ZmIyNjYxZDdjZDEwZmM1OTljMjFjZjg2ZDMzMWFlNjY4NDBhMzUwZGNkMyIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiODMyYWE3YjFiZGM3MTY1MmFjMmUwODIxNWJkOTcxYmQ3MGJjMjRjNzQyYWVlYzA5NDA4YjYzMTE3ZDkzMmQyMSJ9"
         },
         {
-          "resource": "repo://src/main.rs#L106-L141",
+          "resource": "repo://src/main.rs#L117-L152",
           "version": "repo-lines-v1:sha256:58d18a88c972035530c97c24224afd69709cd2c5de07f4236969a3cc38d1fa8e:eyJzZWxlY3RlZExpbmVDb3VudCI6MzYsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjliZmVlNTkyZTcyMzMwNTAzZGVmODM4ZWFjZTE0ODNjMTFmNGVkOTk2OTM3YzlhODA3OGM1OGE4MTljYWVmNzUiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6Ijg0NTAxNDAzZmViNmI2ODk4YjM1MDcxOGQ4MmY3ZmU0YzM4YzBhNTljMjZkY2UzMTRjMzUyMDQ3OTcyYTk4NDQiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImViNTc2ZTc2ZWM5NDRlZjNmY2IzMGZmZDlkYTJiOTU5NzNiNDUyYTdkYTQ4NjM5ODczNjM1MDExOGQ2MDU3ZDAifQ"
         }
       ]
@@ -41,7 +41,7 @@
       "statement": "Only key press events become AppEvent::Key and resize events become AppEvent::Redraw; other terminal events are dropped.",
       "evidence": [
         {
-          "resource": "repo://src/main.rs#L143-L154",
+          "resource": "repo://src/main.rs#L154-L165",
           "version": "repo-lines-v1:sha256:d8d9638a5e048622a2f5afb4eb87071516bfc768606487e27c53c03eb11ffa4f:eyJzZWxlY3RlZExpbmVDb3VudCI6MTIsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjJjNjVkZTMxODE3ZDgzNDIyNDg1NTNiNmZkZTAzN2M1ODI3YTRjOWI1NzA4ODAxMzc4NzYxNWM1NWMwNmU4ZDMiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjE5ZTc2MzU1Y2YxMTk5NDgwYzNmOTk4MzZiMTQ4NmM4NDFkODkxZmFhZDkwNGM2MDhlMDkzMDIyZDM3NTdhZWUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjY0MWNiMWQ2MWJiYTdkZGQ5ZmJiOGEyOTFmODA3ZjA1YzY2NzY5MjM2NjNkYzNiM2E3MjlkNzMyODQ1ZDQ4Y2QifQ"
         }
       ]
@@ -51,7 +51,7 @@
       "statement": "run_tui clears the screen and always restores the terminal after the event loop returns, returning the loop error first and otherwise the clear error.",
       "evidence": [
         {
-          "resource": "repo://src/main.rs#L156-L175",
+          "resource": "repo://src/main.rs#L167-L186",
           "version": "repo-lines-v1:sha256:d25ee631a872cf7bd8251dbd7e2ef5309b07a890ee5181e193fc4b0c81632aa3:eyJzZWxlY3RlZExpbmVDb3VudCI6MjAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjU4NjMxYjQ1YTE1ODQ4ZjQ5YTJhYWQyYTMzNDdiZWRkOWZkZGNmNDBhNTM5MTkwMDkzNDk1OTdhNzYwOWYzMmIiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjE5ZTc2MzU1Y2YxMTk5NDgwYzNmOTk4MzZiMTQ4NmM4NDFkODkxZmFhZDkwNGM2MDhlMDkzMDIyZDM3NTdhZWUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImVkMjRmOWNhMzgxM2IwMmQ1ZGNhODJmNGU3NzUzNDg3OWRhYTNhNDI4ZjJjMzM5YjI1ODU4MjU5NDE4NTYyOGYifQ"
         }
       ]
@@ -61,7 +61,7 @@
       "statement": "The event loop draws a frame, awaits the next event and dispatches it to App until should_quit is set or the channel closes.",
       "evidence": [
         {
-          "resource": "repo://src/main.rs#L177-L205",
+          "resource": "repo://src/main.rs#L188-L216",
           "version": "repo-lines-v1:sha256:fe54d25939a4f3603d298eaff2c9a5760f9ae544a1c95627fcbdba35bd563e7f:eyJzZWxlY3RlZExpbmVDb3VudCI6MjksImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjNlMTkzYjAzYzI2ZWIzM2EwMjk4MWViY2MyNmM2YzIxNmRlZGQzYTkxMDQ5OTA0ODI5ZjJmNDQ0ZTVmYWYyODUiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjZmMmQxOTA3Mzg2ZmZhYmYyMDQzMjNkMGMwMDIyMDE0MTdhNjUwN2IwNGRjYzZlODdjZjFhNDdhZGExNDc0MTgiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjdiYzk4ZDZiMGRjNDkyYzUwMTZkZjEwODA1YTNhNTRlYjljMTg0YTVhNjEzN2U3ZjRiZWNmYTBjMjIwNTUwZDAifQ"
         }
       ]
@@ -71,7 +71,7 @@
       "statement": "event_loop records list_mode_before = app.list_mode before handle_event and, when the list mode changed, resizes the terminal to its current size to force a full repaint without the cursor-position query that Terminal::clear performs.",
       "evidence": [
         {
-          "resource": "repo://src/main.rs#L194-L202",
+          "resource": "repo://src/main.rs#L205-L213",
           "version": "repo-lines-v1:sha256:d5d2f63f2332914a0e3f33f7bb238c97b54971bd35773182f767422e64442902:eyJzZWxlY3RlZExpbmVDb3VudCI6OSwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiOTgwZmU3NzZjMzc0ZWExM2RjYmU5ODhjZjFiMmY0NTQ0MTdkZjA4MGJiNWY4M2Q1N2FhMmRkMmI4YmZiYjA2ZCIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiZTZiMDQ0OTE0Njk3MjJlODM2M2E5NGU0MDQ0NTM4ZTAxM2EyYTIxN2UzNmIxZTU2OGJhNjBhY2I3YjRjOGYyZSIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiODQzMTkxYmFiNjEwYjJjNDZkODgxMjkzMTJhYzQ0ZjUwNjVlODdiYWVjOTVmODg2MDg0YzM2Yzk5ZDg3YTg0NSIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiNTVhYjdiNmQ5YjZlMTk1NjUzZmM4YjU1ZjYzZjM2NDYwNjg1NWM0NTRmYzU2NTg1MWYzMWEyZjA5NDVkYTc0MiJ9"
         }
       ]
@@ -81,7 +81,7 @@
       "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#L95-L104",
+          "resource": "repo://src/main.rs#L106-L115",
           "version": "repo-lines-v1:sha256:54c6448bd45241fd4b90479b1a91e2fd528e7a38f4ac5d543eda728fbf3756d5:eyJzZWxlY3RlZExpbmVDb3VudCI6MTAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImYwZjA3MmY1NzA4NTRiMGM0ODg3YmRlZDU4YzU2NWRiZGExOTNlYmY3NGIwODZmMjYyNTFmZjZhOTZiMmJiYWMiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImRmYWI3MTBkZGY3ODU5ZTU3NTc2NGE5MTNhMDAyYzc0NTg1ZmZhNzAwMTUxMGNiODgxMWRhODNkMmNmNzRiMzQiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImI1OTAxZTAwNGRkYTg1ZmUwNjg0OGVkYjhkMWIzM2Q1NTNmYWM4ZjkyMzZlZTFlMjlkZTNmNGUzMWRmNTM4NTkifQ"
         }
       ]
@@ -91,26 +91,26 @@
       "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#L30-L48",
-          "version": "repo-lines-v1:sha256:075df10ec41f8312822982917d8a541ae8fb012a1de865cb86b483225cc1ac34:eyJzZWxlY3RlZExpbmVDb3VudCI6MTksImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImUzYTMwMjZjMmE4MzdhNTVhMTBlMmU2OTI4NmY3MTVjZDljNGMwNTBiOTJjZmE0MTg0M2YzNTk0OTA4MDgyNGYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6ImY2YmM5OTMzNjdjNWY3OGU1ZDZjZjFjY2U0MmMyOTlkMWUxNTQ2MzM1MGU3Mjk1NGIwMDczMGNiMTUzOGZkMjIiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImE4MjYzNjdkZjY0MGI1YzFkZTYyYjFlNDdkZTc4ZGMxMmY2Y2YxNDAyMTI5OTllOTg2YWI1MDVmYmMyYjZkYjIiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImEyZmFlMzljMjVjZmVlYmZkMTU0MTk0YzA5NjEzZGIzNmY2MmU5YzcwMWNhZGMwMWE4ZDcwMzhmZmIyNjE2OWUifQ"
+          "resource": "repo://src/main.rs#L43-L59",
+          "version": "repo-lines-v1:sha256:d0efc50d63b4ef6575a224d08e55e1860bc394063a06e6a9de2158de8c26b428:eyJzZWxlY3RlZExpbmVDb3VudCI6MTcsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6Ijg3YzViZTA4MWM3NGIwMmViZjRmM2RjZDExMzg0YjdlNGVlNzU0ZjdjZjEyZWNkZjgzYWVhODlmMGVjYmU5YjEiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6ImY2YmM5OTMzNjdjNWY3OGU1ZDZjZjFjY2U0MmMyOTlkMWUxNTQ2MzM1MGU3Mjk1NGIwMDczMGNiMTUzOGZkMjIiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjNiYWFkZDAyODNiNDFkM2YwMmE0NzdiYTQ0NDFhZDA4MGRhNjgyYTg0NWZjMTg1MzVhYWEzMGQ3MWM4MjhiODgiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImEyZmFlMzljMjVjZmVlYmZkMTU0MTk0YzA5NjEzZGIzNmY2MmU5YzcwMWNhZGMwMWE4ZDcwMzhmZmIyNjE2OWUifQ"
         },
         {
-          "resource": "repo://src/main.rs#L73-L79",
+          "resource": "repo://src/main.rs#L84-L90",
           "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>)'.",
+      "statement": "build.rs embeds the short git commit hash as WH_GIT_HASH (falling back to 'unknown') and the git commit count as WH_BUILD_NUMBER (falling back to '0'), and BUILD_VERSION combines the package major and minor version with that build number and hash as '<major>.<minor>.<build number> (<hash>)'.",
       "evidence": [
         {
-          "resource": "repo://build.rs#L1-L33",
-          "version": "repo-lines-v1:sha256:862b7d4a061487e5c9a432ace9cadefe3bc590f6eb970126321e7e932fdd4b65:eyJzZWxlY3RlZExpbmVDb3VudCI6MzMsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjAxNjZlNzY2Mzg4OTM3MmMxNDVhMDYwZDY2Njg0ZTliMGJmYTU5ZjBmN2MzMmRiOGUyMGZiYWI5MWY2MmFlZTIiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjowLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImUzYjBjNDQyOThmYzFjMTQ5YWZiZjRjODk5NmZiOTI0MjdhZTQxZTQ2NDliOTM0Y2E0OTU5OTFiNzg1MmI4NTUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjowLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImUzYjBjNDQyOThmYzFjMTQ5YWZiZjRjODk5NmZiOTI0MjdhZTQxZTQ2NDliOTM0Y2E0OTU5OTFiNzg1MmI4NTUifQ"
+          "resource": "repo://build.rs#L1-L43",
+          "version": "repo-lines-v1:sha256:c9fdc3d951792c05c7b026bd8f9bb2d4fb7f36b948ae20fe2b05cd0531edd276:eyJzZWxlY3RlZExpbmVDb3VudCI6NDMsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6Ijc1ZTU3NDhjMjZjOTBhZDA2MGZhMDRhYTY4NjU1MGExNTRlY2ZlMWZhZmI2NGQ4YzBkMjdkYTkxMWJkMDg2MGUiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjowLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImUzYjBjNDQyOThmYzFjMTQ5YWZiZjRjODk5NmZiOTI0MjdhZTQxZTQ2NDliOTM0Y2E0OTU5OTFiNzg1MmI4NTUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjowLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImUzYjBjNDQyOThmYzFjMTQ5YWZiZjRjODk5NmZiOTI0MjdhZTQxZTQ2NDliOTM0Y2E0OTU5OTFiNzg1MmI4NTUifQ"
         },
         {
-          "resource": "repo://src/main.rs#L30-L31",
-          "version": "repo-lines-v1:sha256:0cf641f44d6658b7b5f5b7157c9d533ac928fa26293fd9f090fcaf80de1ad016:eyJzZWxlY3RlZExpbmVDb3VudCI6MiwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiZTNhMzAyNmMyYTgzN2E1NWExMGUyZTY5Mjg2ZjcxNWNkOWM0YzA1MGI5MmNmYTQxODQzZjM1OTQ5MDgwODI0ZiIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNmQ0OWQyMmQ3NGM5NWQzZDFmMjJjNGNkN2JmNDA1ZGYxNWU2NTcxNDBjMGMwMWFjNDUzZmEwNjQxOTAzYWRhMyIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiYTgyNjM2N2RmNjQwYjVjMWRlNjJiMWU0N2RlNzhkYzEyZjZjZjE0MDIxMjk5OWU5ODZhYjUwNWZiYzJiNmRiMiIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiNWEyMmU1NDJiM2M2MWYzMjAzNTI1OTYwZGFhYTNmMjA4NGYxNTkxNDI4OTNhZDhhMjdhZDE3M2QzZWQ0OTRkNSJ9"
+          "resource": "repo://src/main.rs#L31-L42",
+          "version": "repo-lines-v1:sha256:3cdff8c39d365f638b3f6b7ce72287f226915a02d7934165c10e2756babdeaf1:eyJzZWxlY3RlZExpbmVDb3VudCI6MTIsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImM4ZTZlNzliNjA2ZTViMTc0NDU3YmZiZDI1MzI2Mzg5M2I0OGYzNGYxN2U0ZjUyNjQ0YTA4ZGQ3MDljZmE4NzkiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ5OTY3ZWMxZWQzODM2ZjhlYjE3YWZmMTcyYjViZmVmN2RlNjA0ODVhNDM5YzZjYTExYzU5MWUxNzU1NzM5YWUiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImE4MjYzNjdkZjY0MGI1YzFkZTYyYjFlNDdkZTc4ZGMxMmY2Y2YxNDAyMTI5OTllOTg2YWI1MDVmYmMyYjZkYjIiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjVhMjJlNTQyYjNjNjFmMzIwMzUyNTk2MGRhYWEzZjIwODRmMTU5MTQyODkzYWQ4YTI3YWQxNzNkM2VkNDk0ZDUifQ"
         }
       ]
     },
@@ -127,6 +127,6 @@
   ],
   "verification": {
     "by": "openwiki/0.7.2",
-    "at": "2026-10-23T21:26:24.156Z"
+    "at": "2026-10-25T12:54:53.684Z"
   }
 }

+ 62 - 24
openwiki/.claims/integrations/web-control-panel.json

@@ -1,13 +1,13 @@
 {
   "schemaVersion": 1,
-  "pageVersion": "sha256:deebb3a40f405864bed483e185086a069b8cd23cd1464a3e108c07917ca9b04c",
+  "pageVersion": "sha256:428fec496886d09cde951dc002e97021c4d9fc425843d101d1d5d17cc397bcc6",
   "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#L95-L104",
+          "resource": "repo://src/main.rs#L106-L115",
           "version": "repo-lines-v1:sha256:54c6448bd45241fd4b90479b1a91e2fd528e7a38f4ac5d543eda728fbf3756d5:eyJzZWxlY3RlZExpbmVDb3VudCI6MTAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImYwZjA3MmY1NzA4NTRiMGM0ODg3YmRlZDU4YzU2NWRiZGExOTNlYmY3NGIwODZmMjYyNTFmZjZhOTZiMmJiYWMiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImRmYWI3MTBkZGY3ODU5ZTU3NTc2NGE5MTNhMDAyYzc0NTg1ZmZhNzAwMTUxMGNiODgxMWRhODNkMmNmNzRiMzQiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImI1OTAxZTAwNGRkYTg1ZmUwNjg0OGVkYjhkMWIzM2Q1NTNmYWM4ZjkyMzZlZTFlMjlkZTNmNGUzMWRmNTM4NTkifQ"
         },
         {
@@ -15,7 +15,7 @@
           "version": "repo-lines-v1:sha256:7ca0571f2b3a7bab8e55e28c77db9e518e764cccc07f6f3db6ee22483494b39e:eyJzZWxlY3RlZExpbmVDb3VudCI6MTksImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjhiM2Y3MzZlODVhNTljMGQxNWY5NTM0YmZmM2ZhZjEwYWQ5NDBjZWQwODY3ODBkYWU1YjNlZDk2NjEwMjMyZDYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjhjNDU3YTVkZDNlNWJkNmQyZjgyZTU5MmRhN2IzOWQ3OTAxN2U0NzZkYjc0YjNhZjA0MDMwNWIwYmIyOGUxZTQiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjowLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImUzYjBjNDQyOThmYzFjMTQ5YWZiZjRjODk5NmZiOTI0MjdhZTQxZTQ2NDliOTM0Y2E0OTU5OTFiNzg1MmI4NTUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImU4ZTEwNzM3YjM4ZjIwMmZmZTRjOTFhYTU0N2IzNmEyNzA4NDQ2ODQ4MTVhYzJiZWZlMzc4MmNkMjBhMmNkZjgifQ"
         },
         {
-          "resource": "repo://src/web.rs#L185-L212",
+          "resource": "repo://src/web.rs#L187-L214",
           "version": "repo-lines-v1:sha256:ab494114a58ebd5d3aac2772fa0fad447024157f3749782a39f2e7b5d2db8a0c:eyJzZWxlY3RlZExpbmVDb3VudCI6MjgsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjAxYmE0NzE5YzgwYjZmZTkxMWIwOTFhN2MwNTEyNGI2NGVlZWNlOTY0ZTA5YzA1OGVmOGY5ODA1ZGFjYTU0NmIiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjMxZTI2MzA4NjJmNWZjZTY0Mjk3NGU4YTg5NmEyMjk3NWE3NDZlNjI5ZTcyMWNlYjY2YjhjZTgyOGM4NDJjMTgiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjA3ODdlYjhkMTU5MmY2NGVhMmE3M2NhNjA4ZmUyOWRkNjhlZDJiNmYyMDY2MmRlN2MxMmYxNmFjMTEyZWI0ZTIifQ"
         }
       ]
@@ -29,7 +29,7 @@
           "version": "repo-lines-v1:sha256:8d68f7263f5184bcacbcc32e3f2b90f72d9c90c4693a2bb0a0efaea433982b46:eyJzZWxlY3RlZExpbmVDb3VudCI6MTIsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjM5YzE3NTNiNjYzYWNhNDFjNDA1MzRiYzljMGQwZGE0OGQ1NTc0ZGIxMTU2MjdiZmVlYTViY2RmYTJhMjQ5MmMiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjU5NjJjZGY2NDAxOWI1Zjk5NTA1M2JmOTQ4NGU3MjAyM2I5YTgzYzlkMzUyZDhmMGVkNDRjZGFjZTEzOWZiMWUiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImNkNDMyMjkxNTVmYjM5ODIzZjhkMmY1ZDI1MDFkNzM1NzNiNjI0ODI0ODhlNWRlODg2ZGFiN2EzYTc1YjBmYmUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImM1NDU1MTc4NDk2MGFmNDM2ZTM2YmZkOGY2ODg0ZDg2ZWM3YTFlMmVlODA1NjU3OWY5YjMyMTQ5ZDk3OWJlZWIifQ"
         },
         {
-          "resource": "repo://src/web.rs#L340-L362",
+          "resource": "repo://src/web.rs#L342-L364",
           "version": "repo-lines-v1:sha256:30fb4995bbb20426970a2e31f5f3dd5e5c811e6590f215688cbf4a0fd2662bf7:eyJzZWxlY3RlZExpbmVDb3VudCI6MjMsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjkxYTU1YzdlMmE3ODBiZDViNzE1ZDY2MTYyYjI5M2M2ZTI2MWY2MmI2Mjk5MzY2ZWJiNTZmZjhhZTNhMDlhNDEiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjAzYjZjY2UzZDJjZWYzY2RhMWVkNWEwMzM5Y2QwMmM1ZGViNDRhNzBkMGYxNDA4MmJjZjNlYWYwMDJkN2U3NDkiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImMxYmNlM2Y3ZjQ5NjMxNWJjMWQ1NWYzYzE5MjE4MGI4N2NhYjExNTgxZWVjNGQ0ZWRjMWQ2MzYyNzhhOTllYjUifQ"
         }
       ]
@@ -39,15 +39,15 @@
       "statement": "Every POST route, including /api/relink, requires the header X-Wh-Request with value 1 (name case-insensitive); without it the server returns 403 and forwards nothing to the application.",
       "evidence": [
         {
-          "resource": "repo://src/web.rs#L166-L174",
+          "resource": "repo://src/web.rs#L168-L176",
           "version": "repo-lines-v1:sha256:81e9004c1f6afdff65111c92f71cf2a8b9e2fd1f87d5d54d60e8446e7b1dd361:eyJzZWxlY3RlZExpbmVDb3VudCI6OSwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiODU5OWJkMWM5YjdhMTQ5ZjczYzQzMjQ2MjljMzczMzFmOGNlZTM1NDVhN2IzYWUwNjk1ZWM0NmFlZjBiZmJjNCIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDEyY2EzNDVjY2Y3NWJmOWMwODA2YmNlNjk1YmU4ZGU4MDhiNzk5ODQyNTFhN2E1NGQyMDJjZjYxMDFkZDQ1MSIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiNGRhMDBiODU3YTc3N2IyNzQwZmQxZjg0MWIxZWQ0MzcwNmYwOTc5YWEyNzE5Zjk1YzJjOTJjMGNkYWEzOWJkYiIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiNGNmODQ4NzQ5MjczZDdkMWJlZDQ1ZTg2ZTNmMTU3ZmY3NTE0MjIyMTFkMWE3YzFlM2RhZDIzYWNkODNiZWRmNCJ9"
         },
         {
-          "resource": "repo://src/web.rs#L214-L231",
+          "resource": "repo://src/web.rs#L216-L233",
           "version": "repo-lines-v1:sha256:007fa67085367288f4893ce88fd4bad25907592bb8915e1a4649d9de382800fe:eyJzZWxlY3RlZExpbmVDb3VudCI6MTgsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImRkZDUwMWUyNTM5YWM1OTM1ODBjM2RiMTQ5YzBmMWUzMzBiZGJiNzU1YzZkYzJmZjk5Nzg3ODkzZjMwOWQ5ODMiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjE5ZTc2MzU1Y2YxMTk5NDgwYzNmOTk4MzZiMTQ4NmM4NDFkODkxZmFhZDkwNGM2MDhlMDkzMDIyZDM3NTdhZWUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImUyNmM2NzRiYmY2YzlkMzNlZjM0NTEwOGJhYmYzMmFmY2UxNzc1OTMwYjU0Y2JkOWU5NzkyNWQ3NDI2NWEyNzAifQ"
         },
         {
-          "resource": "repo://src/web.rs#L381-L390",
+          "resource": "repo://src/web.rs#L383-L392",
           "version": "repo-lines-v1:sha256:f6fc6a9f07a89bf80251845896e2bb83deb08f3b693960e20ec04eda2ee7cab9:eyJzZWxlY3RlZExpbmVDb3VudCI6MTAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjE1NzEzZjExZDY3OGYyYmQ4ZDhjMWUyYThjNmRlMDg4ODg4NDQzZWFkYmQ0NTdhZmQyMmJhNzlmMjY2N2RjYzkiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImU5NmY4Y2IwODNmYTgwMGE0ZGNkMjk2OTcyMmQ3YjkyNjg2YWRkOWViZTM2NTU0OTJmYTY3MmIxYTdjZWQyNDIiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjhiZGE3OWRiOTQ2MjJmMThhNzVjMWU4YmFmNzVlMTFhMWJjOGVmNzg4Yjg2M2VkMmQ2M2RhNTUyZTc0MGQyNDAifQ"
         },
         {
@@ -61,7 +61,7 @@
       "statement": "The /api/chat body is the chat JID as text, limited to 1024 bytes; oversized, empty, unreadable or non-UTF-8 bodies return 400.",
       "evidence": [
         {
-          "resource": "repo://src/web.rs#L247-L250",
+          "resource": "repo://src/web.rs#L249-L252",
           "version": "repo-lines-v1:sha256:4ec9fcc7650183cfb21c285788334f7540990a3b6ecc6c0102a5483b1042f772:eyJzZWxlY3RlZExpbmVDb3VudCI6NCwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiOTllNmY5YjI2ZmYxNmJlMThiYjFjOTA0Y2MwNWJhZjFiNTEzMzFhNjkwYTNhZjc0ZGYxNzJmOTdiNzQ3NGI1MSIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiMWU2NzNhYmQ0NTdhOTdlZWExN2E2MDA2MmViMGZhZWE2YmRmMDk0ODY4Y2M2YjhmOGMwZWJkYTJmNjFlZDQyZiIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiYmQ3ZTRhZGRhNDk4ZTkwMmJjZDA5ZGI5YzQ5Yjc4ZmE2NGEzNDk0NjgxMGUyZDhiOTBiYThjOTVkNmRkYTNjZiIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiMTcxMDE3YWI5NThlNTM3MjhlNmNhMTY0YzEzOGY1NzNmMjRhOWZjZDVhNTQzOWQwNTY2MzA1ZTE0ODQ2ODJhZSJ9"
         },
         {
@@ -69,7 +69,7 @@
           "version": "repo-lines-v1:sha256:0a12154e8c7fb736954719a1bc6bbb6919359fd03e35f9e1fc18873bf3953b71:eyJzZWxlY3RlZExpbmVDb3VudCI6MiwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiM2Q1NzUxODkwZTcxOGQzNTJkMDE1MTI4ZTdhYTNmOGZlYjgwOTg0MGFlMWI4OWFjNDlhNDFlMTViMjZiZjg0NCIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiODQ2NTQ3N2VkNjEyOTdmNGI4NTAyYmEzNmU4YzM3Nzg1NGFlM2Q4YzgzMDA1YmExOTI1ZDA2Y2Y3NTBkNWU4ZiIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiNzliYmEwYTMwZjdkZGRiODY5Njk1NTQ4NTc1OWE1ODg3M2YwMjM1NmVlNzQ2YjdhMjFjZmY1YmZjODhlODIyOSIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiNzdlNGFiODViNjRhOWM4YTlkYThkNWI5Yzk0YjViZjgzZTRlZjhlNWNkNWMxNjY1MDhkODYzYTJlYTI1ZDVkMiJ9"
         },
         {
-          "resource": "repo://src/web.rs#L392-L409",
+          "resource": "repo://src/web.rs#L394-L411",
           "version": "repo-lines-v1:sha256:ae5215de4d7affbc92b010adc8ddd1dfad664b5e5f3aeb27dba89330c0f5036d:eyJzZWxlY3RlZExpbmVDb3VudCI6MTgsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjEwODk2MWUyZDgwZWE0ZWQxNThjMDExZjkyYzQ4YjEyYWQ2ZjVhM2FmMjdlMjkwNDA5MDkyMTIxYTVhOGVlOWIiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImEyMzhhMjNmMGFkM2ZkODI4NjY4NzhlZTcwZmI0ZmQwYWVlODlmODFiMDJmZTFmYmQ0YjVmNDAyNzI2MzBiMTkiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjdiYzk4ZDZiMGRjNDkyYzUwMTZkZjEwODA1YTNhNTRlYjljMTg0YTVhNjEzN2U3ZjRiZWNmYTBjMjIwNTUwZDAifQ"
         }
       ]
@@ -79,16 +79,16 @@
       "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#L1139-L1156",
-          "version": "repo-lines-v1:sha256:bbc0ec4cc8f8ab99473d9ab5e429fb3b119ea7a8d7170646720a633d9ab98bc1:eyJzZWxlY3RlZExpbmVDb3VudCI6MTgsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjA3YTEzOTMxODdkZWNmZmM0NzMzYzYwY2FiYjA2MTkwYjdkYjAxMjJjYWEyYWM3YjRjNGUwNzBiNzIxNTk1NWQiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjM2MGUyOWExOGZkNDE5NDVjMjM3NTgxMzM4ZGI3NWFjOTIzYzYxYjVmYmFiOTNlOTM3ODdlNWJmMGMyMjk4OTciLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjQ0NmM2OGRiNzFjZDkwMmMyZDMxY2QyYjQ5Y2MyM2ZjYzFmNjVlYWRlY2JlZGRmNDFhOWEwZjVhNGIxMzE1MmQifQ"
+          "resource": "repo://src/app.rs#L1429-L1447",
+          "version": "repo-lines-v1:sha256:2f4f01d0941f73cc4f75516db52f02e977ce6e14b23e95d6c32f62cba267d74b:eyJzZWxlY3RlZExpbmVDb3VudCI6MTksImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjA3YTEzOTMxODdkZWNmZmM0NzMzYzYwY2FiYjA2MTkwYjdkYjAxMjJjYWEyYWM3YjRjNGUwNzBiNzIxNTk1NWQiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjM2MGUyOWExOGZkNDE5NDVjMjM3NTgxMzM4ZGI3NWFjOTIzYzYxYjVmYmFiOTNlOTM3ODdlNWJmMGMyMjk4OTciLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImNjODZlNGU1ZWEzODcxYTk3ZDc5ZGFkOTAzNTM1ZDc5ZTcwOGIzMjBiZTViZjA3NDM4MTEzMTk0MWEyNzRiZDcifQ"
         },
         {
-          "resource": "repo://src/web.rs#L138-L145",
+          "resource": "repo://src/web.rs#L140-L147",
           "version": "repo-lines-v1:sha256:9dff2fd116ff7266dafbe9bcec01fa24bb6ab864bc1c054f5a6fcb6449d60e7d:eyJzZWxlY3RlZExpbmVDb3VudCI6OCwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiMDgxZDE0MGYzZjljNjA2NWEyZmE5ZjY1ZTQwMmNjNjA0ZThkYmJhYmZlODc0YjAwODI5ZmNkNDhjZjNhY2ExNSIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDEyY2EzNDVjY2Y3NWJmOWMwODA2YmNlNjk1YmU4ZGU4MDhiNzk5ODQyNTFhN2E1NGQyMDJjZjYxMDFkZDQ1MSIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiNTgzZjQ1ZWYzZjM2ZTMyODc0MmY3MjgzOWZmODVhOWZlYTc4ZGVkNmNhYWMzZjk0ZjA1YzAxMjFmMWIyODc0MyIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiZTY5YTZmM2ExYjQ0MmYyNDllOTIzNWEzMTUxOWNkM2VkZTFlMDAxZWMzYzgxZmJhMWJiM2FlMTVhMjIwYjhmZSJ9"
         },
         {
-          "resource": "repo://src/web.rs#L275-L288",
-          "version": "repo-lines-v1:sha256:e50fde998c7b07b6e3110582bc1a3d6bad6d7a63d11a8ce8afdbe7deee340a4d:eyJzZWxlY3RlZExpbmVDb3VudCI6MTQsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjA3NTc5MmI3MWNhYTk3ZDhhNGNiMzI2NGVkYTgxMGE2ZTkxMmY1NTFlY2JkNzAzMTQxMTg3ZmQ0MTZjMjM4NzQiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImYyYjNmMGVkYWNhNjE2MDgyMThmODc3ZmEzM2NiNzJjZTc5NzMyZWJhMDc0ZjBkNTUxNTc3NWY4OWUwZTAwY2QiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjgxY2RjMWUzZDg3ZmIzMTllNzNjMDBiNTc4YzJiMDNjY2EyNGUyOTM3NTcxYzNhYTczMTI4NDhjNGZmNTFiMjIifQ"
+          "resource": "repo://src/web.rs#L277-L294",
+          "version": "repo-lines-v1:sha256:e861fb160a755c379dd5c34376e0b72ed82fbd0a5d14d1e45b1800dd0f424c99:eyJzZWxlY3RlZExpbmVDb3VudCI6MTgsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjA3NTc5MmI3MWNhYTk3ZDhhNGNiMzI2NGVkYTgxMGE2ZTkxMmY1NTFlY2JkNzAzMTQxMTg3ZmQ0MTZjMjM4NzQiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjczNDM2MjAyMzYwMmRiYmFiMWRmYzFmZmY5MzcxN2RiYzU2Yjk1ZDhhN2JmODFhYmVmNDQ2MTZlNmEwMDM3ODYiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImYyYjNmMGVkYWNhNjE2MDgyMThmODc3ZmEzM2NiNzJjZTc5NzMyZWJhMDc0ZjBkNTUxNTc3NWY4OWUwZTAwY2QiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6Ijc2M2E2MWI4NTY0MTFjMWYwMGJmYzJhNDE2YjE4Mjg4MThiYjk4NzYyMDYwOTYzOTZlMzMzNmQ4ZWM5OGYwODkifQ"
         }
       ]
     },
@@ -97,7 +97,7 @@
       "statement": "The quit route writes its 200 response before sending WebCommand::Quit without waiting for the reply.",
       "evidence": [
         {
-          "resource": "repo://src/web.rs#L256-L273",
+          "resource": "repo://src/web.rs#L258-L275",
           "version": "repo-lines-v1:sha256:155b300223ddf57fcf0446c63d0fe37474bd4a7d5c77f97cb07f527646eac78d:eyJzZWxlY3RlZExpbmVDb3VudCI6MTgsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImE0YWQxNDUzZDYzOTBhMjY0YWY3NTcxNTJlZmI2ZDdhMDM1YWQ4YmQ4MTE0ZWZkMWRhZDI4MzAzZDM4NGViNmIiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjE5ZTc2MzU1Y2YxMTk5NDgwYzNmOTk4MzZiMTQ4NmM4NDFkODkxZmFhZDkwNGM2MDhlMDkzMDIyZDM3NTdhZWUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6Ijk2YTMwMjE3MjllMmY0MzA2ZTIwOTY5YTU0YWM4YjFmMTA1MzIwMmM4Mzk4NWM5NzA4OTU3NmM2NzQ4YzE1MjAifQ"
         }
       ]
@@ -107,19 +107,19 @@
       "statement": "The browser page polls /api/state every 5 seconds while visible (every 2 seconds while WhatsApp is not connected), sends the X-Wh-Request header on every POST and orders responses with a ticket counter so stale polls do not overwrite newer toggle results.",
       "evidence": [
         {
-          "resource": "repo://src/web_page.html#L382-L383",
+          "resource": "repo://src/web_page.html#L416-L417",
           "version": "repo-lines-v1:sha256:4c8b0e851a2538b88a9455e95c394596dea318f931d8020c1d9573933faff85d:eyJzZWxlY3RlZExpbmVDb3VudCI6MiwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiOWE2MDhiOWU1ZmE1OWUyMjQyYWY4N2U4MDM5MmYwMjVmYmQ0ODFkYWJkYTZiNWMyZmI5Mjg3ZjIyOTk3MWE1OCIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNjZhY2FmNWU0ODFjNGJmMzE2YTM4YTZhOTczNmMyYjBhNDFmNGJkYTFmOTQ4YzdkNTU3NWIwYWE3YTE1Y2Y3MCIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiOWI4OWM1NTlmYWUxYTMyYzA2MDE1ODQzZWU2YWEzMWU3ZDY1M2JjZWQ2YzI1NmM4ZGFlMTllYWYwNjk4MGJmYyIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiYmY1MWRlZTMxZmYyMmU3NTE0YmRmOWYwNjNlYjIxOWMxZWI0MDYyM2ZlY2ZlMTI5ZGNiZjAyMGNjNjZjNTc4ZSJ9"
         },
         {
-          "resource": "repo://src/web_page.html#L441-L450",
+          "resource": "repo://src/web_page.html#L475-L484",
           "version": "repo-lines-v1:sha256:7b6d2b04110398692241256da2fc7b0be08d5ec5f56a928bacf625c9b8bfcb56:eyJzZWxlY3RlZExpbmVDb3VudCI6MTAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImVhZjIyY2YwNzMyNTkyYzI0NzE0MWNiZTYxZWZhMTEwMTI2MTJmNWEzMTIyYjlhZjliYmFmNTJmNTcyNGQxOTQiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImZiNTgwZTk2NGY4M2E0NzNmNDNkNDQzODQ2Mjk0OWQ2OGI1NjAwZmRhNzg3ZjE2Mzc0NWQwMGRlODljYmYzNmEiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImNlNDhiMTQzZTUxMjVjNzBhMjUyNTdmMTY2MzI2MDE1ZTBhYzdiYTBmMzlkOTY1ZTZjMmY1YTczZjlkZDVmYTkifQ"
         },
         {
-          "resource": "repo://src/web_page.html#L619-L700",
+          "resource": "repo://src/web_page.html#L703-L784",
           "version": "repo-lines-v1:sha256:c47b354da3f2a2f89fc81593d9acf896016b6125b92c47556517046b70378798:eyJzZWxlY3RlZExpbmVDb3VudCI6ODIsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImVhZjIyY2YwNzMyNTkyYzI0NzE0MWNiZTYxZWZhMTEwMTI2MTJmNWEzMTIyYjlhZjliYmFmNTJmNTcyNGQxOTQiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6Ijg5MTRjMDFmMzFhZjgzMzczMDk0NGE5MzZjMWJjNTg4MDlkNTkwNjAzNGMyNGNlY2EyOWE5YTU4M2MzYjE4MjUiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjFjYjQxNTIxYjRlM2FlYmVmYTc3OTBkMzMwY2E3NzE1NzJjNDUxMzU0ZTY2ZjI4Mjc1MGIyY2E1YTE1NmMxNjUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjM3OTI5ZTkxZTBmOTljM2U4MjVkZTY5YjQzNWZiNjQ4NGQxYjBhMWRmMmY0NjU2MWFiZjFhMjlmMDQxODhiODgifQ"
         },
         {
-          "resource": "repo://src/web_page.html#L739-L763",
+          "resource": "repo://src/web_page.html#L823-L847",
           "version": "repo-lines-v1:sha256:bdeccd31cb679fe52d1ccf1cfe46b8f36c2c4ca6a2ee5a02f02d5e018246a621:eyJzZWxlY3RlZExpbmVDb3VudCI6MjUsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImVhZjIyY2YwNzMyNTkyYzI0NzE0MWNiZTYxZWZhMTEwMTI2MTJmNWEzMTIyYjlhZjliYmFmNTJmNTcyNGQxOTQiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImZiNTgwZTk2NGY4M2E0NzNmNDNkNDQzODQ2Mjk0OWQ2OGI1NjAwZmRhNzg3ZjE2Mzc0NWQwMGRlODljYmYzNmEiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6Ijc0YzRlNDU1MWQ1NTYyYjdkNzM1OThiMTYzOTIyNmEyOTE2MDJmYmRjZDE4OWM0NTliYThiMmQ3OTkwYjIzOGIifQ"
         }
       ]
@@ -129,7 +129,7 @@
       "statement": "WebState also reports the current screen as connecting, pairing or main and, while a pairing QR payload is held, the QR code as an SVG document rendered by qr_svg; an unencodable payload yields null.",
       "evidence": [
         {
-          "resource": "repo://src/web.rs#L123-L136",
+          "resource": "repo://src/web.rs#L125-L138",
           "version": "repo-lines-v1:sha256:fbad46fb3a55d90c9fc4c33c1b9c90419816360630851b1b616ef2cbf9f6c491:eyJzZWxlY3RlZExpbmVDb3VudCI6MTQsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjU3MTZiNTVhZWU2YTVlNjNhM2U3Y2FlOGY2YjlhNGU0MzRhMmZmYTE4NTBhMjhhNTFkNjRjZmUzMDFmZjkxMmEiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImNlN2E5NWQ3ZGMzNzRjNzc1YjVlOWQ0OTE3OTFmYWRjYzA3NjdkYTFhOWFkNmZiMTgyZGNjMjk5NTc5YTcyOTEiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjE4OGVjMTc0NDhiZWQ3ZmI1YjcxYWYzZWUyZjAwY2M5MjAzMTJlMDM2YzBiODQxZTdmNGE2OTVhNjlmMjViYzMifQ"
         },
         {
@@ -137,7 +137,7 @@
           "version": "repo-lines-v1:sha256:5441ac1ffdef7b159cbf366e69f7988f719b0b5b0070fa9c484fc6ece13f85bd:eyJzZWxlY3RlZExpbmVDb3VudCI6MTMsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQwY2Q2M2U1YTllNmFjMzM4N2E2OWM1NTBmNWQ5YzIzMGIzN2NhZGM4ZDI2ZDllNTI5MGVkMzU3YzE5NzY0YWUiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6ImVlMTI1N2EyNDMwZTYyZDRjZTU3ZTQ2MjlmZDg0YzNiYTcyY2RhNDgwMDM3ZjIzZGY4MGVkYzZiNDA0NDg4MDgiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImYzYmEzMTQwZTgwZjNkNzlkNTQ2YjA5YjgwZTljY2RjMGM4ZDk5ZjllNmYyNjljMjUyZmE0OWQyNzBlMGZiMjIiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjgzYmIyNTJjMjU1NmVmMTM5MWJjNDU1YmEyOTAxZWQ5MTczNmQ2Njc2MmEyZThmYzExMjJlYTFhMWYzYjVmZmQifQ"
         },
         {
-          "resource": "repo://src/web.rs#L364-L379",
+          "resource": "repo://src/web.rs#L366-L381",
           "version": "repo-lines-v1:sha256:e6d58dec7c8a45140e76290e175a7b2f5a9b3c59f60b3e244064813974a3ed1d:eyJzZWxlY3RlZExpbmVDb3VudCI6MTYsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjMxYjQ2MDBlYjQ4M2JkNTEzNjA1ZTE0ODVkNmRjNmNjMDA3NjJkNzRiODNjNzg4OTQ2MGQzOWNkMGRjMzQ1YTgiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjE5ZTc2MzU1Y2YxMTk5NDgwYzNmOTk4MzZiMTQ4NmM4NDFkODkxZmFhZDkwNGM2MDhlMDkzMDIyZDM3NTdhZWUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjQ0NGIyYWRlMGYzNzRmOGZjZGYxNGM5MTQxYzNkNTMxYjcxMzgwN2ZkZTc0NzI1NTZiOWJlNDI1MjE5NzM4OWYifQ"
         }
       ]
@@ -147,18 +147,56 @@
       "statement": "The browser page shows a pairing card with the connection progress or the QR image while the screen is not main, and its 'Nuovo QR' button asks for confirmation before posting /api/relink and applying the returned state.",
       "evidence": [
         {
-          "resource": "repo://src/web_page.html#L596-L617",
+          "resource": "repo://src/web_page.html#L680-L701",
           "version": "repo-lines-v1:sha256:2996469d07728fe9a9430abce7e412c174f30205d800ed61434df03a8c0d8913:eyJzZWxlY3RlZExpbmVDb3VudCI6MjIsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjkyYWQ3OTc4ZjRiODU3NDhjOGZiNzEzYzlmZjAyMTM3MDYxMDVkZWY3MjQwMmNjMGQwOThmZTg0MTUwYTRiYWMiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImFhMjkzNzIyNTU4YzUyZGEzMGUzYjhjYTFjMTkyYmE1ZjlmNjA0NTNiOGJkMThmMjhlYjEwYTk5ODgwNGIzZTMiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjExZTBkOTIwNWRlZGQyZjZkYTM2MTlhODdmOGYzZDY5Nzg2MjQ5YzAyZTVmYjAwNTdkODdkNzE5ZGIwNjMyY2IifQ"
         },
         {
-          "resource": "repo://src/web_page.html#L703-L718",
+          "resource": "repo://src/web_page.html#L787-L802",
           "version": "repo-lines-v1:sha256:edcf716f5c77bc9ba443f65a19535e9c1b254b9949a238743590df637a35157e:eyJzZWxlY3RlZExpbmVDb3VudCI6MTYsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImVhZjIyY2YwNzMyNTkyYzI0NzE0MWNiZTYxZWZhMTEwMTI2MTJmNWEzMTIyYjlhZjliYmFmNTJmNTcyNGQxOTQiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6Ijc2NTNiYWEyY2QxMjM0ZDlmZTgzOGNiZTJhN2EzODc1YTcyMWMxOTRmNGRjMjZiZTgwYjZlOTU5MGY2YzE3MmIiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjU4MjJhZjlkZGY3ZTI1ZjQxYTFmZjU1NDA2MDljN2FhNGNhZmZjODBkMjhjM2VjNjkxY2ExY2RmOTMxMzQ0MDMifQ"
         }
       ]
+    },
+    {
+      "id": "claim_b1096fa5dd8d4546896d8f4e96f57271",
+      "statement": "Each WebChat in the panel state carries an optional avatar_url, the WhatsApp CDN URL of the chat's profile picture preview taken from App's avatar cache, null when not fetched yet or without picture.",
+      "evidence": [
+        {
+          "resource": "repo://src/app.rs#L1990-L2006",
+          "version": "repo-lines-v1:sha256:c2d9c9a91b384e368f619e50dfb7b5c40a6791f1e5936afb8ff8eeddeb228dad:eyJzZWxlY3RlZExpbmVDb3VudCI6MTcsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImYxMTFhMDk4Y2E0M2I2Zjg1OTYwYTUxMjFiOGVmY2Y5MDVlZDZmNjdmZjE5MjY0NjcxZDhkYTFiYzhjZTEwNWQiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjE5ZTc2MzU1Y2YxMTk5NDgwYzNmOTk4MzZiMTQ4NmM4NDFkODkxZmFhZDkwNGM2MDhlMDkzMDIyZDM3NTdhZWUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjZmZWE5NWYzZGNkZjE2MDA3NzQxOTRlMGQxYzQ4MmM0YTFmZTYwNjcyYjNkZDAwNTk5ZGRjNjg4YTg2YTFlYWUifQ"
+        },
+        {
+          "resource": "repo://src/web.rs#L112-L123",
+          "version": "repo-lines-v1:sha256:c5fdd49df38b20cfcf7dceab82726fe734595fbadb9f46e8ff6e5d719135051c:eyJzZWxlY3RlZExpbmVDb3VudCI6MTIsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjM4NzliYWUzOGUyYTU0NjA2NzhhNGMzNTEwZmY4ZmRhM2RhNjRlNzg5N2NlZGJkMjViM2U1N2U0MzhjZDVlMDEiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjE5MjE5MDY2M2Q0YzgwMTM5MjgzNGZmZDdjMzNjZGRmODI3YjYxMWRjYTY3OGNhZTc2ZGY2OTJkYjY5MGUyNTciLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImM5MjU2ZWJhMWRkYzMwZjUxNzllYTZiZjVkM2YwNDEzYmVhNzlmMzcwZDZiNzdiYmUzODQyODYyMTNjMzY3ZjYifQ"
+        }
+      ]
+    },
+    {
+      "id": "claim_fca3032fc2c94efb808f699e7522639e",
+      "statement": "Every web request ends by calling maybe_start_avatar_fetch before the state snapshot is sent, so profile pictures are fetched only while the panel is polling.",
+      "evidence": [
+        {
+          "resource": "repo://src/app.rs#L1434-L1447",
+          "version": "repo-lines-v1:sha256:95ccc404cb44631061c7ce41c398bebcd048da051c5235b86d9599d5d4b19096:eyJzZWxlY3RlZExpbmVDb3VudCI6MTQsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjhmODZlNzYwNTJhYmUwM2IyNzhmNDYwNDMzZjkyYTYxZTlhNmZjNmEwNjcwNDI4ODRkNzIyYzZlYzZjMDVhMDIiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjkwM2FjNDE5MWFjMTU4NzhjMTg5N2Y3OTdjMDU3MGM5MzZkOWUzODQ4Mjg4MTkzZDFjNmQ4YTdjMzAwZDUwNDMiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImNjODZlNGU1ZWEzODcxYTk3ZDc5ZGFkOTAzNTM1ZDc5ZTcwOGIzMjBiZTViZjA3NDM4MTEzMTk0MWEyNzRiZDcifQ"
+        }
+      ]
+    },
+    {
+      "id": "claim_a093c2f5ba2b4cbea9d25e3baa673f93",
+      "statement": "The browser page renders each chat with a round avatar showing the upper-cased first character of the name, overlaid by the profile picture when avatar_url is set; the image src is replaced only when the URL changes, it is loaded with no-referrer policy, and a load error hides the image so the initial shows.",
+      "evidence": [
+        {
+          "resource": "repo://src/web_page.html#L186-L215",
+          "version": "repo-lines-v1:sha256:02e39e40f7618fd3d1b7eec5e87503b1b0ee1acb7cde76875c4387d28cb6d42b:eyJzZWxlY3RlZExpbmVDb3VudCI6MzAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjdkN2M0YWY2YzM4NThkNDc0Y2E3NDU0NzgyNjEzNjc4M2E3MmY4YTZjYTc0Mjc4NjlhN2IwNzA0Y2IyNmRmMDIiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6ImVhMDNlZmQ2YTA5MTllOWZkMWE1NDc4ZGJiODhjMGYwZTJlZmE1MjlmZWFiMDYzYWIwMDcxNjAxZTVjZWRiMmEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjZiNDc4NmNiNzI3NDE2MDVjZWFiMjVkNjcwNDdjZDk2YTc3ODA5ZmIzMzdiMjM2MDE5MTRhNjhlMTk0MGMwOTIiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjAxMDczMGVhMDBmNjU3YzIzMDYxNmViZjgxMmZlODEwMmQ4YmJjYzFjOWZmZGNjNWFjZWRjYTY1ODlkZmE2NTgifQ"
+        },
+        {
+          "resource": "repo://src/web_page.html#L543-L624",
+          "version": "repo-lines-v1:sha256:0f784ee083486fa4bab91b1c565bb4f77f4099ba8a05f58b1ba83f9d8f394736:eyJzZWxlY3RlZExpbmVDb3VudCI6ODIsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImVhZjIyY2YwNzMyNTkyYzI0NzE0MWNiZTYxZWZhMTEwMTI2MTJmNWEzMTIyYjlhZjliYmFmNTJmNTcyNGQxOTQiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjE0Y2IyNzU0MWM4OTUxOWJlODQwMWM4Y2ZmODE3OWQwYmU5OGFjMTk1NTkzZTg1MDYxYTJlNWMzMjMwNDk1NjEiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjkxZGZkZTU1OTBmNGZkMGVmNzI5ZDNhZGI1MmE0NjIyNTYxMGM3NzNjOGEzZTQ0NGFkYTRiYTg4MDMyZjExYTgifQ"
+        }
+      ]
     }
   ],
   "verification": {
     "by": "openwiki/0.7.2",
-    "at": "2026-10-23T21:26:24.156Z"
+    "at": "2026-10-25T12:54:53.684Z"
   }
 }

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

@@ -1,6 +1,6 @@
 {
   "schemaVersion": 1,
-  "pageVersion": "sha256:4bfc94bcec7d0629fe535e916bfc05dd51ef04c1fe94a8b004f82f7f5cfb98d9",
+  "pageVersion": "sha256:b6ea56337534d7b40808dce1aa32c6121c8c75af2f67d1860b5246e54b4d346b",
   "claims": [
     {
       "id": "claim_5cd95bce2100497cbe7ac9e4c2cb1856",
@@ -17,26 +17,26 @@
       "statement": "start wraps the Session built by open_session in a WaHandle; open_session requires a UTF-8 session path, creates its parent directory, opens SqliteStore, builds a Bot with an EventForwarder, spawns the Translator and bot, and when the bot ends emits Disconnected(client stopped) and marks the session stopped.",
       "evidence": [
         {
-          "resource": "repo://src/whatsapp.rs#L351-L365",
+          "resource": "repo://src/whatsapp.rs#L377-L391",
           "version": "repo-lines-v1:sha256:803ade08ccaecd3502b4e412e74510353b9c4495ad8c7ba92531c66c20e7145c:eyJzZWxlY3RlZExpbmVDb3VudCI6MTUsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjEzMWZkNzY1ZDk5ZGViYjBhNjQwNjJjNzhjMzdmYjVkYjZlODdmOWIwMmJmMDVhOTE4NzQ5NGFmYmM4ZjgwYmEiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjE5ZTc2MzU1Y2YxMTk5NDgwYzNmOTk4MzZiMTQ4NmM4NDFkODkxZmFhZDkwNGM2MDhlMDkzMDIyZDM3NTdhZWUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjIzZDg5MDg5YWZjZjlkYTg2NDg4ZDFjM2E1NDk2YzEwMDIxZGNlMWE0Mjc2YTYwNWUyNzY3ZWM0YmVjYzliYWUifQ"
         },
         {
-          "resource": "repo://src/whatsapp.rs#L422-L469",
+          "resource": "repo://src/whatsapp.rs#L448-L495",
           "version": "repo-lines-v1:sha256:dd10b6e01369abf7053e52f04c2399ba9fe4a096f9593c2658e91f60650f6fbc:eyJzZWxlY3RlZExpbmVDb3VudCI6NDgsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjRmZDNiZDg2NTQyNDAyYjk5Y2JjNDI1MGY1YmQ3YTE5MmNkM2VhODgyNGFjNjFiMDI4NTc1NzQ1MzZmNDAxYTgiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjM1ZjZmNDY2ZTU5Y2ZjMzA3MDNjNDVmNzJhZjY1ZGU1NGFjMzYzZDM1NGNjMWEzMzMwMzUwYWNkNDUyNDM0YzQiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjIzNjEyMDNhNTJjMmI5YTk4MmJjMjVkNWE3OTU1OWIwNGJlNjAxODcxYzZlZjk1ZWExZTMzNzU4MmM2YTRlZDcifQ"
         }
       ]
     },
     {
       "id": "claim_6378b4528ce94c26a0f959ac53e0dcf8",
-      "statement": "EventForwarder forwards raw events in order to an internal channel with interest limited to SUBSCRIBED_KINDS, which includes OfflineSyncCompleted.",
+      "statement": "EventForwarder forwards raw events in order to an internal channel with interest limited to SUBSCRIBED_KINDS, which includes OfflineSyncCompleted and SelfPushNameUpdated.",
       "evidence": [
         {
-          "resource": "repo://src/whatsapp.rs#L472-L493",
-          "version": "repo-lines-v1:sha256:90cc37eed600d805f12bf6cff91abb61379cdfa881ee401bcd902a8e11133f60:eyJzZWxlY3RlZExpbmVDb3VudCI6MjIsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImQxN2YyYmY0MzZiNzViNGYzNDUyNWY2MTk0NjU4OGE5ZmQ3MjRkNDA2ZDNmODk0NjI4MTE2ZGRlNzg0NDVmNTAiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImZiOGQyYjQ2OTczM2U4ZTNmOWVjNWRhMDk2Njk3NDlkMDJiYTczYjM1YTQzYmQ2YzRkNjNhYTE4M2FkY2U5ZGUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImU0NTQ4YjU3YzUzZDdhYmNmY2QwY2E0YTJjZGZiZjdhYjk4NWYzMDYzMGM1N2FiYTA2Y2U1MGZmYjc2ZjZiNmMifQ"
+          "resource": "repo://src/whatsapp.rs#L498-L521",
+          "version": "repo-lines-v1:sha256:bca74a385b9091beda144a8c0183d20aa932f665087f42d4beb3af7db35c9159:eyJzZWxlY3RlZExpbmVDb3VudCI6MjQsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImQxN2YyYmY0MzZiNzViNGYzNDUyNWY2MTk0NjU4OGE5ZmQ3MjRkNDA2ZDNmODk0NjI4MTE2ZGRlNzg0NDVmNTAiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6ImI1NjZiM2FlZmJlNTVjMjIxOWUzNzMxM2JhYjljMGQ2ZDRjZmNkNzc1MzI2YjA0NTVlMWNmNWJjNTMxNzhmYjciLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImZiOGQyYjQ2OTczM2U4ZTNmOWVjNWRhMDk2Njk3NDlkMDJiYTczYjM1YTQzYmQ2YzRkNjNhYTE4M2FkY2U5ZGUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImQwMWI5OGRjYjFjNzJjMzNlODliNWE5ZGFkYjlhNzI3OGQ5OGRhYmMwNzdiMjEyNWRlNGY2M2U4YzM5NWM3MmMifQ"
         },
         {
-          "resource": "repo://src/whatsapp.rs#L58-L77",
-          "version": "repo-lines-v1:sha256:361053245006ac3f73c75b71c22349865d2290626d5d26e9439e3cedd7193959:eyJzZWxlY3RlZExpbmVDb3VudCI6MjAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjkxN2YxNjJiMTNkNjQ1MzcxOWQ0ZTgwZGQxNzc3NzM5Yzc4Y2M5YTIxNDM4MTBmNDU5ODBhOGQwNWUyOThjNzMiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6ImIwMmQyYzBkNmRjNWU3NGQ5MmZhMTRiOTFkYmVhMjY0MDkwYmViMzVmZGI5MTg4ZDliMzQxMTgxZGNlOWY0Y2UiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjNlMzgwNWRiNmFmZTQwNDJiYTcwZTM1YTQyNGFmYWEwZDIyOTUxYzUzZTEyZWU5YThiMTFhMGRlNjZlMDMyMjciLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjhjODM5ODYwY2FlZWIwMmYwOGMwZTE3NDIwYTNjOTFlYTRhYmY0NTg5OGUwNGI3OTIzZjRjYmFhZTRiNjg3MTIifQ"
+          "resource": "repo://src/whatsapp.rs#L59-L79",
+          "version": "repo-lines-v1:sha256:87bbc3ae031917c27f6e04c90441a5b54471fb37ee9b7bd037caf05dea74809e:eyJzZWxlY3RlZExpbmVDb3VudCI6MjEsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjkxN2YxNjJiMTNkNjQ1MzcxOWQ0ZTgwZGQxNzc3NzM5Yzc4Y2M5YTIxNDM4MTBmNDU5ODBhOGQwNWUyOThjNzMiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6ImIwMmQyYzBkNmRjNWU3NGQ5MmZhMTRiOTFkYmVhMjY0MDkwYmViMzVmZGI5MTg4ZDliMzQxMTgxZGNlOWY0Y2UiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjA3YWViNTc3MWNmZmNhMWY1MDgyMjc1NDRlMTdlZGU0MTg5Yzk0YzY1ZjQxNWUxOTZkYzk3ZGUzMDZmMTQ0M2UiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjVlZDMzYjI4ZjJlNzgwZTUyNGJlMDdlZWFiNDMzZGI5MDAxNDNjNjUxOTJhNzk0MjJiYzlmOGRlMDNmOTAyNWIifQ"
         }
       ]
     },
@@ -45,7 +45,7 @@
       "statement": "WaEvent has Qr, Connected, Disconnected, LoggedOut, OfflineSyncCompleted, Message (with optional image and audio references, Some only for live media messages), Contact and Error variants.",
       "evidence": [
         {
-          "resource": "repo://src/whatsapp.rs#L109-L135",
+          "resource": "repo://src/whatsapp.rs#L115-L141",
           "version": "repo-lines-v1:sha256:c833ef517073c3c33b9b8885771f54d2c0026fb424dc83e1cb1c7850d0e6b75d:eyJzZWxlY3RlZExpbmVDb3VudCI6MjcsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjgwYzU5NWY0YTU0MTFjZTFkNGJjNzJiZTEzMjQ0ZTgwZTc0MDg1MTAxZGQwYTdjNzQ4YjcyYzViYWQ5MDkxYTkiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjIxOTJmNjU0MjE1OGExOGY0MDRlMjg1ZmI5NGM2N2VjYmFmMjg0NzUxOGM5ZjQ3Y2IzZDA3MjVmOWYwNTk0NTUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjY1MTM1NTA1ODhkNzA3ZGJiODBlZDU1N2ZjZTRlYTU1NjY0ZWVlMzM2NWE1YzQxOGJjM2FiZjQ4MzNiZDNmZDYifQ"
         }
       ]
@@ -55,8 +55,8 @@
       "statement": "Translator::handle_event maps raw events to WaEvent, including OfflineSyncCompleted, and turns failures into descriptive Error texts.",
       "evidence": [
         {
-          "resource": "repo://src/whatsapp.rs#L638-L687",
-          "version": "repo-lines-v1:sha256:be3833261dc69977863314832b6160d74b61cfd510ba80f67d687d7b53649355:eyJzZWxlY3RlZExpbmVDb3VudCI6NTAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImUyYWRmNTE3NDJlNmQ4MmEyNDE0NjE5ZmJlOWRhOTMzZGIxYzVjYTZlZGIwZjRhZmJhMGE5NmMyNGEzNjA1OGYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6Ijk5OWJkZWZlZWFjYTYzNDBlMDI5MGRjODZlODg5Yzg5ZDVlMTg5YWE1YmE0NGI1ODViM2I3MDczNGY0YTYyZDgiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImM2ZTYxOTdhNjY3YmI1ZjgyYzI4ZDdmYWM2OGIzYWE1YzU1YTk5NmQ2NGRkYmYyOTI3ZWQ3YjAyNzk5MDI0NWYifQ"
+          "resource": "repo://src/whatsapp.rs#L667-L717",
+          "version": "repo-lines-v1:sha256:aeb0c216df6b8d6c80ab3f10a5ffc427032420714d5229bb8fe2ab60a26b4fcb:eyJzZWxlY3RlZExpbmVDb3VudCI6NTEsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjdmNWQyMzJiN2E2YTQ3NzhhZWE4NGU5YTIzNDhjMWU3NDdlZTdjOWY2NzUxNThjNWEwMGMxNDIzMTBlYTAxNTciLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6Ijg2ZDM0ZTcxMTQyZjZhMTJjYmM1NzA1N2QxMTc0ZGE2OGM3MjBkMmRlNGY1YmRjZDMxN2FhMjVjZDljYTE0NDEiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6Ijk3MmY0NmVmYzdkN2U4M2ZhYWYxZTk2NjRiZDE3ZDg1ZTBkODIwODUyY2I1YzZlNjYwMDIzNTY4MzI0ZTk3ZWUifQ"
         }
       ]
     },
@@ -65,7 +65,7 @@
       "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#L594-L609",
+          "resource": "repo://src/whatsapp.rs#L620-L635",
           "version": "repo-lines-v1:sha256:c9a3a286dc3e2c684087cd5ca273467ac6be5cc180556477e58883a1587ad161:eyJzZWxlY3RlZExpbmVDb3VudCI6MTYsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImRjNzhjNDlmYTU0ZmZiOGEwMmNhNDczYzhjYTFmYjY3YjVjNDQyMjBiNDUyMDYzNWI0NmI1MjgyODRkODc1MjgiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjY3ZWQ4MGI0MjM1MDQ2ZTljZWQ4OTEyNjc0ZDRhMTMwMjM3MzJiNTNhYThhNDdhMGYxNzUyNGI2ODM1ZjAxM2YiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjcyNDliZThhYWJlNTBkZDkyNzY5NDkwNzViMDI3NTA1Y2Y0ZWQ3MWE3NmIxYmJiZjg0ZjFhZTFmMjdkZDU4Y2UifQ"
         }
       ]
@@ -75,7 +75,7 @@
       "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 and an AudioRef only for voice notes.",
       "evidence": [
         {
-          "resource": "repo://src/whatsapp.rs#L703-L755",
+          "resource": "repo://src/whatsapp.rs#L757-L809",
           "version": "repo-lines-v1:sha256:b8e0680690aaa46f7e4bc079ff00bc4c7cd87b2d32b6fe4595fb846fad76e44f:eyJzZWxlY3RlZExpbmVDb3VudCI6NTMsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImFkMmYxYjJjNzYwNGI3NDk3NmI5YmMyNjRkMDBlYjJlODJlZWM0MTk5NzE4NzJjMWFjOTE2OTJhNDQ1ZjMxYmYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjdkZTRkYTlhNTUwNmQ1MmI3NWUwOTY1YjY0ZWI2Zjk5ZDhjMWE4MmZjMWU1ZjE1YTZhMjY0MDhlYjc2MmJlOTUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjY2NDFiNWZlM2M5YmRjNTg2MGQzNmM1MzMzZjMyYTY2OGU4OWUzNWJjZTBiNjU2ZTViYjY2MjVhNjFhNTE3ZWQifQ"
         }
       ]
@@ -85,11 +85,11 @@
       "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#L495-L502",
+          "resource": "repo://src/whatsapp.rs#L521-L528",
           "version": "repo-lines-v1:sha256:63fcb3bcc3fee239b6af98cdaeb31163e5ebb60f59467e221c3df0d186d7af3c:eyJzZWxlY3RlZExpbmVDb3VudCI6OCwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiYjU2NmIzYWVmYmU1NWMyMjE5ZTM3MzEzYmFiOWMwZDZkNGNmY2Q3NzUzMjZiMDQ1NWUxY2Y1YmM1MzE3OGZiNyIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDEyY2EzNDVjY2Y3NWJmOWMwODA2YmNlNjk1YmU4ZGU4MDhiNzk5ODQyNTFhN2E1NGQyMDJjZjYxMDFkZDQ1MSIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiMTllNzYzNTVjZjExOTk0ODBjM2Y5OTgzNmIxNDg2Yzg0MWQ4OTFmYWFkOTA0YzYwOGUwOTMwMjJkMzc1N2FlZSIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiMjg2NmRmMDRmMjc4MGQyOWI2MDQ1YWM1OTM2NmUzZjI2OTk5ZDIxMTdjZmVhZTcxY2ExZGQ5NzcwNGE5YTY1YiJ9"
         },
         {
-          "resource": "repo://src/whatsapp.rs#L557-L575",
+          "resource": "repo://src/whatsapp.rs#L583-L601",
           "version": "repo-lines-v1:sha256:0cb33ca45f9b72198804d2a68dffd870e11f0018eff0f5b4d6e6639f0dabecb2:eyJzZWxlY3RlZExpbmVDb3VudCI6MTksImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjI3YzZkOTQ3ZWQ4OWVjMDgxZmY1ZDUxZDliMzYxOWRhOWRmMjZhODdlZGYxM2MwNDMwMzhiMDRjN2E5ZTliZWEiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjZmYjQ4MjFjNTZhMzYwOTAzMGFlYzM0MWQ4ODI5NDhmZjBlMmYwY2VlNDQ1MzQ0YzczNTJiYmMwYjlhYTA3Y2EiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjY1NmEzOWUwZGJjMTIyMGQ0OTg3Mjk2NDBiOTEyNWQ0YjIxMWMzNmVmZGRlODVkMWIzZmM4NjgyZTU2NTE0NWEifQ"
         }
       ]
@@ -99,11 +99,11 @@
       "statement": "History sync chunks are decoded in spawn_blocking and emit push names, inline contacts, conversation names and messages without media references.",
       "evidence": [
         {
-          "resource": "repo://src/whatsapp.rs#L789-L836",
+          "resource": "repo://src/whatsapp.rs#L843-L890",
           "version": "repo-lines-v1:sha256:d9d9b03372176b8ae315c56ec351773837c3d40ddbc09cb3033511ed440dd9f6:eyJzZWxlY3RlZExpbmVDb3VudCI6NDgsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjAzMDUzNzcyNjU4MGYxZDA2OWQxMjUwNTI3MmRhODg5OWM0M2RiYmZiZmM4NmZiY2IzOGJhMmNhZWE0OWU4NDIiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjgwMmNlYTE4N2JjY2NlOWZhNDkyM2M4NzAzZDliYjk3ZWZhZTZkNTk2YTc4OTQ2YTlhZWMzMzc4MDM3YjU2MWMiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjVmMTVmNmFmMDRiYTEzNmRmMTIyMDNiYWY0OWQ1MzZiNDE3OGUzYWYzMWRiMDcxMThhMDhhMzUxOWRjZWFhYzYifQ"
         },
         {
-          "resource": "repo://src/whatsapp.rs#L939-L984",
+          "resource": "repo://src/whatsapp.rs#L993-L1038",
           "version": "repo-lines-v1:sha256:0a7d1c26aa2241d3be70b89924daa94a2d48124759104006fc773fe4d264eb56:eyJzZWxlY3RlZExpbmVDb3VudCI6NDYsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImY1N2FhZjc1ZDg1ZGRhNjUwNDRiYzA5YmE2YjgxN2ExODFlMDNkZmIxNWY5MzIwYWIzNGZmYjkzOTc2MzljZGEiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjJhMjQyN2EwYjJhY2QyYzcyMGE1Njg1ZDM2NjNlMzZjNDNlY2I3YTUzNjU3OTE2MDk2NmQ0M2ZhOTY2YTgzOTciLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjliYjA3YjBjZTBkNjhiMjljNDMxZWQyZjUyODcxN2Y0MDY3ZTA2NzZlNWViZGIzNzQ2N2Y5ZWZjNTM1YmI4YTMiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjViZDM0NDI4NjBhN2VjNjA0NTEzYmIxY2Q0MmFiNTk3MzY5MWQ3ZDQzMTdlYTJhODU3YzUyN2UyZDNmMTRkYTQifQ"
         }
       ]
@@ -113,7 +113,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#L1071-L1129",
+          "resource": "repo://src/whatsapp.rs#L1125-L1183",
           "version": "repo-lines-v1:sha256:2b1c51b48fce477ab09efb54b0e727a20b91bb212e43c005ffb0ba48ba3f3c2b:eyJzZWxlY3RlZExpbmVDb3VudCI6NTksImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjNjNjE2NzkwYTVhOWYyMTUyNGQyMTkzMGYwODhiYWNiYjkxZmYwNGQ1YTcxZTQzOTljNjEyMjdiNTVlNmRiODAiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjkwNzA2MWVjY2M3ZmRhZTJiN2I0MjlhYTAzYTZkMTNmMjNjNTY5NzZmMmRmOWU5Y2UyY2ZhNjM4ZjdlMTQ1ZDMiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjcwNzFhZGQ3YWQ1Mzg3NDYwZWJmZWY1YTkyOGQyMTAxZGJkMGQ2ZTU2YzRhMzliNzA0NDViMWM3OTU3NmZlNmEifQ"
         }
       ]
@@ -123,11 +123,11 @@
       "statement": "send_text rejects empty text and returns the message id; download_image keeps images in memory with an image/jpeg MIME fallback; both fail while no session is running.",
       "evidence": [
         {
-          "resource": "repo://src/whatsapp.rs#L233-L243",
+          "resource": "repo://src/whatsapp.rs#L239-L249",
           "version": "repo-lines-v1:sha256:8ed098e34276e2c4b9ff74e786b647375c2d873d70640b609b0ae4bca9505394:eyJzZWxlY3RlZExpbmVDb3VudCI6MTEsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjEwYjNlNTU4ZDczYzgzNWI1NWVkY2U4NWUxNTMzYjM1ODczM2U1OWU4ZGNmYTJmZTM3N2FmNDBjZTcyZjY4OTUiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjE5ZTc2MzU1Y2YxMTk5NDgwYzNmOTk4MzZiMTQ4NmM4NDFkODkxZmFhZDkwNGM2MDhlMDkzMDIyZDM3NTdhZWUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImE3M2Y4MjFiY2U5NjYxNDdhMDEyMmRjYjVhZGExZmVjZjgxMWIyZmNiNjIwZDUwZGFiY2U2MTg0OTUzNjVhNWUifQ"
         },
         {
-          "resource": "repo://src/whatsapp.rs#L272-L310",
+          "resource": "repo://src/whatsapp.rs#L278-L316",
           "version": "repo-lines-v1:sha256:6876a8217f10e021bfed1374f2f51f88f81c6fd8b45ae385c7fdf5056b93c1bb:eyJzZWxlY3RlZExpbmVDb3VudCI6MzksImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImRhZmU4MDgzNzljMjNlNzE5MThmNzNiNmYyZTU3ZGZjYjIxM2U3Y2RhNDhhZDZlYzY5NWFlNGFhODZiODIxNDEiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjE2MmVlNTdlNDFhOGMyMGFlMDE2NGRmMjYxNDM5ZGNjNDk5ZDM1MjA4MzQxNTE1MTZkMDQ3ZThkZWFlOGE0NjYiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjMwZGZlODgwMmNkNjM3Y2NmMjZjNWIwYjZhOGJhYmNhOWRhYzI5NTNlOTRmYjA4YzFjNzA1NGVkOWUyNmYwMmUifQ"
         }
       ]
@@ -137,11 +137,11 @@
       "statement": "own_jids returns the account PN and LID without device suffix and is empty before pairing and while no session is running.",
       "evidence": [
         {
-          "resource": "repo://src/whatsapp.rs#L1060-L1062",
+          "resource": "repo://src/whatsapp.rs#L1114-L1116",
           "version": "repo-lines-v1:sha256:2dff1120b156dde5cd502a03211b000f533177e7214711f8a8a081955c3663ac:eyJzZWxlY3RlZExpbmVDb3VudCI6MywiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiOWY1MGVlZDdhYWNmZGE2Mjk4Mzg4Yzg2MmI1ZjIyNDNlNzIyYWMxNWEyY2FkNzAwNWUyOWQ1NzIxMmE1YThiOSIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDEyY2EzNDVjY2Y3NWJmOWMwODA2YmNlNjk1YmU4ZGU4MDhiNzk5ODQyNTFhN2E1NGQyMDJjZjYxMDFkZDQ1MSIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiNGNjZjc5MzkxNTY5MjhjYzk4Y2ViZDIwYjJlYTA2OWM0ZWNlZGJlZWE5Nzk2MjBiN2IzNWUzYTY5Mjg3YWE4MyIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiM2I4MWNkZTZlMzVlZWI1MjRlMzM5ZDFlMjM1MzE5Y2Y1NTI5YjFhNTBhMmE2MDUyZmUzNWI1ZDQ4NWRkOWUxMyJ9"
         },
         {
-          "resource": "repo://src/whatsapp.rs#L332-L348",
+          "resource": "repo://src/whatsapp.rs#L358-L374",
           "version": "repo-lines-v1:sha256:2716d1991bf1de21e1b70d5a1702f8e881af4787f4afd11837ef8767c63b07b3:eyJzZWxlY3RlZExpbmVDb3VudCI6MTcsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImE2OGU3Zjk2MjU1MTJjM2E1NzQ5NDIzZDA2MDc0ODRhZjQxODc4NThmZDhkNjhhMTcwMWQ1ZWI1ODA3MWU4ZTkiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImFkMjVmMDBjOGVlOTdlZDMzZGJiMTNiMzNiYmRjOWEyNjhjMzhkODZmMjlkYTQyYTg1YjBkN2U0NGE0YTYxNzciLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjFiY2ZkYmJhNTAwZmIyZWEyMTlhZTdiNDQ0NmUzZTU1NTA4YWViMWQ3ZjdjYmM4MGYxNTUyNjMzMzk5ZGY1MWMifQ"
         }
       ]
@@ -151,15 +151,15 @@
       "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#L1291-L1294",
+          "resource": "repo://src/whatsapp.rs#L1345-L1348",
           "version": "repo-lines-v1:sha256:af6676593e9e90c7ac6a4b2b64625d687ad77fa0265e7f46bb6b8dbef4446d86:eyJzZWxlY3RlZExpbmVDb3VudCI6NCwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiMzFiZTM3ZmI4YTA3YjI0YzY1ZmVjNGQ3MGI2NWNhNDAzNTU5ODQwOGE3NDhhYjg2YWY4YTY4YWJlMGJjMTNhYSIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDg5M2Y0MGUzNzgxZWI4OTZjODJkMDQ2YTA4MDY3YTExZTM1Y2RmMmYxZGZhZjI2ZmVhOTBiOGRiNjY3NjQxMyIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiYTAwZGIwNDBmMjkwOWJmOTc4ZmYwYTk1NmE1NWU4M2M2NTM0MThkZTZiYjY4ZjgwNDFkYTk3NmY4ZDNkMjI3MCIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiYjQ1MTJjNzhmN2UyZTJiODdhNDZhYThlZWUxYTg3YjIwZjZjMWE0Y2MwZmNkNDQ4ZDhmZjJmMWJlOGYxMGQ4YSJ9"
         },
         {
-          "resource": "repo://src/whatsapp.rs#L665-L674",
+          "resource": "repo://src/whatsapp.rs#L695-L704",
           "version": "repo-lines-v1:sha256:55006adc8131456b9811de9f46520c07874aab146e6d5deb0b359a138bc063e8:eyJzZWxlY3RlZExpbmVDb3VudCI6MTAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjljNjAwZjllYTA2YzVjZTAxYjEzODExOGM5YjdhZDVjNTUxYzBhMmNjZDkxZTJmNWQxMWI4OTNiMWU5YzdjN2IiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjU1MjJjNDJjZDE5YzZkMDNkODA4YzQ1MjcyMDU1MjgyNWI3ZWExYzViNDkxYjIxODUxMWNhYzc0MTVjZjlkOWIiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjlhNDQ2MGQwYzUyNjkwYzM3YTQyOTI4MTc3NjkzNmQ4ZDA3YTJhYjg3MDEyYTRkMDY0MWNiMTQ4ZjkzOThjOGUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjQ3YTNlYmViMDcyOTcwMjY0MTFmZWVhZWViOTNkODBhMDZjOGQzNmE0OWE0NzgyNjhiZDFjNjJkMWRkOTk2YTkifQ"
         },
         {
-          "resource": "repo://src/whatsapp.rs#L1038-L1045",
+          "resource": "repo://src/whatsapp.rs#L1092-L1099",
           "version": "repo-lines-v1:sha256:e6e70ed71f30e7f2d2201053fec0c50f7a478e2081980f40b73ee55fd11ccbfe:eyJzZWxlY3RlZExpbmVDb3VudCI6OCwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiN2ZmYzA3OTdhYmQ1NjAwYmU5MWY4MjMyNWZmZjhlMjllMDVmNDY3MGZiOTQ5YzdmODZkZTRlZmJhOTQ5ZmIyOSIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDEyY2EzNDVjY2Y3NWJmOWMwODA2YmNlNjk1YmU4ZGU4MDhiNzk5ODQyNTFhN2E1NGQyMDJjZjYxMDFkZDQ1MSIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiMTllNzYzNTVjZjExOTk0ODBjM2Y5OTgzNmIxNDg2Yzg0MWQ4OTFmYWFkOTA0YzYwOGUwOTMwMjJkMzc1N2FlZSIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiYzYxNmY0NTJmYTU5OWI0YjM5MWIwMmVjYjI1OTNjMzk3ODVhMDFjZDZiZTIwMmM0OTQxMjk2NGViYWQ3YzFmYyJ9"
         }
       ]
@@ -169,15 +169,15 @@
       "statement": "WaHandle holds the running Session in an Arc<Mutex<Option<Session>>> shared by all clones together with the session database path and the WaEvent sender, and client operations fail with 'WhatsApp session not running' while the slot is empty.",
       "evidence": [
         {
-          "resource": "repo://src/whatsapp.rs#L193-L213",
+          "resource": "repo://src/whatsapp.rs#L199-L219",
           "version": "repo-lines-v1:sha256:0f8cd1675d28ef481cfcb8fdf5554a3529190b4edb526cd8f974d32643f56909:eyJzZWxlY3RlZExpbmVDb3VudCI6MjEsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjdhYTg4OWY5NzI4NTVlZGJiMWEyNzExOGE1NDY1ZTc2MjhlM2NjZWYzMDg2OTU5YThjNGZlOTViMDJmNDM1ZWIiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjVkYjk4OGU0ZDZiYzFkOGViNDU4NmY1NWY3MDBlNGVjY2EzYjhkYzliZDM2YzJjMjgyNDQ2MDhkN2M1NWEwMjAiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjViODNiNGUwM2Y4ZmFkZmM1OWI5NGZjZjhmNGUyYTI2ZjNlNTdmMjI0NzczODU0NjVmNjAzNjBhODQwNDdiNTMifQ"
         },
         {
-          "resource": "repo://src/whatsapp.rs#L233-L252",
+          "resource": "repo://src/whatsapp.rs#L239-L258",
           "version": "repo-lines-v1:sha256:5de6b5901fa8a014bd2fffe970ba820db963f438b9ec7bf051f62c957198ecd2:eyJzZWxlY3RlZExpbmVDb3VudCI6MjAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjEwYjNlNTU4ZDczYzgzNWI1NWVkY2U4NWUxNTMzYjM1ODczM2U1OWU4ZGNmYTJmZTM3N2FmNDBjZTcyZjY4OTUiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjE5ZTc2MzU1Y2YxMTk5NDgwYzNmOTk4MzZiMTQ4NmM4NDFkODkxZmFhZDkwNGM2MDhlMDkzMDIyZDM3NTdhZWUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjQ4Njc5ODk5ZGJiYzc0NzgxOGMzNWI3ZTNiMjY2ZWYwNTQyMWJmNTJkYzBmNmU4OWI4N2VhZjcyZjAzMzQ0NGEifQ"
         },
         {
-          "resource": "repo://src/whatsapp.rs#L88-L89",
+          "resource": "repo://src/whatsapp.rs#L94-L95",
           "version": "repo-lines-v1:sha256:ee851fc74e7caf112f9b070991f9371e3bbc3d48aad2bfcd6b2c38abde32e4d4:eyJzZWxlY3RlZExpbmVDb3VudCI6MiwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiYjE5NGEwYTk4MmFhMjdkODcxNzRlZGY2MzkzNDUyYmE3NTNmYmU1NTBiNDc3Zjc3M2FlNTFhMmI4Y2YxNGU5NSIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNTk2ZTRkZmU0ZWMxZjgxODE2NWRkZmU0M2RjOTU5YWNhM2UwMDA3MWM2ZGJjMmEwOWUyNTM3YTE5YTZjNjA0MyIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiYjE2YTZmNGMyZGQ0ODZiMTg0MjZhZTM0ZjIyNzlhZWJiMzg5NjQ5MzcxMTRiZmZmNzU2Zjg0YjBjZGZkYzY3OCIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiNjE0YjMxNTYwYmE5YmZmZDJjODQ2OTVhZjE5MjM5MjBkYjBiODhhYjVjNDA1NmUyNmVlZDljNzVhMjNlZGIwZCJ9"
         }
       ]
@@ -187,11 +187,11 @@
       "statement": "WaHandle::relink takes the current session out of the slot, stops it (logout, wait up to 10 seconds for the run loop, abort the translator), resets the session database files and starts a new unpaired client whose QR codes arrive as WaEvent::Qr; on error the slot stays empty.",
       "evidence": [
         {
-          "resource": "repo://src/whatsapp.rs#L215-L231",
+          "resource": "repo://src/whatsapp.rs#L221-L237",
           "version": "repo-lines-v1:sha256:bc94058fad104fda8ef2d9889e6539ac65f19dd28226e1edec14d240905e4aa2:eyJzZWxlY3RlZExpbmVDb3VudCI6MTcsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjYwMzUyMWI1YmRmYWViNzk2MzY5ZTExMzEwODIwMjIwZTNhMTI1MGUwY2E5NzIyMGEzYTI1OWFkMjQxNTcwZGIiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6Ijg3NzllYzNlN2JjNmZkMzdlNDYxMTdhODY2ZDA2YWIzZTVlNGVlYmE0ODJjYmViYzkzZjhjZGRjZjYyOTM5ZTAiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImM4YzBjZDA2MzU4OWY4OWUzMTY0ZTQ3MjczYzg2ZDQzYjBjNzkwZTc2OThhMDFmNTU1NGQ1ZTUxODkzMmRiYjYifQ"
         },
         {
-          "resource": "repo://src/whatsapp.rs#L254-L270",
+          "resource": "repo://src/whatsapp.rs#L260-L276",
           "version": "repo-lines-v1:sha256:63e137b22805f72a6e35c74bf1f9296c30d5dd1c696101d7b7db2f3dedb3b664:eyJzZWxlY3RlZExpbmVDb3VudCI6MTcsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjY4YjU3MmE2ZGZjZGNjZDc1YmE0ZDBkZDZiYmEzNjUwY2UwNDFkZDkxYzRjNjE1YTBmMDJhNDllMjNlNTA2OWQiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImIyNTdmMTI1NGQ0NjJmZjgyYTFmYTFmZjYyNzFlNzIzNTM1NTFkMDZlNjMxMDQ2OWYxMGZmZWUxZTY2ZmVlMjMiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6Ijk5OTU0Y2ZjYmI3ZmVlMDA0ZTg0M2I4ZDdmZjAxNTA3ZTlmOGU5YTYzNWI1MjZlOWQ1ODRmNzY1MDA4YjRiZDYifQ"
         }
       ]
@@ -201,11 +201,11 @@
       "statement": "reset_session_files deletes session.db and its -wal/-shm side files; when the database cannot be deleted (still open on Windows) it empties every table except SQLite internals and __diesel_schema_migrations in one transaction with a 5 second busy timeout.",
       "evidence": [
         {
-          "resource": "repo://src/whatsapp.rs#L367-L420",
+          "resource": "repo://src/whatsapp.rs#L393-L446",
           "version": "repo-lines-v1:sha256:dae8e9fd58393c76159603a1143da95f40e9d77d62eaa8e9531737a4a58b8edb:eyJzZWxlY3RlZExpbmVDb3VudCI6NTQsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjgyNTI5M2Y3MjNjNGMzMjZmNDJlMDFmMzc1Nzk1ZTJjMjE4NzQxNGEyNjI5NDEzOTEwODc3Mjc3MzdlMWMzMDEiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjYwNjNjNGQxYWY4Y2Q4OWJiMGJkZDc2NTdmZmNiZjY4MDZhNjEwZDc5ZDZjOGQ0YzMwN2MyNDFkZjM5MDA1MjIiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjI1MjJlMWI3MWMwMDczNGUwMWUwYTJhN2ZjYjU1ODdjYWEwN2RkYzRlZjRjMTViYTEyNGFhN2M1ZWRlZTVjYzUifQ"
         },
         {
-          "resource": "repo://src/whatsapp.rs#L91-L105",
+          "resource": "repo://src/whatsapp.rs#L97-L111",
           "version": "repo-lines-v1:sha256:ee68380c7344a72f8bb87cef4651b5340e1832b4eaed61c614d88b8964dc7950:eyJzZWxlY3RlZExpbmVDb3VudCI6MTUsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjE0YmIxNWU3MmU0YTFmMzBmNDU2OThjMDA4MDc2ODZjYWVjZTEzNTI1Yzk3YmU2ZTc5ZWZkYTlmMDQyMGNkZmMiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjZkMDcxMDczNGRhYWVkNTFlMTIwNmE1ZDU0NzMxMGQwOTg2Nzg2MTAyYzA3YzM0NjRiYzEyMzY5ODJkZjFjY2MiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImFlMDAzODBjNDQ0YjdjNmE5YTgyZTAxMDJjY2IwNzI0MWVlYjhhNTU1NGE2MTkxOTFhNzk2MDJhNGFhMDc4MGQiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjIxOTJmNjU0MjE1OGExOGY0MDRlMjg1ZmI5NGM2N2VjYmFmMjg0NzUxOGM5ZjQ3Y2IzZDA3MjVmOWYwNTk0NTUifQ"
         }
       ]
@@ -215,18 +215,56 @@
       "statement": "Audio messages render as the '[messaggio vocale]' placeholder; WaHandle::download_audio downloads and decrypts the audio bytes into memory with an 'audio/ogg' MIME fallback, and AudioRef's Debug omits key material.",
       "evidence": [
         {
-          "resource": "repo://src/whatsapp.rs#L1091-L1103",
+          "resource": "repo://src/whatsapp.rs#L1145-L1157",
           "version": "repo-lines-v1:sha256:45a35da8df6fc27760a95dca63d9e04961dc44f29c71fcdb85fef2c62a9e848b:eyJzZWxlY3RlZExpbmVDb3VudCI6MTMsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjE3OGM5MzE3NmI2YjkyM2QwMDkzOGM3N2FkMGMzNmEzOTg2ZjJlOTJjYzY2Mjc3ODcyZjFlN2QxNGZiNDk5YjciLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjVhOTA2ZmY3MWMzYTY2N2Y1MTE0ODk0MWJiNDA3MzgwNjFkMTBhNTA2NzI2OGM4ZTBhNmRjYjJjMmJkZjU0NmQiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjAyYzFjZTZmZmI5ZGEyOTBlMjgyNWRlNjVmNTllYWExNjliZDJiMDBlNDBlZjA0NjAzYWRkODFjZjU5MzZhNjIifQ"
         },
         {
-          "resource": "repo://src/whatsapp.rs#L317-L330",
+          "resource": "repo://src/whatsapp.rs#L323-L336",
           "version": "repo-lines-v1:sha256:948cec998055e53be3e45a29232f60f6f315896f8a9f3b0ac6617ef65852536c:eyJzZWxlY3RlZExpbmVDb3VudCI6MTQsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImE4MjAwYTdhNDMzMTFkNzkxN2NkYWQ4NjIyMjhjYTg3ZDIxYjRhZmMyMjJmMGZjYWM3MjJmODU3YWEwNzAwNWYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImFkNzI0MjA4NTQ4MTRiMWU2YzUwNzc4ZDU3YjMxNTIzMWYzYTBmMDRkMzFkOGVjMDNiMjc5ZWU2YTdmOGE1OWIiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjMwZGZlODgwMmNkNjM3Y2NmMjZjNWIwYjZhOGJhYmNhOWRhYzI5NTNlOTRmYjA4YzFjNzA1NGVkOWUyNmYwMmUifQ"
         }
       ]
+    },
+    {
+      "id": "claim_cc2dcc9a424b47a38f790df4db8360ff",
+      "statement": "After every Connected and SelfPushNameUpdated event the Translator schedules a task that waits PRESENCE_SETTLE_SECONDS (5 s) and, if the client is still connected, sets the presence to unavailable so the phone keeps receiving push notifications.",
+      "evidence": [
+        {
+          "resource": "repo://src/whatsapp.rs#L674-L678",
+          "version": "repo-lines-v1:sha256:c808a4b2f40500fb739cedcd414ec736ee2d5195bc46d5a5ee91bafa896bc005:eyJzZWxlY3RlZExpbmVDb3VudCI6NSwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiOGFhNWE2ZjgzMWMwNGVhMTQwYzE0N2MzMGVlOTMxNmIxMGU2ZDVkNmVmZjM2MTNhZWI1ZDYwYjY3MjlmNDM0MSIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiMGVhMDI1MTA1NjU0ZWJiNjRkNzMzMzA5MzMzMDI1OWVmNDViZjUzNjExYzY4NGIyOGQwMTI4MjVhODE2NWZhMyIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiNjk2OWZhNmNiMzIyNmMwN2U0NWI0MTNkNWU2NWNiNjc4ZTVmMGRiZDM2NTkwY2FkODBiMWFjYTE3NmZkYWI1MSIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiMDI5ODI1NGMwYmYzYmIxOGViZTBkOTg2NWMzY2RkMGI5ZTQwYjNiMWIwMjgwMWQyOWMxYjlkYmQzNmNjNDA0YSJ9"
+        },
+        {
+          "resource": "repo://src/whatsapp.rs#L719-L741",
+          "version": "repo-lines-v1:sha256:325ad7da5f94c5e2c3a2ce9b22ca961b2e5247222fb5434ab20779ea988122cb:eyJzZWxlY3RlZExpbmVDb3VudCI6MjMsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImIwMWU1OGYwZTM3MzU5ODEzMWEwMjYyNTA4NjhkYmNlNTIyYTc1NjM1OTgwMTljYTk0OWI4ZGI5NzJlOTU0OTEiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6Ijk5OWJkZWZlZWFjYTYzNDBlMDI5MGRjODZlODg5Yzg5ZDVlMTg5YWE1YmE0NGI1ODViM2I3MDczNGY0YTYyZDgiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImM2ZTYxOTdhNjY3YmI1ZjgyYzI4ZDdmYWM2OGIzYWE1YzU1YTk5NmQ2NGRkYmYyOTI3ZWQ3YjAyNzk5MDI0NWYifQ"
+        },
+        {
+          "resource": "repo://src/whatsapp.rs#L81-L83",
+          "version": "repo-lines-v1:sha256:cacb364bf98c7683ac12b818dd4c3e24bc07ef61fa8079b7e2d98f1f83b0b3df:eyJzZWxlY3RlZExpbmVDb3VudCI6MywiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiZDk1OTBkNDgzN2UyNTAzMDFmMDMwNDIxYzBlNzI1ZmQ2NzU5YzBmNzBjNTQ1YmZiZDFmYTA5ODY2NzJkMDNlMyIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiZThlNWFhNGYzYjM4ODg5OWI0ZDM4ZDFiNDFiOGQyNWQwMjA4NTc5OTJkMzE4OTg2MmYyZGEyZGJiZGU2NTllYyIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiMjBiNDRiYzZkYTM1MTRhMDNjMTIwMmE4N2IxODJkOWU4YzE5ZmEyZTBmZjUxMzYyOWNmNTFiMWNhYjY0ZWM0OCIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiOGM4Mzk4NjBjYWVlYjAyZjA4YzBlMTc0MjBhM2M5MWVhNGFiZjQ1ODk4ZTA0Yjc5MjNmNGNiYWFlNGI2ODcxMiJ9"
+        }
+      ]
+    },
+    {
+      "id": "claim_bd9cdce70e4440d88bb18d813e49b46a",
+      "statement": "A PushNameEmpty presence error is ignored because the later SelfPushNameUpdated event reschedules the presence; any other presence failure is reported as 'failed to set unavailable presence: <error>'.",
+      "evidence": [
+        {
+          "resource": "repo://src/whatsapp.rs#L733-L739",
+          "version": "repo-lines-v1:sha256:6ef93fc5e4e58401cff6dab79914dfee0a8b2c49d32c128bd7009f7aa4b05ebe:eyJzZWxlY3RlZExpbmVDb3VudCI6NywiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiN2NlOTk4OGE2MWRmOTRlMmUwZjdmNGZkM2RlMzczODY4YzJmY2E1NTlmZWM5YjYyZTNmZjM5MTY0ZDQwODQ2MCIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNTUyMmM0MmNkMTljNmQwM2Q4MDhjNDUyNzIwNTUyODI1YjdlYTFjNWI0OTFiMjE4NTExY2FjNzQxNWNmOWQ5YiIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiMWE1NTJhNjBkYjhmYmU3YzY5OWY3MGU0ZjEwZTMzZTViZTdlNjY5Y2UwNDg0YzBjZjE4MGZjMTY4NmRlNzliNSIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiYTJjYzBkNDRkZGUzOTEwMThhNDU5YzA2YjA5MGVlZWM4ZGY1MDQxODgyOTczMjAyZDBhNzY1ZmM5MzU2NGNhNiJ9"
+        }
+      ]
+    },
+    {
+      "id": "claim_ba38a9eaf31c4088a4b64f64d30cf704",
+      "statement": "WaHandle::profile_picture_url parses the chat JID and requests the preview profile picture through the contacts API, returning the CDN URL, None when the chat has no or a hidden picture, or an error for an invalid JID, no running session or a network failure.",
+      "evidence": [
+        {
+          "resource": "repo://src/whatsapp.rs#L338-L357",
+          "version": "repo-lines-v1:sha256:c9f679e1e9e8e714472e018fe5f8ae646de6830a0bfd53255181dd1f2c940f93:eyJzZWxlY3RlZExpbmVDb3VudCI6MjAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjVlNzQ3N2M0N2Q5ODRkZTBiNGFhY2E4YWIzYjg3MzRmNTlhZTBlYWZlYWJhNjUzMGVkYjE2OTljYWMxYTgzZjYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjAxYmE0NzE5YzgwYjZmZTkxMWIwOTFhN2MwNTEyNGI2NGVlZWNlOTY0ZTA5YzA1OGVmOGY5ODA1ZGFjYTU0NmIiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6Ijk1MjkzMjAzYjU5N2QyYmJhZGVkZmY2ZGIxNzhmNzc3ZmRmMDgyZDA4OGY5M2M0NmFkMWUzZjk1ZGJiODkyZWMiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjE3MDU1YWQ2NjJhNzVmYWM3MTk4MTc0ZTJiODQ4OTRiMTI5YzQwMDE1OTYwZmRlMDYwMmM3M2ExNDU3NWQ2YzkifQ"
+        }
+      ]
     }
   ],
   "verification": {
     "by": "openwiki/0.7.2",
-    "at": "2026-10-24T16:39:50.206Z"
+    "at": "2026-10-25T12:54:53.684Z"
   }
 }

+ 36 - 26
openwiki/.claims/testing/unit-tests.json

@@ -1,6 +1,6 @@
 {
   "schemaVersion": 1,
-  "pageVersion": "sha256:50c64fbc1c28a488dc62c88f83e98a06ce449b71661690714c90854f832928e9",
+  "pageVersion": "sha256:87e88f2284955c6a45419d031922bce94b2d30c29040e151fd42811374151dc0",
   "claims": [
     {
       "id": "claim_2e232262c6d940c7ba52a945b96928f6",
@@ -11,11 +11,11 @@
           "version": "repo-lines-v1:sha256:03c5b4b27fb001def5489d5e0264e20f2538b8214c3df8ceda3354aaee53c2c8:eyJzZWxlY3RlZExpbmVDb3VudCI6MiwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiMjVjZjVmYzIzZjJhNmQyM2MzZWE1MWU3MmY1ZTg5NmM3NTU1ZmJhYTA1Y2Q1YjFlYTE4MmJmOTYxOGE4ZTgxYiIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiMWUwYWYzZmQ0YTRjM2ExZDQ3YzdmNDcwZWQwZDg2ZDkwZDlkZTIxMGE1ZWIxNDU1ZmJkOTQ0ODJhNDc2NWI3MiIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiZWRjODMxMjQ5NjYxNGM2YzllMTE0NTc0YTc5ZDkxODU3MDM0YmJlM2IzMDk5MmYyNzYwZjcxNjY0ZjAwNWM2OSIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiOTM0OTA4YTE2NjZlZThkZWFiMmIyYTc2NjdjODQ0OGZiYjRkNDZlMzJhYjZjNTY4YmI3MjMyMGU1ZWJmMDlmMyJ9"
         },
         {
-          "resource": "repo://src/app.rs#L1941-L1942",
+          "resource": "repo://src/app.rs#L2041-L2042",
           "version": "repo-lines-v1:sha256:03c5b4b27fb001def5489d5e0264e20f2538b8214c3df8ceda3354aaee53c2c8:eyJzZWxlY3RlZExpbmVDb3VudCI6MiwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiMjVjZjVmYzIzZjJhNmQyM2MzZWE1MWU3MmY1ZTg5NmM3NTU1ZmJhYTA1Y2Q1YjFlYTE4MmJmOTYxOGE4ZTgxYiIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiMWUwYWYzZmQ0YTRjM2ExZDQ3YzdmNDcwZWQwZDg2ZDkwZDlkZTIxMGE1ZWIxNDU1ZmJkOTQ0ODJhNDc2NWI3MiIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiZjY5NGY2NjJmYWIyMzFlOGQ4ZGIwOTRiNTU3OTgxNWYwOTlkYTQwZGU3ZThiOTY1YWUwNDU2YWRjMDQ5YjM4NiIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiOGJlOTk2NWNlNGEzMmY5N2Y5ZjMzYzU1MzhjZWY5ZDE0Njc4YjE0NGQ4ODg1ZDY2Mjk4YTZkZTJjODg3NTZlMSJ9"
         },
         {
-          "resource": "repo://src/autoreply.rs#L505-L506",
+          "resource": "repo://src/autoreply.rs#L529-L530",
           "version": "repo-lines-v1:sha256:03c5b4b27fb001def5489d5e0264e20f2538b8214c3df8ceda3354aaee53c2c8:eyJzZWxlY3RlZExpbmVDb3VudCI6MiwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiMjVjZjVmYzIzZjJhNmQyM2MzZWE1MWU3MmY1ZTg5NmM3NTU1ZmJhYTA1Y2Q1YjFlYTE4MmJmOTYxOGE4ZTgxYiIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiMWUwYWYzZmQ0YTRjM2ExZDQ3YzdmNDcwZWQwZDg2ZDkwZDlkZTIxMGE1ZWIxNDU1ZmJkOTQ0ODJhNDc2NWI3MiIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiOTMyZjlmMDdlZDQ0ZTc0ZjliN2Y1ZGEzNTY1NjIxZjExNGRiNzdlZGY1NDc2NzZjNjZiNzRjMWMwNzQzN2MyMSIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiN2M0NzcyZjY4MTNiYmFmYmNhYjU4MWZlN2JiZmIwOTI4NDRlMGZhOWU3MjQwNmMxNzNjNmYyNTMxMjgyMTZmMSJ9"
         },
         {
@@ -31,7 +31,7 @@
           "version": "repo-lines-v1:sha256:03c5b4b27fb001def5489d5e0264e20f2538b8214c3df8ceda3354aaee53c2c8:eyJzZWxlY3RlZExpbmVDb3VudCI6MiwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiMjVjZjVmYzIzZjJhNmQyM2MzZWE1MWU3MmY1ZTg5NmM3NTU1ZmJhYTA1Y2Q1YjFlYTE4MmJmOTYxOGE4ZTgxYiIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiMWUwYWYzZmQ0YTRjM2ExZDQ3YzdmNDcwZWQwZDg2ZDkwZDlkZTIxMGE1ZWIxNDU1ZmJkOTQ0ODJhNDc2NWI3MiIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiMzViMjE2ODczZmZmNmEwNTc3MDU5NWM1OWNhZDNlNmE5NzM1MmJiNDE3ZDYwYzc2MmVhNDM4MWYxOTkwMjg3OSIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiNDVlYjIxMDg3NDU0NjZhOTVjZjg5YjVhZjdmMDZhMDA2NGU0OGRiMDVkMmMyOWU5Y2YzZDZkMGQxNmY3MjY5ZiJ9"
         },
         {
-          "resource": "repo://src/whatsapp.rs#L1215-L1216",
+          "resource": "repo://src/whatsapp.rs#L1269-L1270",
           "version": "repo-lines-v1:sha256:03c5b4b27fb001def5489d5e0264e20f2538b8214c3df8ceda3354aaee53c2c8:eyJzZWxlY3RlZExpbmVDb3VudCI6MiwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiMjVjZjVmYzIzZjJhNmQyM2MzZWE1MWU3MmY1ZTg5NmM3NTU1ZmJhYTA1Y2Q1YjFlYTE4MmJmOTYxOGE4ZTgxYiIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiMWUwYWYzZmQ0YTRjM2ExZDQ3YzdmNDcwZWQwZDg2ZDkwZDlkZTIxMGE1ZWIxNDU1ZmJkOTQ0ODJhNDc2NWI3MiIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiZDZkMTNkYTUzZGRmNTRlMDlhOWZiMzc3ODc0MTRhOTYxNDZkNDcxYzVlYmM2Y2UyNmNmOWEzODRmMTg3NzhmMSIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiMWU1MWM0MjBiN2E3NDU0MmZlNGYwMjY4Yzk0YTYwZjRmYjAwZTdhODI0NmFhNTlkNzQ0NTkzYmY3OGUyN2M5MCJ9"
         }
       ]
@@ -48,11 +48,11 @@
     },
     {
       "id": "claim_0bfd5fc27a434254a016373d3bb60e52",
-      "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.",
+      "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, today_label formatting and build_turns behaviour including the date line, stored image descriptions and group sender prefixes.",
       "evidence": [
         {
-          "resource": "repo://src/autoreply.rs#L505-L1093",
-          "version": "repo-lines-v1:sha256:73d6683e98d1a479be5b3c5c82533ad41e429042edcbbc8f6c7d722a1eca2628:eyJzZWxlY3RlZExpbmVDb3VudCI6NTg5LCJmaXJzdFNlbGVjdGVkTGluZUhhc2giOiIyNWNmNWZjMjNmMmE2ZDIzYzNlYTUxZTcyZjVlODk2Yzc1NTVmYmFhMDVjZDViMWVhMTgyYmY5NjE4YThlODFiIiwibGFzdFNlbGVjdGVkTGluZUhhc2giOiI0MTJjYTM0NWNjZjc1YmY5YzA4MDZiY2U2OTViZThkZTgwOGI3OTk4NDI1MWE3YTU0ZDIwMmNmNjEwMWRkNDUxIiwicHJlY2VkaW5nQ29udGV4dExpbmVDb3VudCI6MywicHJlY2VkaW5nQ29udGV4dEhhc2giOiI5MzJmOWYwN2VkNDRlNzRmOWI3ZjVkYTM1NjU2MjFmMTE0ZGI3N2VkZjU0NzY3NmM2NmI3NGMxYzA3NDM3YzIxIiwiZm9sbG93aW5nQ29udGV4dExpbmVDb3VudCI6MCwiZm9sbG93aW5nQ29udGV4dEhhc2giOiJlM2IwYzQ0Mjk4ZmMxYzE0OWFmYmY0Yzg5OTZmYjkyNDI3YWU0MWU0NjQ5YjkzNGNhNDk1OTkxYjc4NTJiODU1In0"
+          "resource": "repo://src/autoreply.rs#L529-L1144",
+          "version": "repo-lines-v1:sha256:58fb3ec8d45ba6e12bdc5b84fd277be293d2c16647ba0f8638d3a5ee805c1e79:eyJzZWxlY3RlZExpbmVDb3VudCI6NjE2LCJmaXJzdFNlbGVjdGVkTGluZUhhc2giOiIyNWNmNWZjMjNmMmE2ZDIzYzNlYTUxZTcyZjVlODk2Yzc1NTVmYmFhMDVjZDViMWVhMTgyYmY5NjE4YThlODFiIiwibGFzdFNlbGVjdGVkTGluZUhhc2giOiI0MTJjYTM0NWNjZjc1YmY5YzA4MDZiY2U2OTViZThkZTgwOGI3OTk4NDI1MWE3YTU0ZDIwMmNmNjEwMWRkNDUxIiwicHJlY2VkaW5nQ29udGV4dExpbmVDb3VudCI6MywicHJlY2VkaW5nQ29udGV4dEhhc2giOiI5MzJmOWYwN2VkNDRlNzRmOWI3ZjVkYTM1NjU2MjFmMTE0ZGI3N2VkZjU0NzY3NmM2NmI3NGMxYzA3NDM3YzIxIiwiZm9sbG93aW5nQ29udGV4dExpbmVDb3VudCI6MCwiZm9sbG93aW5nQ29udGV4dEhhc2giOiJlM2IwYzQ0Mjk4ZmMxYzE0OWFmYmY0Yzg5OTZmYjkyNDI3YWU0MWU0NjQ5YjkzNGNhNDk1OTkxYjc4NTJiODU1In0"
         }
       ]
     },
@@ -92,35 +92,35 @@
     },
     {
       "id": "claim_1814788a665c474fbe9623328521c079",
-      "statement": "The suite contains 235 test functions (234 #[test] plus one ignored live test): autoreply 55, config 41, ai 27, whatsapp 24 (one ignored live), app 23, history 19, transcribe 17, ui 15, web 11, main 2, config_watch 1.",
+      "statement": "The suite contains 238 test functions (237 #[test] plus one ignored live test): autoreply 58, config 41, ai 27, whatsapp 24 (one ignored live), app 23, history 19, transcribe 17, ui 15, web 11, main 2, config_watch 1.",
       "evidence": [
         {
-          "resource": "repo://src/ai.rs#L589-L914",
-          "version": "repo-lines-v1:sha256:563cc7b7eeb9a5af67ee42215a0be871ea267497ef548419e7715973caa737c6:eyJzZWxlY3RlZExpbmVDb3VudCI6MzI2LCJmaXJzdFNlbGVjdGVkTGluZUhhc2giOiIwMWJhNDcxOWM4MGI2ZmU5MTFiMDkxYTdjMDUxMjRiNjRlZWVjZTk2NGUwOWMwNThlZjhmOTgwNWRhY2E1NDZiIiwibGFzdFNlbGVjdGVkTGluZUhhc2giOiI0MTJjYTM0NWNjZjc1YmY5YzA4MDZiY2U2OTViZThkZTgwOGI3OTk4NDI1MWE3YTU0ZDIwMmNmNjEwMWRkNDUxIiwicHJlY2VkaW5nQ29udGV4dExpbmVDb3VudCI6MywicHJlY2VkaW5nQ29udGV4dEhhc2giOiJjNGI2ODI1NzRkYjY3YzNjOGJmNjVmZjk2NmZlMzJiZWE0OGRmYzA0MTNjODMwMzcwNjIxMDEwMzkyYTYyMGI5IiwiZm9sbG93aW5nQ29udGV4dExpbmVDb3VudCI6MCwiZm9sbG93aW5nQ29udGV4dEhhc2giOiJlM2IwYzQ0Mjk4ZmMxYzE0OWFmYmY0Yzg5OTZmYjkyNDI3YWU0MWU0NjQ5YjkzNGNhNDk1OTkxYjc4NTJiODU1In0"
+          "resource": "repo://src/ai.rs#L581-L914",
+          "version": "repo-lines-v1:sha256:4105d564dc0aea7b7828f95965694b788e21c9ea78fdb3252a1d5e25335bbe21:eyJzZWxlY3RlZExpbmVDb3VudCI6MzM0LCJmaXJzdFNlbGVjdGVkTGluZUhhc2giOiIyNWNmNWZjMjNmMmE2ZDIzYzNlYTUxZTcyZjVlODk2Yzc1NTVmYmFhMDVjZDViMWVhMTgyYmY5NjE4YThlODFiIiwibGFzdFNlbGVjdGVkTGluZUhhc2giOiI0MTJjYTM0NWNjZjc1YmY5YzA4MDZiY2U2OTViZThkZTgwOGI3OTk4NDI1MWE3YTU0ZDIwMmNmNjEwMWRkNDUxIiwicHJlY2VkaW5nQ29udGV4dExpbmVDb3VudCI6MywicHJlY2VkaW5nQ29udGV4dEhhc2giOiJlZGM4MzEyNDk2NjE0YzZjOWUxMTQ1NzRhNzlkOTE4NTcwMzRiYmUzYjMwOTkyZjI3NjBmNzE2NjRmMDA1YzY5IiwiZm9sbG93aW5nQ29udGV4dExpbmVDb3VudCI6MCwiZm9sbG93aW5nQ29udGV4dEhhc2giOiJlM2IwYzQ0Mjk4ZmMxYzE0OWFmYmY0Yzg5OTZmYjkyNDI3YWU0MWU0NjQ5YjkzNGNhNDk1OTkxYjc4NTJiODU1In0"
         },
         {
-          "resource": "repo://src/app.rs#L1941-L2171",
-          "version": "repo-lines-v1:sha256:4246f63e0a5bf224e095e0ed67f18427d444f16f51597b335864cac6b00c053c:eyJzZWxlY3RlZExpbmVDb3VudCI6MjMxLCJmaXJzdFNlbGVjdGVkTGluZUhhc2giOiIyNWNmNWZjMjNmMmE2ZDIzYzNlYTUxZTcyZjVlODk2Yzc1NTVmYmFhMDVjZDViMWVhMTgyYmY5NjE4YThlODFiIiwibGFzdFNlbGVjdGVkTGluZUhhc2giOiI0MTJjYTM0NWNjZjc1YmY5YzA4MDZiY2U2OTViZThkZTgwOGI3OTk4NDI1MWE3YTU0ZDIwMmNmNjEwMWRkNDUxIiwicHJlY2VkaW5nQ29udGV4dExpbmVDb3VudCI6MywicHJlY2VkaW5nQ29udGV4dEhhc2giOiJmNjk0ZjY2MmZhYjIzMWU4ZDhkYjA5NGI1NTc5ODE1ZjA5OWRhNDBkZTdlOGI5NjVhZTA0NTZhZGMwNDliMzg2IiwiZm9sbG93aW5nQ29udGV4dExpbmVDb3VudCI6MCwiZm9sbG93aW5nQ29udGV4dEhhc2giOiJlM2IwYzQ0Mjk4ZmMxYzE0OWFmYmY0Yzg5OTZmYjkyNDI3YWU0MWU0NjQ5YjkzNGNhNDk1OTkxYjc4NTJiODU1In0"
+          "resource": "repo://src/app.rs#L2041-L2271",
+          "version": "repo-lines-v1:sha256:122ecbc13fa43844a4bcdc3fb8bc3da9522b9b522894cc3724746416d336cea5:eyJzZWxlY3RlZExpbmVDb3VudCI6MjMxLCJmaXJzdFNlbGVjdGVkTGluZUhhc2giOiIyNWNmNWZjMjNmMmE2ZDIzYzNlYTUxZTcyZjVlODk2Yzc1NTVmYmFhMDVjZDViMWVhMTgyYmY5NjE4YThlODFiIiwibGFzdFNlbGVjdGVkTGluZUhhc2giOiI0MTJjYTM0NWNjZjc1YmY5YzA4MDZiY2U2OTViZThkZTgwOGI3OTk4NDI1MWE3YTU0ZDIwMmNmNjEwMWRkNDUxIiwicHJlY2VkaW5nQ29udGV4dExpbmVDb3VudCI6MywicHJlY2VkaW5nQ29udGV4dEhhc2giOiJmNjk0ZjY2MmZhYjIzMWU4ZDhkYjA5NGI1NTc5ODE1ZjA5OWRhNDBkZTdlOGI5NjVhZTA0NTZhZGMwNDliMzg2IiwiZm9sbG93aW5nQ29udGV4dExpbmVDb3VudCI6MCwiZm9sbG93aW5nQ29udGV4dEhhc2giOiJlM2IwYzQ0Mjk4ZmMxYzE0OWFmYmY0Yzg5OTZmYjkyNDI3YWU0MWU0NjQ5YjkzNGNhNDk1OTkxYjc4NTJiODU1In0"
         },
         {
-          "resource": "repo://src/autoreply.rs#L551-L1093",
-          "version": "repo-lines-v1:sha256:c45662796f439d33fcddbeb69c7dff5d616cdf1c336cabc0814728361d089191:eyJzZWxlY3RlZExpbmVDb3VudCI6NTQzLCJmaXJzdFNlbGVjdGVkTGluZUhhc2giOiJjNTBjNWU1NDBmMDhkZWZhYTY4ZTQwODlhMTRlNDAzNWIyM2MwODUwM2UwNDcyYmU4ZmUzOTkyNmEyMWRhOThkIiwibGFzdFNlbGVjdGVkTGluZUhhc2giOiI0MTJjYTM0NWNjZjc1YmY5YzA4MDZiY2U2OTViZThkZTgwOGI3OTk4NDI1MWE3YTU0ZDIwMmNmNjEwMWRkNDUxIiwicHJlY2VkaW5nQ29udGV4dExpbmVDb3VudCI6MywicHJlY2VkaW5nQ29udGV4dEhhc2giOiI2M2I4NDA4NjIxZjgzYzM1N2YwMWRjM2FhMWU0NGJkZDcxYzYyMDE0NWI3Yjg5NDMxOTQ1MmNhZTczZjhmYmQwIiwiZm9sbG93aW5nQ29udGV4dExpbmVDb3VudCI6MCwiZm9sbG93aW5nQ29udGV4dEhhc2giOiJlM2IwYzQ0Mjk4ZmMxYzE0OWFmYmY0Yzg5OTZmYjkyNDI3YWU0MWU0NjQ5YjkzNGNhNDk1OTkxYjc4NTJiODU1In0"
+          "resource": "repo://src/autoreply.rs#L529-L1144",
+          "version": "repo-lines-v1:sha256:58fb3ec8d45ba6e12bdc5b84fd277be293d2c16647ba0f8638d3a5ee805c1e79:eyJzZWxlY3RlZExpbmVDb3VudCI6NjE2LCJmaXJzdFNlbGVjdGVkTGluZUhhc2giOiIyNWNmNWZjMjNmMmE2ZDIzYzNlYTUxZTcyZjVlODk2Yzc1NTVmYmFhMDVjZDViMWVhMTgyYmY5NjE4YThlODFiIiwibGFzdFNlbGVjdGVkTGluZUhhc2giOiI0MTJjYTM0NWNjZjc1YmY5YzA4MDZiY2U2OTViZThkZTgwOGI3OTk4NDI1MWE3YTU0ZDIwMmNmNjEwMWRkNDUxIiwicHJlY2VkaW5nQ29udGV4dExpbmVDb3VudCI6MywicHJlY2VkaW5nQ29udGV4dEhhc2giOiI5MzJmOWYwN2VkNDRlNzRmOWI3ZjVkYTM1NjU2MjFmMTE0ZGI3N2VkZjU0NzY3NmM2NmI3NGMxYzA3NDM3YzIxIiwiZm9sbG93aW5nQ29udGV4dExpbmVDb3VudCI6MCwiZm9sbG93aW5nQ29udGV4dEhhc2giOiJlM2IwYzQ0Mjk4ZmMxYzE0OWFmYmY0Yzg5OTZmYjkyNDI3YWU0MWU0NjQ5YjkzNGNhNDk1OTkxYjc4NTJiODU1In0"
         },
         {
-          "resource": "repo://src/config.rs#L410-L804",
-          "version": "repo-lines-v1:sha256:5cd3b7a51ee76024e35b0337d198566fb9c818591ca051940cf26fbc8226e22b:eyJzZWxlY3RlZExpbmVDb3VudCI6Mzk1LCJmaXJzdFNlbGVjdGVkTGluZUhhc2giOiIyNWNmNWZjMjNmMmE2ZDIzYzNlYTUxZTcyZjVlODk2Yzc1NTVmYmFhMDVjZDViMWVhMTgyYmY5NjE4YThlODFiIiwibGFzdFNlbGVjdGVkTGluZUhhc2giOiJkYTFhMzRjY2E4NGI3ZWZjMzBhMDRmOGVkMGFlZmE1MmIzZGNhNzEzNWE1ZDQzNDM3OGE4YzI4MDQ1NTllNGZiIiwicHJlY2VkaW5nQ29udGV4dExpbmVDb3VudCI6MywicHJlY2VkaW5nQ29udGV4dEhhc2giOiIxOWU3NjM1NWNmMTE5OTQ4MGMzZjk5ODM2YjE0ODZjODQxZDg5MWZhYWQ5MDRjNjA4ZTA5MzAyMmQzNzU3YWVlIiwiZm9sbG93aW5nQ29udGV4dExpbmVDb3VudCI6MywiZm9sbG93aW5nQ29udGV4dEhhc2giOiJkODJkZGZhZmJiMTU0NzFhNDQ3NzU4YjJhNTRjMDc5NzQ4Njg5YmEyOWM0ZmMwNjg4ZGMzYzg3NmJkOGZlMTcwIn0"
+          "resource": "repo://src/config.rs#L410-L808",
+          "version": "repo-lines-v1:sha256:2f8174cee8beb74c1d70e9186f4e116435cdd8be80e7db30dad1130f3657f9e9:eyJzZWxlY3RlZExpbmVDb3VudCI6Mzk5LCJmaXJzdFNlbGVjdGVkTGluZUhhc2giOiIyNWNmNWZjMjNmMmE2ZDIzYzNlYTUxZTcyZjVlODk2Yzc1NTVmYmFhMDVjZDViMWVhMTgyYmY5NjE4YThlODFiIiwibGFzdFNlbGVjdGVkTGluZUhhc2giOiI0MTJjYTM0NWNjZjc1YmY5YzA4MDZiY2U2OTViZThkZTgwOGI3OTk4NDI1MWE3YTU0ZDIwMmNmNjEwMWRkNDUxIiwicHJlY2VkaW5nQ29udGV4dExpbmVDb3VudCI6MywicHJlY2VkaW5nQ29udGV4dEhhc2giOiIxOWU3NjM1NWNmMTE5OTQ4MGMzZjk5ODM2YjE0ODZjODQxZDg5MWZhYWQ5MDRjNjA4ZTA5MzAyMmQzNzU3YWVlIiwiZm9sbG93aW5nQ29udGV4dExpbmVDb3VudCI6MCwiZm9sbG93aW5nQ29udGV4dEhhc2giOiJlM2IwYzQ0Mjk4ZmMxYzE0OWFmYmY0Yzg5OTZmYjkyNDI3YWU0MWU0NjQ5YjkzNGNhNDk1OTkxYjc4NTJiODU1In0"
         },
         {
           "resource": "repo://src/config_watch.rs#L54-L74",
           "version": "repo-lines-v1:sha256:e1c5a3b6eb06c5bf32c7c814c75a97ed0aedf8c45c474dcd004bb5f4c4023d58:eyJzZWxlY3RlZExpbmVDb3VudCI6MjEsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjI1Y2Y1ZmMyM2YyYTZkMjNjM2VhNTFlNzJmNWU4OTZjNzU1NWZiYWEwNWNkNWIxZWExODJiZjk2MThhOGU4MWIiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImQ4ODU5MjEwZWJjYjEyY2Y2OGM2MTYyMjJmYzU2MGMxZjY4ZTJmYTMxZjYzMDQ5M2RmODA5NTg4ODdkNTdhZjkiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjowLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImUzYjBjNDQyOThmYzFjMTQ5YWZiZjRjODk5NmZiOTI0MjdhZTQxZTQ2NDliOTM0Y2E0OTU5OTFiNzg1MmI4NTUifQ"
         },
         {
-          "resource": "repo://src/history.rs#L563-L872",
-          "version": "repo-lines-v1:sha256:5a7faac412b418e8d4aca9502c8b2c44ebacbab61bb9be1912c746fbcfa3ea92:eyJzZWxlY3RlZExpbmVDb3VudCI6MzEwLCJmaXJzdFNlbGVjdGVkTGluZUhhc2giOiI0ZDJkMDQ1MDNjYWJjZWRhODhiZTgwM2NmMjYxNGExZTI1MWJjZjQwZDE3YTc5ZGRlZmI3MWExMjlmMDNkMjNmIiwibGFzdFNlbGVjdGVkTGluZUhhc2giOiIwMWJhNDcxOWM4MGI2ZmU5MTFiMDkxYTdjMDUxMjRiNjRlZWVjZTk2NGUwOWMwNThlZjhmOTgwNWRhY2E1NDZiIiwicHJlY2VkaW5nQ29udGV4dExpbmVDb3VudCI6MywicHJlY2VkaW5nQ29udGV4dEhhc2giOiIzNmU0YjEyNGNhM2YzZjM2MGQxOWQyMTgwMjQxZDk2NzAyMjE4OWUzOTljZmY3ZTM4M2E0YTVmNmY2MmJmZDliIiwiZm9sbG93aW5nQ29udGV4dExpbmVDb3VudCI6MywiZm9sbG93aW5nQ29udGV4dEhhc2giOiI5MmY2MjMxNmZlZTgwNWMwNjg2MzgxZWUyODkwNzEzNWM2ZjE5NTc0ZTlmNzY0YWE3NWMzNDNhOTMxZDJlOTRmIn0"
+          "resource": "repo://src/history.rs#L557-L879",
+          "version": "repo-lines-v1:sha256:21f6aa26aea839f419e69f756a7deee82a90938951e439640afa2d2015e3218e:eyJzZWxlY3RlZExpbmVDb3VudCI6MzIzLCJmaXJzdFNlbGVjdGVkTGluZUhhc2giOiIyNWNmNWZjMjNmMmE2ZDIzYzNlYTUxZTcyZjVlODk2Yzc1NTVmYmFhMDVjZDViMWVhMTgyYmY5NjE4YThlODFiIiwibGFzdFNlbGVjdGVkTGluZUhhc2giOiI0MTJjYTM0NWNjZjc1YmY5YzA4MDZiY2U2OTViZThkZTgwOGI3OTk4NDI1MWE3YTU0ZDIwMmNmNjEwMWRkNDUxIiwicHJlY2VkaW5nQ29udGV4dExpbmVDb3VudCI6MywicHJlY2VkaW5nQ29udGV4dEhhc2giOiI2MDYzYzRkMWFmOGNkODliYjBiZGQ3NjU3ZmZjYmY2ODA2YTYxMGQ3OWQ2YzhkNGMzMDdjMjQxZGYzOTAwNTIyIiwiZm9sbG93aW5nQ29udGV4dExpbmVDb3VudCI6MCwiZm9sbG93aW5nQ29udGV4dEhhc2giOiJlM2IwYzQ0Mjk4ZmMxYzE0OWFmYmY0Yzg5OTZmYjkyNDI3YWU0MWU0NjQ5YjkzNGNhNDk1OTkxYjc4NTJiODU1In0"
         },
         {
-          "resource": "repo://src/main.rs#L208-L230",
-          "version": "repo-lines-v1:sha256:6f0ef80029cd8b3a3ed5570438cff5846f3f70c6e0fe4866184c8210aac3873f:eyJzZWxlY3RlZExpbmVDb3VudCI6MjMsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjI1Y2Y1ZmMyM2YyYTZkMjNjM2VhNTFlNzJmNWU4OTZjNzU1NWZiYWEwNWNkNWIxZWExODJiZjk2MThhOGU4MWIiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjM1ZjZmNDY2ZTU5Y2ZjMzA3MDNjNDVmNzJhZjY1ZGU1NGFjMzYzZDM1NGNjMWEzMzMwMzUwYWNkNDUyNDM0YzQiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjowLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImUzYjBjNDQyOThmYzFjMTQ5YWZiZjRjODk5NmZiOTI0MjdhZTQxZTQ2NDliOTM0Y2E0OTU5OTFiNzg1MmI4NTUifQ"
+          "resource": "repo://src/main.rs#L218-L248",
+          "version": "repo-lines-v1:sha256:f7cd754f65b78b7c7cbecbdb74744fa5e5445848aa5591ee568515855d4c39a3:eyJzZWxlY3RlZExpbmVDb3VudCI6MzEsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjI1Y2Y1ZmMyM2YyYTZkMjNjM2VhNTFlNzJmNWU4OTZjNzU1NWZiYWEwNWNkNWIxZWExODJiZjk2MThhOGU4MWIiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjM1ZjZmNDY2ZTU5Y2ZjMzA3MDNjNDVmNzJhZjY1ZGU1NGFjMzYzZDM1NGNjMWEzMzMwMzUwYWNkNDUyNDM0YzQiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjowLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImUzYjBjNDQyOThmYzFjMTQ5YWZiZjRjODk5NmZiOTI0MjdhZTQxZTQ2NDliOTM0Y2E0OTU5OTFiNzg1MmI4NTUifQ"
         },
         {
           "resource": "repo://src/transcribe.rs#L299-L520",
@@ -131,12 +131,12 @@
           "version": "repo-lines-v1:sha256:e85913975a63028c40bc84fd3c1b842d226f277fb56a1588907f49fff7f23119:eyJzZWxlY3RlZExpbmVDb3VudCI6MTc4LCJmaXJzdFNlbGVjdGVkTGluZUhhc2giOiIyNWNmNWZjMjNmMmE2ZDIzYzNlYTUxZTcyZjVlODk2Yzc1NTVmYmFhMDVjZDViMWVhMTgyYmY5NjE4YThlODFiIiwibGFzdFNlbGVjdGVkTGluZUhhc2giOiI0MTJjYTM0NWNjZjc1YmY5YzA4MDZiY2U2OTViZThkZTgwOGI3OTk4NDI1MWE3YTU0ZDIwMmNmNjEwMWRkNDUxIiwicHJlY2VkaW5nQ29udGV4dExpbmVDb3VudCI6MywicHJlY2VkaW5nQ29udGV4dEhhc2giOiIwNmE2YTY2NDFiOGNiOGMwMjgxMWEzMzBhMmYyYTMyMzRkOTk5Zjg4YWJlYWNhYzEyYThlYTU1N2ZjZWQ4MzQzIiwiZm9sbG93aW5nQ29udGV4dExpbmVDb3VudCI6MCwiZm9sbG93aW5nQ29udGV4dEhhc2giOiJlM2IwYzQ0Mjk4ZmMxYzE0OWFmYmY0Yzg5OTZmYjkyNDI3YWU0MWU0NjQ5YjkzNGNhNDk1OTkxYjc4NTJiODU1In0"
         },
         {
-          "resource": "repo://src/web.rs#L411-L503",
+          "resource": "repo://src/web.rs#L413-L505",
           "version": "repo-lines-v1:sha256:c5f71abad037e96401b479b47443f71e3ace10e2d54fca94fb9318d2f14bbd6b:eyJzZWxlY3RlZExpbmVDb3VudCI6OTMsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjI1Y2Y1ZmMyM2YyYTZkMjNjM2VhNTFlNzJmNWU4OTZjNzU1NWZiYWEwNWNkNWIxZWExODJiZjk2MThhOGU4MWIiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjhmZDM2OGIzODI2OTRjMDYxZWMzMDkyOTYzNjNkYTBlYWNkNzI3NDlmZTQzYmFiNTM4ZWMyMjlhMmM5MDE0NTkiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjowLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImUzYjBjNDQyOThmYzFjMTQ5YWZiZjRjODk5NmZiOTI0MjdhZTQxZTQ2NDliOTM0Y2E0OTU5OTFiNzg1MmI4NTUifQ"
         },
         {
-          "resource": "repo://src/whatsapp.rs#L1216-L1492",
-          "version": "repo-lines-v1:sha256:41d01ec83c54ff5591cb09d8237121d972db1aa080e4f8aa6cae6678d9eea12b:eyJzZWxlY3RlZExpbmVDb3VudCI6Mjc3LCJmaXJzdFNlbGVjdGVkTGluZUhhc2giOiIxZTBhZjNmZDRhNGMzYTFkNDdjN2Y0NzBlZDBkODZkOTBkOWRlMjEwYTVlYjE0NTVmYmQ5NDQ4MmE0NzY1YjcyIiwibGFzdFNlbGVjdGVkTGluZUhhc2giOiI5MmI1ZWRmNTk0OWNmZWE2NzgyMmNmNjkxMTMyMzYzZDY4ZDZmMWUxNjlmNWUwZDRmN2MxNGU1ZmExZTZkNGU1IiwicHJlY2VkaW5nQ29udGV4dExpbmVDb3VudCI6MywicHJlY2VkaW5nQ29udGV4dEhhc2giOiJhNTIxNjA0YjM1ZmM0MGFmMDBjYmE0ZWVhMmZkNWUyYzNhZjE4NjNiMzkxMTg1ZDdiZjRiYjA5ZTcyZDBjOGIzIiwiZm9sbG93aW5nQ29udGV4dExpbmVDb3VudCI6MywiZm9sbG93aW5nQ29udGV4dEhhc2giOiI2N2IyMGRmN2RkMmVlZjRhMjFmNmM1YjYxMWM5ZTcxZTAzMmI2Y2E3NmE2NTQ5ZDZjNzkwYjVhYTc1ZmJiODEzIn0"
+          "resource": "repo://src/whatsapp.rs#L1269-L1552",
+          "version": "repo-lines-v1:sha256:8d6ded2fcba643cfcaffffe1de4d8ac945643f081cccc288faece1c101cae0ce:eyJzZWxlY3RlZExpbmVDb3VudCI6Mjg0LCJmaXJzdFNlbGVjdGVkTGluZUhhc2giOiIyNWNmNWZjMjNmMmE2ZDIzYzNlYTUxZTcyZjVlODk2Yzc1NTVmYmFhMDVjZDViMWVhMTgyYmY5NjE4YThlODFiIiwibGFzdFNlbGVjdGVkTGluZUhhc2giOiI0MTJjYTM0NWNjZjc1YmY5YzA4MDZiY2U2OTViZThkZTgwOGI3OTk4NDI1MWE3YTU0ZDIwMmNmNjEwMWRkNDUxIiwicHJlY2VkaW5nQ29udGV4dExpbmVDb3VudCI6MywicHJlY2VkaW5nQ29udGV4dEhhc2giOiJkNmQxM2RhNTNkZGY1NGUwOWE5ZmIzNzc4NzQxNGE5NjE0NmQ0NzFjNWViYzZjZTI2Y2Y5YTM4NGYxODc3OGYxIiwiZm9sbG93aW5nQ29udGV4dExpbmVDb3VudCI6MCwiZm9sbG93aW5nQ29udGV4dEhhc2giOiJlM2IwYzQ0Mjk4ZmMxYzE0OWFmYmY0Yzg5OTZmYjkyNDI3YWU0MWU0NjQ5YjkzNGNhNDk1OTkxYjc4NTJiODU1In0"
         }
       ]
     },
@@ -155,7 +155,7 @@
       "statement": "web tests cover query stripping, route resolution including /api/relink, the X-Wh-Request header check, which routes require it (relink included), qr_svg producing an SVG document, and request body trimming, rejection and size limit.",
       "evidence": [
         {
-          "resource": "repo://src/web.rs#L411-L503",
+          "resource": "repo://src/web.rs#L413-L505",
           "version": "repo-lines-v1:sha256:c5f71abad037e96401b479b47443f71e3ace10e2d54fca94fb9318d2f14bbd6b:eyJzZWxlY3RlZExpbmVDb3VudCI6OTMsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjI1Y2Y1ZmMyM2YyYTZkMjNjM2VhNTFlNzJmNWU4OTZjNzU1NWZiYWEwNWNkNWIxZWExODJiZjk2MThhOGU4MWIiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjhmZDM2OGIzODI2OTRjMDYxZWMzMDkyOTYzNjNkYTBlYWNkNzI3NDlmZTQzYmFiNTM4ZWMyMjlhMmM5MDE0NTkiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjowLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImUzYjBjNDQyOThmYzFjMTQ5YWZiZjRjODk5NmZiOTI0MjdhZTQxZTQ2NDliOTM0Y2E0OTU5OTFiNzg1MmI4NTUifQ"
         }
       ]
@@ -179,7 +179,7 @@
       "statement": "The whatsapp test relink_deletes_the_session_and_offers_a_new_qr is ignored by default because it connects to the WhatsApp servers; it relinks a real client and asserts that the second QR carries different device keys.",
       "evidence": [
         {
-          "resource": "repo://src/whatsapp.rs#L1218-L1258",
+          "resource": "repo://src/whatsapp.rs#L1272-L1312",
           "version": "repo-lines-v1:sha256:041308c138b61aed5f622ca006de95e62d877c7ece53ef7e68db634046e5e877:eyJzZWxlY3RlZExpbmVDb3VudCI6NDEsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjAxYmE0NzE5YzgwYjZmZTkxMWIwOTFhN2MwNTEyNGI2NGVlZWNlOTY0ZTA5YzA1OGVmOGY5ODA1ZGFjYTU0NmIiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImQxMzYwZTc4OTM3Mjc0OTA3NjlhYzM4ZTYxMjRhNzZhMGIyOGZkZGJiZjFlYTM1ZDJiOTk4MDYzMjNiMWZjODUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImNjZWY2YTcxMDEyOTdmNmI5ZGNmZDFhOTE1YjE4Y2QyY2EzZTcyMmY5NWI2YzA2YWY5ZDYyNmZmYWNmZTFiYTYifQ"
         }
       ]
@@ -193,10 +193,20 @@
           "version": "repo-lines-v1:sha256:da199b4f8ef9b9aac55eada5da5b2f75daebee1df240a1bc0d2f3ba4aed098bd:eyJzZWxlY3RlZExpbmVDb3VudCI6MjAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjI1Y2Y1ZmMyM2YyYTZkMjNjM2VhNTFlNzJmNWU4OTZjNzU1NWZiYWEwNWNkNWIxZWExODJiZjk2MThhOGU4MWIiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImQ4ODU5MjEwZWJjYjEyY2Y2OGM2MTYyMjJmYzU2MGMxZjY4ZTJmYTMxZjYzMDQ5M2RmODA5NTg4ODdkNTdhZjkiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjoxLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEifQ"
         }
       ]
+    },
+    {
+      "id": "claim_f2f34f7818814600af9b40f72edbf5e1",
+      "statement": "The main test checks that BUILD_VERSION starts with the package major.minor, continues with an all-digit build number and ends with the commit hash in parentheses.",
+      "evidence": [
+        {
+          "resource": "repo://src/main.rs#L235-L247",
+          "version": "repo-lines-v1:sha256:d3b06105e597846134046edb9e948e881f8b64be24dd910b55a7f6514fd64ae5:eyJzZWxlY3RlZExpbmVDb3VudCI6MTMsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjkyYjVlZGY1OTQ5Y2ZlYTY3ODIyY2Y2OTExMzIzNjNkNjhkNmYxZTE2OWY1ZTBkNGY3YzE0ZTVmYTFlNmQ0ZTUiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImRlODVjNDAwNWQxMjA1OWIyOGYwOTZkNjE0NGZjMWRjNDUwOTBkZmYyYjZhNzU2YjIyZWI3MTdjZDI3OTBjZDEiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjoxLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEifQ"
+        }
+      ]
     }
   ],
   "verification": {
     "by": "openwiki/0.7.2",
-    "at": "2026-10-24T16:39:50.206Z"
+    "at": "2026-10-25T12:54:53.684Z"
   }
 }

+ 71 - 49
openwiki/.claims/workflows/ai-autoreply.json

@@ -1,6 +1,6 @@
 {
   "schemaVersion": 1,
-  "pageVersion": "sha256:51f277c84ac8b779ca6c470fcddd420be0463aa6c621620207ab047c32ec60ae",
+  "pageVersion": "sha256:90d3dc1c89e757fdbd947c7d787a70192a1f423ea5575b17fd0b0cb5cfe6e505",
   "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; F3 targets the selected list row with list focus and the open chat with input focus.",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L1342-L1378",
+          "resource": "repo://src/app.rs#L1391-L1427",
           "version": "repo-lines-v1:sha256:0186ad31b1302456cd660841125a254c3e2f46e2074372b97855046cd705d5de:eyJzZWxlY3RlZExpbmVDb3VudCI6MzcsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjBjMWZhZjI0NjQzNTZlZjY0NzAyNjI4YzM0MGYyOGNhZDljMjY3OGYxMTViNTAxNDZkMGMyNTBmNzhmZDk2NWMiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjkzNzNhMTgxNzI2YzQ3ZjdjNzlmOTI3ZDI2NzhhYzQ3NjYzY2VhMzI1OGYxOGEwNTM0ZTg2NDVlYmFlOTlkMGUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImUzZWNmNGIzNzRlN2M0ODFlZjUzODQ1Y2Y1YjlmNDI5MDk3NmQxYTlmNDlkOWY5NWJjNjIxNWEyY2VhMDYyOTAifQ"
         },
         {
-          "resource": "repo://src/app.rs#L1889-L1902",
+          "resource": "repo://src/app.rs#L1963-L1976",
           "version": "repo-lines-v1:sha256:76ea2311dd543d33bf1c52b4cee8997184cb67643a8ba3b2f715abf2fea34667:eyJzZWxlY3RlZExpbmVDb3VudCI6MTQsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjA0NDI0MGFiOGNkMTM1NDE2ZTQ5MjMyYTEzMGIzYzlkYmI2NWZmYzg0OGNjYzY3OWJhYTQxYWQzNjY4ZTA3MjQiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjE5ZTc2MzU1Y2YxMTk5NDgwYzNmOTk4MzZiMTQ4NmM4NDFkODkxZmFhZDkwNGM2MDhlMDkzMDIyZDM3NTdhZWUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjhkYTViNjcyY2ViZDYwMjY1YTM5NmE4NTAxZmMwMmEyYzg2YTZkNWJkYzMyNzRlMGQyZDA5ZDYwMTJhY2Q1ZTgifQ"
         }
       ]
@@ -31,7 +31,7 @@
       "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#L58-L107",
+          "resource": "repo://src/autoreply.rs#L64-L113",
           "version": "repo-lines-v1:sha256:2c5bff97825cbfe1e0822d2f2450e3d71e38f9eba0e4ba5d78ce48dd225acc29:eyJzZWxlY3RlZExpbmVDb3VudCI6NTAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImRlNzAzNDZhYmE0OTZiOTAzNzI1MTY5OWQzY2YyNDAwNmU0ZGYyMDM2N2MzNDRiZjllZjkyOWEwMjJmYTRjZGYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjIxNTY3ZTI1ZmQ3ZTExMzc3YmU4NDk5OTc2NDEzOThhYzRmZTMxNTBiNzE3OTA4NjNiNzc0ZTAzYmNiZGM0ZjkiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjE2MzhiZmQ4ZjNiOGY1OTQzZDQyODJhMmY3MGQ0ODBhOWE5MGVkMGE3ZTA1ZWQyOWNmMWE0YzRjMzZkYWJkN2IifQ"
         }
       ]
@@ -41,15 +41,15 @@
       "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#L485-L532",
+          "resource": "repo://src/app.rs#L531-L578",
           "version": "repo-lines-v1:sha256:25898277ac10619d92709af2d4e7204132d29205e0a7f7d7888b7f4f15606eed:eyJzZWxlY3RlZExpbmVDb3VudCI6NDgsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjBlYWIwYmIzYTMyYTBlZGUzMGY0ZTk1MmQ1MjcxYzRhMTgwMTZjMGU4Y2M4N2VhYmE1ODA1OTYyZDNjOGE4NTYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjgyODRmZjk5YWRlYTg2MmRlOTFhNTQwZTNjOTcwNzc4NWU0ODY1ODg2YjZlOTFmMzFiNzdmNzdlOGQzNzI4OGMiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImI1YjUzYzQyYTJjZjU2YmRlZWIxOWJhYTZhZjdhYTQzZmRkNjFmN2VhNTQzOGVjOWRmYTAwYjc0Nzg3NWM1ZGQifQ"
         },
         {
-          "resource": "repo://src/app.rs#L858-L860",
+          "resource": "repo://src/app.rs#L904-L906",
           "version": "repo-lines-v1:sha256:2f717e0ab27c971d848f3c4b787b63e1843cdddb271adaa95d5811bab8ccb1c6:eyJzZWxlY3RlZExpbmVDb3VudCI6MywiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiZjZkNGY2ZDY0NTVjYjVlMWJiMTEwOTU0NzU5ZjliOTU2YzhmMTU5YTE1NTExZjVkODZhZTZhMGUwMGMyMTQzMiIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDg5M2Y0MGUzNzgxZWI4OTZjODJkMDQ2YTA4MDY3YTExZTM1Y2RmMmYxZGZhZjI2ZmVhOTBiOGRiNjY3NjQxMyIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiYWE0OGNmOWNjZDU2MDljYjc0MmQxYjNkMmY5MjIyNmYzZjQ2YjU4MWE3Zjk5YWM5N2Q4NDdmYTBjOWQ5MWI3OSIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiOTEyYzE5ODI4NjExZDJhZjdmZWFmNjQyN2ZmNzE0MjFiN2JhMTU4MzM5OWY0YWU5ODBiMzJkNThlNDUzYTJiYSJ9"
         },
         {
-          "resource": "repo://src/app.rs#L987-L989",
+          "resource": "repo://src/app.rs#L1033-L1035",
           "version": "repo-lines-v1:sha256:a9005f55bfc9d8f0fe3bcd2d8af41d7200648c575a982bd6184e0a709f666f1d:eyJzZWxlY3RlZExpbmVDb3VudCI6MywiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiY2ZhYjYzNzAzMWNiMGJiMTEzOGI0YjQ2OTE3MDhiNjRhNzA3MTQ3NTZhMWExYTQzZTE0NGZmY2MwY2E2NTllMCIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDg5M2Y0MGUzNzgxZWI4OTZjODJkMDQ2YTA4MDY3YTExZTM1Y2RmMmYxZGZhZjI2ZmVhOTBiOGRiNjY3NjQxMyIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiYTZjZWVhMWQ3YmU1MjdlYWYxYTAzZTNhYWU0MjQwYjg3ZWQyZDg3NzI1YWIxNzQyZmNlMjRmZjczNTE2ZTllMiIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiYTZlYTYzN2IyZDc3YWVjZjY1Y2EyOWFjNTgzOGQ4NmRmNGEwODM4NTdmMDQwM2U5OWM2NWU3NzQyMzI5NDMwYyJ9"
         }
       ]
@@ -59,11 +59,11 @@
       "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#L182-L191",
+          "resource": "repo://src/autoreply.rs#L188-L197",
           "version": "repo-lines-v1:sha256:1b2ab043319224b4a08306fe10e69a7a41bd99836ab89ee9cbb0c1a1f49fbf28:eyJzZWxlY3RlZExpbmVDb3VudCI6MTAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImRhNjA3MDVjZGJlZjdmYjI4MGZmYjZkOTRhM2YyNTBhOGRmNTAzZGVhNTgxZGZhMzU0NTM4NTM4ODJhYjc3NTEiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjU0ZWYyYjYzZjg4Njk1ZTc2OTIzMjVmMjE3Mzk4NzJjMDhiNTgxNjdjY2YyZWM2NTY2MGYzMDJkOTMyYTEyY2EiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjhkOWU4ZmE2ZTkzOTA1ZTQwYzdmNmM4YWM2NWRkNTQ4MzQyNDQ3M2I1MmMxYWUxMDE5MGIwNGNmNzI4ZTZiNWEifQ"
         },
         {
-          "resource": "repo://src/autoreply.rs#L241-L265",
+          "resource": "repo://src/autoreply.rs#L247-L271",
           "version": "repo-lines-v1:sha256:bbcff26d49ec29313b51b2a51c2d5df2f8c9ed3f715d9fc6b4af559d93b7a68d:eyJzZWxlY3RlZExpbmVDb3VudCI6MjUsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6Ijc3OWM1NTkxNmMzNjZmZDdhNmU2ZTYwZmE4MDVjOTYxZGExYjVhNDFhNGJkMDk0MDkwZjZkM2YxODc5NTJkMTIiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6Ijc3MDE1ZTdkMThiMDkxMTVlZjkyMzU2NGJlMDQ3N2IxODllZTViYWI2ZjdkYmI4ZDg5NTZkMjQ0M2YwMjFmOWIiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjJmYmE5MjI2NzJlMWM1MjY1NTMwZjBkYTNlN2RlZDBmOWQxZjUxNjY4YjJlMWMyZWI2MThlYWEzM2VlZmZkMDMifQ"
         }
       ]
@@ -73,11 +73,11 @@
       "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#L148-L180",
+          "resource": "repo://src/autoreply.rs#L154-L186",
           "version": "repo-lines-v1:sha256:32e4689dd25915397864463e71677879f2543d73b615303632cb241a548e433b:eyJzZWxlY3RlZExpbmVDb3VudCI6MzMsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjBjMzM4MTNhM2RiZmNlNjRmYzY2MTk0YmVlY2NlNDQxMjgxZjBiMTBkYjkyMzJjMmNmMzQ4N2NiMmExOGExZDgiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImQ3YTcwYTRiOGY2ODNiMmVhZGFlYzIyYWRkYzU4NzY3ZDUwZDAwZDlmYTQ0OTU0YjQzOTY1NTdmZDMwZWU3M2UiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImI0ZTI0ODc3YWIzZWIyYTI0NWE4ZWQxOTJiMDIxNjI5MjE2N2RhN2ViMDRlM2Y1Y2MxYjQ5Yzc1MjEzMzUyYjQifQ"
         },
         {
-          "resource": "repo://src/autoreply.rs#L293-L309",
+          "resource": "repo://src/autoreply.rs#L299-L315",
           "version": "repo-lines-v1:sha256:daeb52efe3c35b1565aea1badb2260d9cfb4caef892ee11755af3f03d40c0788:eyJzZWxlY3RlZExpbmVDb3VudCI6MTcsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImRhNTE4MDYxM2Q4NjEwNGE4Zjk1N2FhYTI2ODI4YjBmMjQ2NTk3ZTg4M2UyMzk3YTc3NDEyMzVkNjNhMTkzZmMiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjE5ZTc2MzU1Y2YxMTk5NDgwYzNmOTk4MzZiMTQ4NmM4NDFkODkxZmFhZDkwNGM2MDhlMDkzMDIyZDM3NTdhZWUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjBjNGVkNWUzYjNkYWRhZTk2YzQyZDg5ZDk2MTBkNTYyMmY5ZTkxOGI2ZTU5ZTQ2ZDMzNzUzMjNiNTNmNGJmMWUifQ"
         }
       ]
@@ -87,34 +87,34 @@
       "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#L324-L332",
+          "resource": "repo://src/app.rs#L352-L360",
           "version": "repo-lines-v1:sha256:00767085b96f8924c277929b72de9745e84060a578a404e5382f4fe6e07a3b7b:eyJzZWxlY3RlZExpbmVDb3VudCI6OSwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiNWQ4YjIxOTEzNzM3OTM3Zjk1YTUzZjNjODY4MzdjYmFmZjcxZWZkMzQyOGUwMzc0ZWQ0ZTFkZTM4ODczODVjOCIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNTUyMmM0MmNkMTljNmQwM2Q4MDhjNDUyNzIwNTUyODI1YjdlYTFjNWI0OTFiMjE4NTExY2FjNzQxNWNmOWQ5YiIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiYTA5MzYwMDY0NjVjYWQ2YzlkM2JmMGViMWQ1ZTFkNTNhMTIwN2FiN2JlMGEyZTRiMDc0OGNmMmE0ZGI1MTExMiIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiZmY3ZThiZWFmYzVhNjZlNTc3ZDk0MmMyNWZiNWYzOWE0ZTRjZGRlMTJkODg3NjBlZmM4ZTE1Y2U4OTEyOWEwNiJ9"
         },
         {
-          "resource": "repo://src/app.rs#L539-L542",
+          "resource": "repo://src/app.rs#L585-L588",
           "version": "repo-lines-v1:sha256:dd72c631d7b9707b384154ff042bea6cc03745c04b4c6aa82dd6b8d77676a250:eyJzZWxlY3RlZExpbmVDb3VudCI6NCwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiODgxNjNhYjZmYzg4NDRiMTBjZWNhZTQ3YjVlYWY5YmYzNjg5NGRjMDk0N2IzYjAxNmRkYmRlODI5MTc2YjNiOSIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDg5M2Y0MGUzNzgxZWI4OTZjODJkMDQ2YTA4MDY3YTExZTM1Y2RmMmYxZGZhZjI2ZmVhOTBiOGRiNjY3NjQxMyIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiM2ZiYTBkNGUwM2NhZGQzMTgwZmNhZDlhYzRiZmZlZGNhOTU5ZTY1MDUwYjI3YWFmYzA1Mjc2MjUxMDE4MzI4NCIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiMGZkOTUxZjMzMzEyYjBkYjE5OTA3M2NkMjM0MDgxYzczMzI0MjFmNjBmNGM2ZDgwMDdiZTZiMzVjMzA4NjE0ZSJ9"
         },
         {
-          "resource": "repo://src/app.rs#L837-L852",
+          "resource": "repo://src/app.rs#L883-L898",
           "version": "repo-lines-v1:sha256:ca8594a3de60f6943b74187223fab353c0af7a724cf3ef68f1d2f06141510c59:eyJzZWxlY3RlZExpbmVDb3VudCI6MTYsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjNmN2EzMzI2Mzk4MGZiOTQ4OGI5NDcyZTI4ZGU0NjE4ZWVjMzc3MTE2ZmFmZDViODZlNmUzYTZhZGVjYjM2YTEiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjRjZTljYzQ1ZjFjOTkzN2IyMGMyZmJjOTM3YmUxN2QxYzhhMmU0MTdlMjUwMDI2ZjM4MzU3N2MyY2ViNGRkN2UiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImNmMGY4ZWU3NGMyYmNhMjZlNTlmY2NmYjUzZmQyYWI2OGJjNzIwZTY2MGZiZjM3M2IwZjFhNTY4ZmIyNjBjOTUifQ"
         }
       ]
     },
     {
       "id": "claim_e5cd4fb182a34e47aec8344c6682627c",
-      "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.",
+      "statement": "build_turns starts with the system prompt, an optional 'Oggi è <date>.' line, then 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:eyJzZWxlY3RlZExpbmVDb3VudCI6MTUsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImJkYTIxOWRlYzM3NjlkNDZkZjMxMmE5MzYzZGFhZmI5N2IzMGQxMTRkNWE0NGY4MmY4ZmNhZjJmNTJlNWY1NzkiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjdjY2U3ZjMxMmFjMmQ5ZmMzNGFlOGJjZDJhM2RkZmNiNGVjNjEyNDM0OTBjNTYwMDVhZmJmZmE4ZGM5ZjFiMTgiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImZkMGM2MzgyNzI5ZDA4ZTE0NGQ1NmJiNWJlNzAwYjMzMjEzMTMyMjAwNzQ3ZWI3M2JlYjc0NTAwOTlhYjg2Y2UiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjM1N2I1ZjVkNWU3MTlhMDlmOTM2OGE5OWI5MzEwZTU2YjM1YTI1NDNjYjE2MGE2NmJiYzIyNjgyOTM3Nzk5ZWEifQ"
         },
         {
-          "resource": "repo://src/autoreply.rs#L311-L436",
-          "version": "repo-lines-v1:sha256:5a954d83ee76e833ed24d427d0e8e36e66ff9c8a1ac999e67ad6a3124d5df508:eyJzZWxlY3RlZExpbmVDb3VudCI6MTI2LCJmaXJzdFNlbGVjdGVkTGluZUhhc2giOiIzZTBiMjBmNDZiMWYzNDE5OGQ3NGE0N2I3ZGNiZDUxZjI5NzhmYjNhM2YyNWE2OGQ0YzE1NzA3YTUwNTI0NjVlIiwibGFzdFNlbGVjdGVkTGluZUhhc2giOiI0MTJjYTM0NWNjZjc1YmY5YzA4MDZiY2U2OTViZThkZTgwOGI3OTk4NDI1MWE3YTU0ZDIwMmNmNjEwMWRkNDUxIiwicHJlY2VkaW5nQ29udGV4dExpbmVDb3VudCI6MywicHJlY2VkaW5nQ29udGV4dEhhc2giOiIxOWU3NjM1NWNmMTE5OTQ4MGMzZjk5ODM2YjE0ODZjODQxZDg5MWZhYWQ5MDRjNjA4ZTA5MzAyMmQzNzU3YWVlIiwiZm9sbG93aW5nQ29udGV4dExpbmVDb3VudCI6MywiZm9sbG93aW5nQ29udGV4dEhhc2giOiI1NGJhMWQ4NGE2YjE3M2RlOGMwZjhlNjNlZTgzNTFhOTE2ZGUyYTNlYWViZjFhZDBjZjA1YmExN2JiYzY1YjdjIn0"
+          "resource": "repo://src/autoreply.rs#L317-L450",
+          "version": "repo-lines-v1:sha256:99a36b8a7917973178f949421f5c633f1b93bf83efb1559ce7915127743e9d65:eyJzZWxlY3RlZExpbmVDb3VudCI6MTM0LCJmaXJzdFNlbGVjdGVkTGluZUhhc2giOiIzZTBiMjBmNDZiMWYzNDE5OGQ3NGE0N2I3ZGNiZDUxZjI5NzhmYjNhM2YyNWE2OGQ0YzE1NzA3YTUwNTI0NjVlIiwibGFzdFNlbGVjdGVkTGluZUhhc2giOiI0MTJjYTM0NWNjZjc1YmY5YzA4MDZiY2U2OTViZThkZTgwOGI3OTk4NDI1MWE3YTU0ZDIwMmNmNjEwMWRkNDUxIiwicHJlY2VkaW5nQ29udGV4dExpbmVDb3VudCI6MywicHJlY2VkaW5nQ29udGV4dEhhc2giOiIxOWU3NjM1NWNmMTE5OTQ4MGMzZjk5ODM2YjE0ODZjODQxZDg5MWZhYWQ5MDRjNjA4ZTA5MzAyMmQzNzU3YWVlIiwiZm9sbG93aW5nQ29udGV4dExpbmVDb3VudCI6MywiZm9sbG93aW5nQ29udGV4dEhhc2giOiI1NGJhMWQ4NGE2YjE3M2RlOGMwZjhlNjNlZTgzNTFhOTE2ZGUyYTNlYWViZjFhZDBjZjA1YmExN2JiYzY1YjdjIn0"
         },
         {
-          "resource": "repo://src/autoreply.rs#L482-L503",
-          "version": "repo-lines-v1:sha256:9f1d57d8cf162701d624737e5d0477297db574585ccc1e38af3f9cd95ce227a1:eyJzZWxlY3RlZExpbmVDb3VudCI6MjIsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImFmMDViNzI5MDFlOTRmOTRjNzZmMmI3YjY0YjU5YjlmMDg3M2JlNWFiY2ZhYTBlY2ZhMWU0NTk3ZDE4MDJhZWMiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjY3NDJmNGI4NTg1Y2M2ZDZjMmFiNzdiYTVmNGVhMmY0OWQxNzFiMzRmNWExMjdlYmM4NzU2ZWUzOWQ2OTI4ZGQiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjdiYzk4ZDZiMGRjNDkyYzUwMTZkZjEwODA1YTNhNTRlYjljMTg0YTVhNjEzN2U3ZjRiZWNmYTBjMjIwNTUwZDAifQ"
+          "resource": "repo://src/autoreply.rs#L506-L527",
+          "version": "repo-lines-v1:sha256:f9c597463e1fac233f311f33358ed9a949831e38776f7c9d0d8965be5952272a:eyJzZWxlY3RlZExpbmVDb3VudCI6MjIsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImFmMDViNzI5MDFlOTRmOTRjNzZmMmI3YjY0YjU5YjlmMDg3M2JlNWFiY2ZhYTBlY2ZhMWU0NTk3ZDE4MDJhZWMiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjFlZWQyZTI2YzY2Mzk5ZjM4YWRhOGY4MDMxYzA4ZTZlMTYzNjdlOWE3NzNkNjYzMTUyZDI2NDI4ZTMyNzZjYmMiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjdiYzk4ZDZiMGRjNDkyYzUwMTZkZjEwODA1YTNhNTRlYjljMTg0YTVhNjEzN2U3ZjRiZWNmYTBjMjIwNTUwZDAifQ"
         }
       ]
     },
@@ -123,15 +123,15 @@
       "statement": "Before a reply, audios in the context without a transcription, at most the 3 most recent, are transcribed first (the reply follows AudiosTranscribed, which then describes the postponed images), and images in the context without a stored or in-progress description, at most the 5 most recent, are described (the reply follows ImagesDescribed).",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L656-L679",
+          "resource": "repo://src/app.rs#L702-L725",
           "version": "repo-lines-v1:sha256:a257dd09ef5fad9478ec541c5e5e2b908a11cdc448654986a2150099337f2e63:eyJzZWxlY3RlZExpbmVDb3VudCI6MjQsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjE2N2NhYmY5MDc5Njk4OWRkMTE4ZmVmZjZmYjJhNzI0ZjE5OTAxMjJhOGYzNmQxNmEyM2E5MDU5ZjdiNzEzM2EiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjQxZjkzM2MwZjY2NWU1OWJiMWU0ZTk5NjBmNjA2ZGVjMjM1YjBiYzA2ZGM0ZTUwYmYwZGUzNTI5M2E2Nzg0NmEiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjM5YzEwY2I0YTNhZmI3NWE4N2VmNmE4MmZmYzVhMjBiZDZiN2U0ZmQ0NDRhMmNjMWRlMGZmZDg3MjljODQzZGUifQ"
         },
         {
-          "resource": "repo://src/app.rs#L784-L829",
+          "resource": "repo://src/app.rs#L830-L875",
           "version": "repo-lines-v1:sha256:2da3a90f0f2862e08e4c2b56a2b26f8f018449d7b0112fc46e11f214fbeca676:eyJzZWxlY3RlZExpbmVDb3VudCI6NDYsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImJkYmFiZjFkNjI2OTdjZWE2NTk5ZWQyNjg5MDQzY2RhYzAyOWUxMWQ3NWIwY2U4MTRlYWVkNzhjZTk2Zjg5ZGMiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjNiOWQxNjk5ZDM4YTRkZDQ3ZjgxMGFmMDViYTIzZTM2N2NiNTE5NzljODAxM2Q3YTZmYWQyYjAzY2Q0MzA0MTEiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjBhN2YyNDA4YTExOTJiY2VmZTI0ODJhNTg2MGM1MTVhMmQ4NGIxMzQzZTE1ZDdmMGVmNGRhMTA0YzY1ZTJiM2EifQ"
         },
         {
-          "resource": "repo://src/app.rs#L888-L918",
+          "resource": "repo://src/app.rs#L934-L964",
           "version": "repo-lines-v1:sha256:a6beeca1efcc61a62e317b875ec0455fe4adfd64a193fd9393c5b484952ab986:eyJzZWxlY3RlZExpbmVDb3VudCI6MzEsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImNlNGZiYTJmZGY0ZTk4MmNjZmE1ZThkYzBmOTA1ZWQ2NjVhY2E1YzgxZWZiZjRjNDNlMjAwMGQ5YjBlYTBhN2QiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImI2MmYxYzgxNDE1YjNjNTQ5ZmRlNjE3MThkZDRiOGQ0Nzg2MzU5MjEyNWI1NzEwMDZkODhmODkyYzc3NDgxNjUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjIyMGUyNjkzMzFhZTU2NTJiYTMxNWViYTdmYzRlNWMxNWFhNDlhMzdiMjE2NGY3NjRlYmNiYTA1ODE5ZGMyNDAifQ"
         }
       ]
@@ -141,7 +141,7 @@
       "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#L1058-L1120",
+          "resource": "repo://src/app.rs#L1107-L1169",
           "version": "repo-lines-v1:sha256:dc263d71dc225c968c33c780874494b82440bfd214546483d0aef9fd6a02bf46:eyJzZWxlY3RlZExpbmVDb3VudCI6NjMsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjBkNzQ1Mzc3ODY0MzI0MmZhZDE3MGZhNjcxNTUxYTJiYjI4ODM4Y2M2NmQ5ZjMwZWNkODhjZTNmNjFhYjRlMGEiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6Ijk5OWJkZWZlZWFjYTYzNDBlMDI5MGRjODZlODg5Yzg5ZDVlMTg5YWE1YmE0NGI1ODViM2I3MDczNGY0YTYyZDgiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjYzYjQ0YTU2ODZkYjM0MjNlMDVmOGMxOWMwYmMyNzlmMDA1NTE4NDJmMzgwZjIxMTM1ZTkzMTZhY2IwNmQ5ODEifQ"
         }
       ]
@@ -151,11 +151,11 @@
       "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#L1581-L1591",
+          "resource": "repo://src/app.rs#L1655-L1665",
           "version": "repo-lines-v1:sha256:be3fc24d7db22727c3dddd8c5116b8fd64a13c8a473f99818b61f3bb6ac76204:eyJzZWxlY3RlZExpbmVDb3VudCI6MTEsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImMxMmIwOGNlNWY0N2RmY2RjMTQ1MTA4ZDFkNmQ2NzhiYTllNDhjZjBmMGY5YzdiYTBhNzMxYTkzMzFjYjc2ZDMiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjRjNjg0ZTk4OWVmNzk4ODJiNmIyOTMwNDdiMTJiNDg4YjkwNjlhYmUzMzc2ZTVhNTNhOGEyZTI5YjY4ODcyMTAiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjA1ZDJlYmFjZThlMTE0NjAzZTYyZDc1YjVmNmRjODFjMzJiMzgzMmI4YjBiMGUzZDVhZDY0OTMyMDQ4YTZkMzgifQ"
         },
         {
-          "resource": "repo://src/app.rs#L534-L542",
+          "resource": "repo://src/app.rs#L580-L588",
           "version": "repo-lines-v1:sha256:df493befb56dd10251865172577fc5c0bae10fe73f070b409398d5734657fd5f:eyJzZWxlY3RlZExpbmVDb3VudCI6OSwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiMjVhYmM1NWJiNzAyNjM3YTU5MGU3ZjU3ZjVlNGQ1ZDI3MTc5NmYzNmY4YWQxNzM5ODI3NWZlZThlOWY3ODM4NCIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDg5M2Y0MGUzNzgxZWI4OTZjODJkMDQ2YTA4MDY3YTExZTM1Y2RmMmYxZGZhZjI2ZmVhOTBiOGRiNjY3NjQxMyIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiOTk5YmRlZmVlYWNhNjM0MGUwMjkwZGM4NmU4ODljODlkNWUxODlhYTViYTQ0YjU4NWIzYjcwNzM0ZjRhNjJkOCIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiMGZkOTUxZjMzMzEyYjBkYjE5OTA3M2NkMjM0MDgxYzczMzI0MjFmNjBmNGM2ZDgwMDdiZTZiMzVjMzA4NjE0ZSJ9"
         },
         {
@@ -169,15 +169,15 @@
       "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#L634-L679",
+          "resource": "repo://src/app.rs#L680-L725",
           "version": "repo-lines-v1:sha256:15f93324b5c3304cece9ae39892bf0620866af782fe276ac9cb827e11f6c89cb:eyJzZWxlY3RlZExpbmVDb3VudCI6NDYsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjIxMGU4Y2Q4NGRkNGVlMzUzOTU2ODAxODRjZTNlYTEwZDlhMTI3Mjg1Yzk2MDVkY2RmNDJiZjkwOWJkMDZlNjYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjZjMThiNWFhYjhlMGE5MGZhY2E2ZTI5ZmMwYjdmN2NiZDQ0NWFhN2MxZmQyNzA2YjQ4M2ViMDU0ZjJkNjU1ZTkiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjM5YzEwY2I0YTNhZmI3NWE4N2VmNmE4MmZmYzVhMjBiZDZiN2U0ZmQ0NDRhMmNjMWRlMGZmZDg3MjljODQzZGUifQ"
         },
         {
-          "resource": "repo://src/app.rs#L868-L880",
+          "resource": "repo://src/app.rs#L914-L926",
           "version": "repo-lines-v1:sha256:7ecf503d147e02720d6b1181aff0c941477154a712db724f4f85db692d7aadbd:eyJzZWxlY3RlZExpbmVDb3VudCI6MTMsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImIxNjE1NThmYWRiMWZlYjc4NjlhNTk4NzEzNzMxNTk4YTMyM2Y1NWZiNTFhOGY0NTA3OTAyOTlmZWE5ODljM2UiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjI0M2Y0MzIxMzM1MDhkOTFhODZlMWI0Mjc2Y2YyMjgwN2MyMzhlMDIxZDk3ZTJiNDFmY2U0NDU2YTIzMzZiMzQiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjNhMzhmYjI3YWI5N2YyMTc1ZGQ0MDc1NDhmMTcyMGUxNzMwYzY4YjA5ZGI1M2MzZTZiZTQ4OGQyYjMzMzMxZmYifQ"
         },
         {
-          "resource": "repo://src/autoreply.rs#L231-L239",
+          "resource": "repo://src/autoreply.rs#L237-L245",
           "version": "repo-lines-v1:sha256:e449aa643a18bfc13c65470f8160c3d49e5fc14fad1a09a5a427b0337ee2c354:eyJzZWxlY3RlZExpbmVDb3VudCI6OSwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiNDc3NWNlNjg3YjU5YjZlZTJkYjRhOThlZTgwOGIzNDEwNThmOWRjNTY4NDMxYjg2NDA2Y2NhMjVkMzdkNGM2MiIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDg5M2Y0MGUzNzgxZWI4OTZjODJkMDQ2YTA4MDY3YTExZTM1Y2RmMmYxZGZhZjI2ZmVhOTBiOGRiNjY3NjQxMyIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiMzZjYmYyODkwNTIyYmM3ZmQ4YzgyM2M4NjczYThjZDcyMDFjNWE4NDI4Nzg0ODRjNjI2NjljMWNlYmNhNWRkYiIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiZjZhMWQ3MTgwMWZlMmYwNzkwNjkwZDA3NjczMjAxODY5MDRhODZhYmQzNGNhMTk1ZmMwZjhkYzJkYjlhMzc0OCJ9"
         }
       ]
@@ -187,11 +187,11 @@
       "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 audios, and reports the count in the status.",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L924-L981",
+          "resource": "repo://src/app.rs#L970-L1027",
           "version": "repo-lines-v1:sha256:c43af5df86c402afe0390574efa8d5408228a57e12985a9f712e1691b2f8c63b:eyJzZWxlY3RlZExpbmVDb3VudCI6NTgsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjJiYWY4NjRiNzJmMjM1YjJhNzAxNzllOWRiZTlkYTg1ZmY0NWM1OTg4YjY1MTUzNmQyYjNmMDM0MzRkMTA3YjgiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjJkOWY0YzAxNDlmZTYwZWNjMDY0NDU2Y2VhNTgwNWIyMmI4YjgzZTljYzY0YzVmZTc3YzhjZDNjMzQ5ODg1OWMiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6Ijc4OTU5NGE5NzIzN2Y2NjVkMmU0Y2RhMTIyZmY1NTIyNzJjMjVmZTFhMTI0NmZjMGNkNjllMDQ4MjE2ZTNlYWMifQ"
         },
         {
-          "resource": "repo://src/autoreply.rs#L278-L290",
+          "resource": "repo://src/autoreply.rs#L284-L296",
           "version": "repo-lines-v1:sha256:978337272e69a0817dc2c97ec4cf141555c1153662f4d02558b1f314701d2555:eyJzZWxlY3RlZExpbmVDb3VudCI6MTMsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjMwOTdhNjIzNjEzOTc1ZTZhZGVhNzc4Y2ZiZjAyY2RhY2ZmMjIxMzBlZWNlMTA1NGY5MTZiMjY4MmE4MjYyYWMiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjlmOWZkZjU2MjAyM2ZlMGUwZTk0ZDE3YmZiNjg5NzA1YzM4ZGU5YTcyOTE2ZDMzNzhjNzAzNTAwYTFmY2E2MjAiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjhjYTUwMTNjY2ZiYWFhNGY4YTdiMmRlYTU1MDU2NTRhN2VmN2ExNGVmNTZhZGI3NzEyNGM2Y2NkMWVhMWNjN2UifQ"
         }
       ]
@@ -201,11 +201,11 @@
       "statement": "The AI context is loaded with messages_since from local midnight of yesterday, capped at CONTEXT_MESSAGES.",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L991-L998",
+          "resource": "repo://src/app.rs#L1037-L1044",
           "version": "repo-lines-v1:sha256:7c77ded5ac6d33c39416f329b08cebb428d4745fd351bf74af8942d0b7a7ca02:eyJzZWxlY3RlZExpbmVDb3VudCI6OCwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiNTUxNGM3ODU1Zjc2MTlkMWNmNzRmYjkxNzViOTNiMDc1ZTAwMGFjNTA0MjFlMWRmMWQ0Nzc4NWQ3NDkxYTdmOSIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDg5M2Y0MGUzNzgxZWI4OTZjODJkMDQ2YTA4MDY3YTExZTM1Y2RmMmYxZGZhZjI2ZmVhOTBiOGRiNjY3NjQxMyIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiMWJhZTkzZGRjNWYwMDFlOWRhMTI5YmY0ODA5MzQ3MGI1ZWQ4ZjNjNzdjYzA2MjQ2YjJjZTkxODdmMTIzNGI0ZCIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiNmZlMTlkZGI5ZjIxNTk1NWQzNzM5NjBlODk1MTI1ZGE0ZmYwYTYwNTAxMGY1ZmVmYWExYTI2ZDE0NWQ4NmNkMiJ9"
         },
         {
-          "resource": "repo://src/autoreply.rs#L459-L480",
+          "resource": "repo://src/autoreply.rs#L473-L494",
           "version": "repo-lines-v1:sha256:50ee4ad69b117378616ca21b79b698134c88777e95ce91db1351b2411baa697f:eyJzZWxlY3RlZExpbmVDb3VudCI6MjIsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImZkNDcwYzIwOWRjNTFhNjVkMGE4MzYzYjE3ZmI5NGI2MTZiOTMwNjU4MTIxZTc3ZjFmMTcwZGU2OWEzMGZiYzEiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjAzYTEwMzdmMDcxOTYxZmIzYjEzYTdkYWZmNjlhY2MyYmFiODEwZmU4YmRhMTZhMmIyMjcxNTU0MzcxNGNjNzUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjIwYzRkZTBiYjA5ZGM2YTUzMmVjOGUxOGQ0ODFjMGQyZWJjMDA3ODNlMzhiODk4MDMyNzJiMmY4ZTMwOTJhMzIifQ"
         }
       ]
@@ -215,7 +215,7 @@
       "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#L251-L265",
+          "resource": "repo://src/autoreply.rs#L257-L271",
           "version": "repo-lines-v1:sha256:3500de003e15b84a4a4ea0d82efa712d262694653367bfa7e0ba301b46eb3133:eyJzZWxlY3RlZExpbmVDb3VudCI6MTUsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImVhYjdlMDgxZWM2YWFmNTMwMmViYzE1MWJiYTc0NGYxZTI5MGU5NzllNGQxN2Y4YjQ3NjMzNmZhNjk2NzQyNGUiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjY0ZDMxNTc4NWMxODA1YWU3YzEzYzUwZWEyZjhhN2E2ZTcyYjNiMWVhOGQxNjgxNDIyMzYxYWZlNGRjNDIwMzEiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjJmYmE5MjI2NzJlMWM1MjY1NTMwZjBkYTNlN2RlZDBmOWQxZjUxNjY4YjJlMWMyZWI2MThlYWEzM2VlZmZkMDMifQ"
         }
       ]
@@ -225,7 +225,7 @@
       "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#L1427-L1444",
+          "resource": "repo://src/app.rs#L1501-L1518",
           "version": "repo-lines-v1:sha256:05cde78eab7927a8b5c33ce971f85c0e210ce60936eada88f8c9481585c1519e:eyJzZWxlY3RlZExpbmVDb3VudCI6MTgsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjlhMjY2MjVlMDNiMmE0MDg1NDg0YjQ2NjUzOTllOWRmOWFhZDQzYmVkNGQxNWFkODEwODMzMGIyMDhjNmM5ZTYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6Ijk5OWJkZWZlZWFjYTYzNDBlMDI5MGRjODZlODg5Yzg5ZDVlMTg5YWE1YmE0NGI1ODViM2I3MDczNGY0YTYyZDgiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImU2ZWQ1MmZlYjhjZTliMjNiYmNiYTkwMGFkYjk0MzE3MjA4ODNhZTk0ODY4NmQyNjhlNmRkMjFlNjljYTU3YjkifQ"
         },
         {
@@ -239,11 +239,11 @@
       "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 media received meanwhile.",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L1696-L1698",
+          "resource": "repo://src/app.rs#L1770-L1772",
           "version": "repo-lines-v1:sha256:f837e4630ef0ee6fe89e07bf810f5851838ae48e227e0659b648d4ea480392fa:eyJzZWxlY3RlZExpbmVDb3VudCI6MywiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiNTk5ZjgxMThmMmE5ODIzYzNmZDQwNDcyYjlhMmUwNDE1ZTRhZGQzMWQ5NTkwNjE0NzY3MzlhYjk4YWIyNTUxMCIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDEyY2EzNDVjY2Y3NWJmOWMwODA2YmNlNjk1YmU4ZGU4MDhiNzk5ODQyNTFhN2E1NGQyMDJjZjYxMDFkZDQ1MSIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiNGQ4NDkzOWY4ODU5NjI0YmY1NGNiOTdjYWJjMDE2ZjRhYmY3OTU0YWUwNGU0MTVmODUzMjQ3ZTRmYWY2Zjc3ZCIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiNDlhNTk5ZGY5ZDUwOWVhMzBiNDMzZjkzYjY4ZjE4ZGQ1ODY2ZDVmMGJjMGMzMGEyMTU1OWFkN2U4MWM1ZmMwMCJ9"
         },
         {
-          "resource": "repo://src/app.rs#L837-L852",
+          "resource": "repo://src/app.rs#L883-L898",
           "version": "repo-lines-v1:sha256:ca8594a3de60f6943b74187223fab353c0af7a724cf3ef68f1d2f06141510c59:eyJzZWxlY3RlZExpbmVDb3VudCI6MTYsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjNmN2EzMzI2Mzk4MGZiOTQ4OGI5NDcyZTI4ZGU0NjE4ZWVjMzc3MTE2ZmFmZDViODZlNmUzYTZhZGVjYjM2YTEiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjRjZTljYzQ1ZjFjOTkzN2IyMGMyZmJjOTM3YmUxN2QxYzhhMmU0MTdlMjUwMDI2ZjM4MzU3N2MyY2ViNGRkN2UiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImNmMGY4ZWU3NGMyYmNhMjZlNTlmY2NmYjUzZmQyYWI2OGJjNzIwZTY2MGZiZjM3M2IwZjFhNTY4ZmIyNjBjOTUifQ"
         }
       ]
@@ -253,11 +253,11 @@
       "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#L29-L41",
+          "version": "repo-lines-v1:sha256:a7df1e8151fbc9518136df20c28d749dbefa6d916debc172216697f30e6a8fd6:eyJzZWxlY3RlZExpbmVDb3VudCI6MTMsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQyMDQ3NjYzODdjNzE0ODkzYjdiZTA0YTBjMzdiM2RiYmMyM2MyMGU3YjJmMzgzZWFiNjc3M2RlMzQyMDMxNmIiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjBmZmRmOTQ2MGJlNGM0MjliMWVmYzAxNGM4MzdlY2I3ZWQxZTY0ZTQxMjIwMTQzYWY2NmM2MGM4ZmIzNzAxNjciLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImY1NDhiZWY1MzdjMDk5NjEzNDNhYzg0YTM5MTI2Nzk5NDhhMDQ4M2UxZWNiYmNmNzBiNTc5ZGZjNDljNjYzOTIiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImMzMjk0YzZiYjNiNjMzNThkNGEwOWI1ZGZiYzM4ZDljZmFkMzU4ODU0YzI4MzhkY2M2Mjk5ODQzYWZiMDFkNjkifQ"
         },
         {
-          "resource": "repo://src/autoreply.rs#L336-L379",
+          "resource": "repo://src/autoreply.rs#L344-L387",
           "version": "repo-lines-v1:sha256:d5ff8e0615e8b48256c14bd7751292dd94626fd4c21d40b36d73c5e2f3cb8740:eyJzZWxlY3RlZExpbmVDb3VudCI6NDQsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjZjNDVmNTRiYjMyMGQzN2U2ZjZmZGJjYTUzMjBlMmYyNjI1ODJmODkwNzlhYmY1ODJjNjI4MjgzOTM5Zjg2ZjciLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImY5YWU5MTBkNzg0M2M3YWNhZWUxMGFhMDhhMGE2ZGRhYzFhZjhjMWExMjc5ZDQ2MTRhOGNhODViOTgxMmIwODkiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjQwNWQ4YWU3NjdiNjQ4YzZmM2Y4ODE1ZDZjODQ1MjFiOTQ5MWVmZTEyNmRiYTVmZjU0MWIyMmJjNWY3YjllNWQifQ"
         }
       ]
@@ -267,16 +267,16 @@
       "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#L1009-L1028",
-          "version": "repo-lines-v1:sha256:0a937f62450210fb1b4a45c2eb392c8533cab8fc5e7f2ca88caec3ba64669dbb:eyJzZWxlY3RlZExpbmVDb3VudCI6MjAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImZkMjA2ZWQ4NGMzNGNhMWE0ZmNmZmQ1NmFlOTBiNDgyZDIyNGM2OWQ0ZmRlMDY4MmI2MTllODNlMDRlNWJhYjYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjEyZTA5MzVlNDEzNWQ5MWEyZGFlMDUxOGE3ZTY5NjQ0MmIxYmM5ZGYxOThiNDA1ZmVmMGQzMjUwY2VmOWU1ZWQiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImQwMjI2ZGQ4YmZkY2JkMDhlNjUxMjUzOGY4MmU5YWNiOTEyNzg2NTgwNDEwMmQ2YjRlMzQ4NmRmMjU4YjMxNmYifQ"
+          "resource": "repo://src/app.rs#L1056-L1077",
+          "version": "repo-lines-v1:sha256:861a0689d2b7c1c987c57b17528c56c211837f1c8e9b48cd0f48336dd10a790a:eyJzZWxlY3RlZExpbmVDb3VudCI6MjIsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImZkMjA2ZWQ4NGMzNGNhMWE0ZmNmZmQ1NmFlOTBiNDgyZDIyNGM2OWQ0ZmRlMDY4MmI2MTllODNlMDRlNWJhYjYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjA5OTBjZDRiNWVmNTk5ZGQ4N2ZhYTJjMzdjOWQyODMxZjY1ODFlNmQ0MTI3ZjE2OTAyY2U4ODZkZTQ5Y2JmMDciLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImQwMjI2ZGQ4YmZkY2JkMDhlNjUxMjUzOGY4MmU5YWNiOTEyNzg2NTgwNDEwMmQ2YjRlMzQ4NmRmMjU4YjMxNmYifQ"
         },
         {
-          "resource": "repo://src/autoreply.rs#L159-L170",
+          "resource": "repo://src/autoreply.rs#L165-L176",
           "version": "repo-lines-v1:sha256:50d1a6dfe10b9769d40a33924bcb0bf9d4aa264c3e7775b149a95cc7fa629e6e:eyJzZWxlY3RlZExpbmVDb3VudCI6MTIsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjM4NmEyNGE3ZTg2NTVkY2M2ODIzMDg0NGM1ZGNjZTA5ODgzNDU1MzFjOTZlNjdjZTIxOTZkMjU1Mjg3NGM2MTMiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImRiZjcwNGZiMzY2MTg3NDdmNjJlYTQ1M2MxMDg3ZGNiMWVjNDZlYmYxNmJjNjFmMDg5MWZjMjhkZGNiMDgzYTIiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImY4OGMyOWJiOTczMDg1Yzk0N2E2NWRmM2MxMjU0NTI3ZTA0MGFiZWFlMmM2MzM3Mzc3ODA2NjA4NDYwNjVhMmIifQ"
         },
         {
-          "resource": "repo://src/autoreply.rs#L438-L457",
-          "version": "repo-lines-v1:sha256:9a7559f07b9cf4dc7eb7bab1c174e1b73cd3a491c63f7dcb65449c9d15f42438:eyJzZWxlY3RlZExpbmVDb3VudCI6MjAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjFlNmM4N2FkOGEyMmJiNTMzZmQ5ZjQxZDk2ZTkyNTFkNGUxNTZlY2Q2YjIwNjcxYzVhZmYxZDM2ZTk3YTVjOWIiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImU5NmY4Y2IwODNmYTgwMGE0ZGNkMjk2OTcyMmQ3YjkyNjg2YWRkOWViZTM2NTU0OTJmYTY3MmIxYTdjZWQyNDIiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjZhZjExM2E5ZmE3NTZlZTI4ODYzNGUyZTYzOWM5M2Q0OTJkOTRlMGMyZjRmZWE0ZTJiYjFlZjA3Y2EwOGFmOGUifQ"
+          "resource": "repo://src/autoreply.rs#L452-L470",
+          "version": "repo-lines-v1:sha256:f29ce1456f6f024610ce3451390a2628cdfaa7dd0c5dd12018ebc6aa2f78a1ab:eyJzZWxlY3RlZExpbmVDb3VudCI6MTksImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjFlNmM4N2FkOGEyMmJiNTMzZmQ5ZjQxZDk2ZTkyNTFkNGUxNTZlY2Q2YjIwNjcxYzVhZmYxZDM2ZTk3YTVjOWIiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjRlY2UxMWZmY2E4ZmJlZmViODgwZjI2MmI2MTJlZWM0Yjg5MTc5NGZkMWU4YTY2NjY2MDgxYjQyYWZhYmM2N2IiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImU5NmY4Y2IwODNmYTgwMGE0ZGNkMjk2OTcyMmQ3YjkyNjg2YWRkOWViZTM2NTU0OTJmYTY3MmIxYTdjZWQyNDIiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImY0MjhjZjIyZDEyNTUyYWNhNmU1NWVjNzM2ZDQyMWExYjA4ZmIwZGY4NzU3Y2Y4MjIwMTJkMDMxYWY0Y2NhYjMifQ"
         }
       ]
     },
@@ -285,11 +285,11 @@
       "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#L1700-L1708",
+          "resource": "repo://src/app.rs#L1774-L1782",
           "version": "repo-lines-v1:sha256:a271b225aba65e2765d3d7b1ab0f8a02114cdf60cf3a5298066b80ba40c06576:eyJzZWxlY3RlZExpbmVDb3VudCI6OSwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiZmI5YzZkZjEyNGI2NDgxODMyZDk5NTgyMjRmNzFjYjRjZDJjM2Y3YTU3Njc1OWQ0OGM5NjBkZDNmZTE4OTliZCIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDEyY2EzNDVjY2Y3NWJmOWMwODA2YmNlNjk1YmU4ZGU4MDhiNzk5ODQyNTFhN2E1NGQyMDJjZjYxMDFkZDQ1MSIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiZjk2YWY2OWU3OGEwNDBlNTAxNjM2YjMwNDhkNGVjNTQ2Yzc0OTU1ZTEyN2IyY2M5OTU4MDFiZDI2YWM2NjZkZSIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiOTViMzliZDc5NmMyYjVjYThiYjhjMWEzNjUzODk5NzY5MGE0OTE5ZDY5NzVlNGMxMGZiMWE1ZGE3MGU1Mjc3OSJ9"
         },
         {
-          "resource": "repo://src/app.rs#L738-L770",
+          "resource": "repo://src/app.rs#L784-L816",
           "version": "repo-lines-v1:sha256:49bdb09f74d399910bca1c218286bf995bae8e8457efe83cb97034814f6d67d5:eyJzZWxlY3RlZExpbmVDb3VudCI6MzMsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImJkOGRiMTAzNzg3MWI3NGU1OTVmYTZhZThkNjAwNTE4NDIyNDNkNzJkODZkNDM2OGUyZGU1OWI2MTdkYWM2NmIiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImJhMGM2MzVlMzlkMTFmNTgyOGE1Mzk1MzhkYjE0ZTdlNzU1YWFiMzlhZDNhNzljYmM0ZTFmZTQ4ZDgyZGZmYzIiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjBhN2YyNDA4YTExOTJiY2VmZTI0ODJhNTg2MGM1MTVhMmQ4NGIxMzQzZTE1ZDdmMGVmNGRhMTA0YzY1ZTJiM2EifQ"
         }
       ]
@@ -299,7 +299,7 @@
       "statement": "Each media download (image or audio) 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#L1737-L1784",
+          "resource": "repo://src/app.rs#L1811-L1858",
           "version": "repo-lines-v1:sha256:b0f4c6798d97128a8721a067c8b836f5c71583de7b58ebdd10d60b683f0e944e:eyJzZWxlY3RlZExpbmVDb3VudCI6NDgsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjJmNWJhZjEwNWExMDViNTk0MjMyNjIwYzMxMGFlZWJmOTYyNzMwMjIzMmY2OTVhNGM4ZWQ4NmRlMzI1YmVmN2YiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6Ijk0M2IxZmMzZTY5ZTA3NDk5NTEzYjJlZTRmNTYyZGMwZWM0MTkzN2EwZTVlNTQwYWFhODAwNTAyOWEzMWU0ZWYiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImU4MzE5MzEzYWQ5OGM5ZTYxMjViMjE3ZTNhMWVlNDc4MzQ4N2EwNWYyMGI5ZjQ1MmFiYzM2OWQ0MDBmODMxNDkifQ"
         },
         {
@@ -313,7 +313,7 @@
       "statement": "When the configuration files change, the reloaded configuration replaces the current one and the AI client is rebuilt, while replies already running keep the previous client.",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L417-L434",
+          "resource": "repo://src/app.rs#L457-L474",
           "version": "repo-lines-v1:sha256:11076b2b498b6d19b7a0629886f2cdb37229105049de848490f4e1d9faead710:eyJzZWxlY3RlZExpbmVDb3VudCI6MTgsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjVhYTEzNjY2OGNiMzc5NzgyYTUwMTY4ZDNlOWM4Njg4MmM5YTBhZTAxOGQwNTZiNTEzZWU2ODA5OWYxNjc3YzYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjVmMjAxZDg2ZGM3NzY0YWFmOTA5MGNhNDRkODcxNDM3Y2JiODk4ZGE5ZWJlMGU0ZDhlNjVlNDZiZDJkZGVkYjQiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjAyNjE2ZDk0ZDJmZDg4NWViMDZhMGFmZjVkYjQ1YWUyOTdiMWQ5NzViYzU0MjRlZGIyYWUwZmE2NzZkMzE0ZTYifQ"
         },
         {
@@ -321,10 +321,32 @@
           "version": "repo-lines-v1:sha256:39a3d9ca14f3c3010ef0a2fadf3e6a8ecf8749be831215006c0808392cb62813:eyJzZWxlY3RlZExpbmVDb3VudCI6MjYsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImQ3NGMwOGQzMzg5NGUyMWQyYzBmNTMyMDkwYmQwNjFjNDNjMzg1ZDU1MzkwMmI0MDkwZDkxZjJjZDY3M2VjMWQiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjE1Y2VkZDkyMzY4ZGI1MDMwYjUxMjQ3ODcwODlmY2FmZTI5YzhmOGQ4MjExZDA0MTE4ZmM4MTViYWM1MDBjOWEiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImI4MTU5NjA0YjAyZjI1MGMwMzAwZTljNDQ5ODlmMzMyYjA4N2RjN2UxODAxZGI5Y2IyMmM2ZWM5YzdmMjUyZjIifQ"
         }
       ]
+    },
+    {
+      "id": "claim_4f2a611ca9724dc695eab88785604e8e",
+      "statement": "start_reply passes today's local date, formatted by today_label as dd/mm/yyyy, to the system turn, which then contains the line 'Oggi è <date>.' right after the SOUL.md prompt in both normal chats and the self-chat; an empty date (unresolvable local time) omits the line.",
+      "evidence": [
+        {
+          "resource": "repo://src/app.rs#L1066-L1073",
+          "version": "repo-lines-v1:sha256:471b36477292b80954d5676400af2fe9d587622bf05569430909329eab31b024:eyJzZWxlY3RlZExpbmVDb3VudCI6OCwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiYjRlOTM0MDVjYjg2NmUxZjI1N2U1MTliZmM2NTcxMGZiYWE4ZDY2NWQ3NTlhMWNlODFmMTAxNDYzZDA4ZTk2NiIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiYzVjZGVmODA4NWFlNzRhYjYzNmQ2NDU5ZjVjNmQ1ZmJmYjBkYjI2NGYxYjBkNzViYWM1MTljOWIzZjY3MTdhYyIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiNDc3MTQwNzkwYjI5YzQ0NzNiNWE1OWE4MWM4MDIxOTRkMmFjZTA3N2RmOTI5YzFjNDY5OGYyZmQwZjRlZTlkZiIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiNzVjYTY2YWQwMDA5OGQ0ZTc3YzE2MzM3M2UwNWNmZmM4M2YwYTVlZmY4ODkxODM0ZjliMGQyNWZiYmY0NzMzYyJ9"
+        },
+        {
+          "resource": "repo://src/autoreply.rs#L429-L450",
+          "version": "repo-lines-v1:sha256:239ac0647e61654099b2ded7ef2421495ce1a43e4009d9d4edb2da55b0105707:eyJzZWxlY3RlZExpbmVDb3VudCI6MjIsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjkxMWU4YjYyZTMzYmM0ODQxYzg2YTUyYmJmZDQyNGFlNDFlMGJiZWE4MjQyMmQxMGUwZDIwYjlmMzA1ZmI0MGMiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImMxYWI1Y2IzMzk1OGUzM2FiM2VlMGY2NjEyZjA5ZWQ5YzhhOGZjMmYxZTY2NWZlMDljNDljYmI5M2NjM2Y3NTkiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjU0YmExZDg0YTZiMTczZGU4YzBmOGU2M2VlODM1MWE5MTZkZTJhM2VhZWJmMWFkMGNmMDViYTE3YmJjNjViN2MifQ"
+        },
+        {
+          "resource": "repo://src/autoreply.rs#L49-L53",
+          "version": "repo-lines-v1:sha256:49f702e8bf33b82a9043beef7df1bae0c3a6ac85f2cdd66c258d64eb69734dae:eyJzZWxlY3RlZExpbmVDb3VudCI6NSwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiZmI0NGIyZmFmOTFlMzMwYmNhM2M1ZGY5NDBiMTlkOWRlODc1MzA5YTExMzc0Yjk0ZDU2YjgzN2ZhNzliYTcyMiIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiMTA5YWNiODQ0YWIzODRjNzU1ZmZlNTlkMjdiODM3MDIyZDkxNWJiZWM3MGZlN2Y3NzY4OWZmMGE0Y2IxZmIxMiIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiOWFlYzlkMzMxMmU3ODZlNGQzZmRhMDJhNmFiMDMxZGJkNTE4NGNhZTRkYTFjMGM0OTY4YTViZWQyNjcwN2E0MyIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiZDQyYmNkNzZjM2RhZTFiZTkxMjY5MWFlMjNkNzQ2OWNiNTdjNTY0MjIwM2M5NjhkZjZiYTdjYWE5NTI2NzRjYSJ9"
+        },
+        {
+          "resource": "repo://src/autoreply.rs#L496-L504",
+          "version": "repo-lines-v1:sha256:09923dabc8705a7632eb9839bef45c7a3904cfa871fcb7c2f6826b3202dd7d17:eyJzZWxlY3RlZExpbmVDb3VudCI6OSwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiMjViYTQzZDg2MzlhYTJhOTQzNDRkYjkxM2E0YTNmYWRiMDJjMTcxMzU1MzdmMzQ0MzY2MzkzMmZkZTgzMWUxZiIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDEyY2EzNDVjY2Y3NWJmOWMwODA2YmNlNjk1YmU4ZGU4MDhiNzk5ODQyNTFhN2E1NGQyMDJjZjYxMDFkZDQ1MSIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiNjc0MmY0Yjg1ODVjYzZkNmMyYWI3N2JhNWY0ZWEyZjQ5ZDE3MWIzNGY1YTEyN2ViYzg3NTZlZTM5ZDY5MjhkZCIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiMjBjNGRlMGJiMDlkYzZhNTMyZWM4ZTE4ZDQ4MWMwZDJlYmMwMDc4M2UzOGI4OTgwMzI3MmIyZjhlMzA5MmEzMiJ9"
+        }
+      ]
     }
   ],
   "verification": {
     "by": "openwiki/0.7.2",
-    "at": "2026-10-24T16:39:50.206Z"
+    "at": "2026-10-25T12:54:53.684Z"
   }
 }

+ 42 - 24
openwiki/.claims/workflows/relink-and-pairing.json

@@ -1,17 +1,17 @@
 {
   "schemaVersion": 1,
-  "pageVersion": "sha256:4b312458a35b3e82b48821c6414d376325ab935da8d66022b63d67277491f3e9",
+  "pageVersion": "sha256:0033245928a8e464561a73b6aec4301ea2d0c9ccb6ffeea2847a0684680cda63",
   "claims": [
     {
       "id": "claim_5f458a6b32c4452a9d25edf481e1f07e",
       "statement": "F8 is handled on every screen: the first press asks for confirmation and the second starts the relink, while any other key cancels the pending request and an Esc used to cancel is consumed.",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L356-L381",
+          "resource": "repo://src/app.rs#L430-L455",
           "version": "repo-lines-v1:sha256:b64194da9c7bce682333ce6e12c758fef9076e9a0af5e51f088b07063965a086:eyJzZWxlY3RlZExpbmVDb3VudCI6MjYsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjI2OTEwMzllOGFhMzZiZmZmZmI3OTMyMjRhMjA3MWIwYmRlZmE0Mzg3OGNlYmE0MDJiMmY1NDlhODczYjNjMDgiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImM4MDg4M2RjYzQ1NWNiNGUwODk3NDkxNzMwZmVhYmQzOWM3MTRhYTRmYTUzZTc4MzA1YThkN2ViMWQyZjYxZmYiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjBlZjIyYTA2NDVkNDAwY2EyNWVmYzI2Mjg2ZTdjMDZmOWUzZmZhZmQxY2UwMzVjMmZjOTkyYWZhNGI2NGIxY2EifQ"
         },
         {
-          "resource": "repo://src/app.rs#L884-L903",
+          "resource": "repo://src/app.rs#L1171-L1190",
           "version": "repo-lines-v1:sha256:dd7415d7eb098a51dfb451afc950bc0918baaa5c2b9d01f64bd1354a87ccdb76:eyJzZWxlY3RlZExpbmVDb3VudCI6MjAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjlhZGRmNjgzMjdhYTA4ZWZjNDYxNGFmZDIwOTkzZTFkNzZiZjUxNmQ3NzRjM2QyMmIzOTUyOTEwNmNkOTBhZmMiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6ImU2YjA0NDkxNDY5NzIyZTgzNjNhOTRlNDA0NDUzOGUwMTNhMmEyMTdlMzZiMWU1NjhiYTYwYWNiN2I0YzhmMmUiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjE2MmVlNTdlNDFhOGMyMGFlMDE2NGRmMjYxNDM5ZGNjNDk5ZDM1MjA4MzQxNTE1MTZkMDQ3ZThkZWFlOGE0NjYiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImE2NTVlZGVmNDZhYzg4YTI3NGFkODVmNWQ2NmRhYTgyYTZkZDg3ZGY4MWQwOWNiMTIxNzVjNDFkNWY5YWY1MTgifQ"
         }
       ]
@@ -21,26 +21,26 @@
       "statement": "The web command Relink calls start_relink directly, without the TUI double-press confirmation; the browser page asks for confirmation before posting /api/relink with the X-Wh-Request header.",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L1144-L1151",
+          "resource": "repo://src/app.rs#L1434-L1441",
           "version": "repo-lines-v1:sha256:6a5fafc027a4dad94444764670c73f86302a4de5f80873b8c2731496881881a1:eyJzZWxlY3RlZExpbmVDb3VudCI6OCwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiOGY4NmU3NjA1MmFiZTAzYjI3OGY0NjA0MzNmOTJhNjFlOWE2ZmM2YTA2NzA0Mjg4NGQ3MjJjNmVjNmMwNWEwMiIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiZTZiMDQ0OTE0Njk3MjJlODM2M2E5NGU0MDQ0NTM4ZTAxM2EyYTIxN2UzNmIxZTU2OGJhNjBhY2I3YjRjOGYyZSIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiOTAzYWM0MTkxYWMxNTg3OGMxODk3Zjc5N2MwNTcwYzkzNmQ5ZTM4NDgyODgxOTNkMWM2ZDhhN2MzMDBkNTA0MyIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiZTc3ZDk1Mzg3NWIzYjM4OTk2YjhkOTM5Mzk5M2YyZTUxOGY1MGMzMzY1NjgzZDMxNGFjNmE5NjNhMjliOTBmMSJ9"
         },
         {
-          "resource": "repo://src/web.rs#L166-L174",
+          "resource": "repo://src/web.rs#L168-L176",
           "version": "repo-lines-v1:sha256:81e9004c1f6afdff65111c92f71cf2a8b9e2fd1f87d5d54d60e8446e7b1dd361:eyJzZWxlY3RlZExpbmVDb3VudCI6OSwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiODU5OWJkMWM5YjdhMTQ5ZjczYzQzMjQ2MjljMzczMzFmOGNlZTM1NDVhN2IzYWUwNjk1ZWM0NmFlZjBiZmJjNCIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDEyY2EzNDVjY2Y3NWJmOWMwODA2YmNlNjk1YmU4ZGU4MDhiNzk5ODQyNTFhN2E1NGQyMDJjZjYxMDFkZDQ1MSIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiNGRhMDBiODU3YTc3N2IyNzQwZmQxZjg0MWIxZWQ0MzcwNmYwOTc5YWEyNzE5Zjk1YzJjOTJjMGNkYWEzOWJkYiIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiNGNmODQ4NzQ5MjczZDdkMWJlZDQ1ZTg2ZTNmMTU3ZmY3NTE0MjIyMTFkMWE3YzFlM2RhZDIzYWNkODNiZWRmNCJ9"
         },
         {
-          "resource": "repo://src/web_page.html#L703-L718",
+          "resource": "repo://src/web_page.html#L787-L802",
           "version": "repo-lines-v1:sha256:edcf716f5c77bc9ba443f65a19535e9c1b254b9949a238743590df637a35157e:eyJzZWxlY3RlZExpbmVDb3VudCI6MTYsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImVhZjIyY2YwNzMyNTkyYzI0NzE0MWNiZTYxZWZhMTEwMTI2MTJmNWEzMTIyYjlhZjliYmFmNTJmNTcyNGQxOTQiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6Ijc2NTNiYWEyY2QxMjM0ZDlmZTgzOGNiZTJhN2EzODc1YTcyMWMxOTRmNGRjMjZiZTgwYjZlOTU5MGY2YzE3MmIiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjU4MjJhZjlkZGY3ZTI1ZjQxYTFmZjU1NDA2MDljN2FhNGNhZmZjODBkMjhjM2VjNjkxY2ExY2RmOTMxMzQ0MDMifQ"
         }
       ]
     },
     {
       "id": "claim_61c1cf660cdb4769a895097a3dc5d711",
-      "statement": "start_relink ignores requests while a relink is running; otherwise it shows the connecting screen, clears the QR, resets own_jids_known and spawns a task that runs WaHandle::relink and reports AppEvent::Relinked.",
+      "statement": "start_relink ignores requests while a relink is running; otherwise it clears wa_connected, shows the connecting screen, clears the QR, resets own_jids_known and spawns a task that runs WaHandle::relink and reports AppEvent::Relinked.",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L320-L341",
-          "version": "repo-lines-v1:sha256:562a22627a48012be000f4fe8e09f389e6e78bb3e5f7b9930922352ad69710bc:eyJzZWxlY3RlZExpbmVDb3VudCI6MjIsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImIzZDgxYmQ3NmJjMThmZDc2YTBlMzE3M2FiZTI5Yzc5ZDZiMzYzZGUxMjhkZDEwOGI5ZDg4M2Q2OWVlNGJlNGQiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjM0YjVjYjQyNzgzMDViZTExYTkyOWY2MjYzOWFkYjMzMzI3NjczYTdjZmVmZDgxNTBjOGNhMjcwMmJiZjE5N2YiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImI5ZWM4YTdlN2ExNmVjZmZhNzMzMzI3ODJjM2M4YzRlNjNkYjczODcxNzQ5YjVlMjhkMWNlMmYyOTZmZmYyYmMifQ"
+          "resource": "repo://src/app.rs#L392-L415",
+          "version": "repo-lines-v1:sha256:f078f59f008936d5dc1d8f800ec00972712811c68abfe76e95c5ef3d2d727720:eyJzZWxlY3RlZExpbmVDb3VudCI6MjQsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6Ijg5MTExNTQ5YTZkMjMyZjJiN2NjMTMxMDNhZjA5ZjM2ZDExNTFlMjRmNTU3MjJkNGQ1YmIzNDBkODNhZTNhZGMiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6Ijk5OWJkZWZlZWFjYTYzNDBlMDI5MGRjODZlODg5Yzg5ZDVlMTg5YWE1YmE0NGI1ODViM2I3MDczNGY0YTYyZDgiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImI5ZWM4YTdlN2ExNmVjZmZhNzMzMzI3ODJjM2M4YzRlNjNkYjczODcxNzQ5YjVlMjhkMWNlMmYyOTZmZmYyYmMifQ"
         }
       ]
     },
@@ -49,11 +49,11 @@
       "statement": "WaHandle::relink stops the old client (logout, wait up to 10 seconds, abort the translator), deletes session.db and its side files or empties its tables when the file is locked, and starts a new unpaired client that sends its events to the same channel.",
       "evidence": [
         {
-          "resource": "repo://src/whatsapp.rs#L182-L237",
+          "resource": "repo://src/whatsapp.rs#L221-L276",
           "version": "repo-lines-v1:sha256:18f2dfb379587e6702f10be30e023ea43187acf5f2e74eee0646e34c148de96c:eyJzZWxlY3RlZExpbmVDb3VudCI6NTYsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjYwMzUyMWI1YmRmYWViNzk2MzY5ZTExMzEwODIwMjIwZTNhMTI1MGUwY2E5NzIyMGEzYTI1OWFkMjQxNTcwZGIiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6Ijg3NzllYzNlN2JjNmZkMzdlNDYxMTdhODY2ZDA2YWIzZTVlNGVlYmE0ODJjYmViYzkzZjhjZGRjZjYyOTM5ZTAiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6Ijk5OTU0Y2ZjYmI3ZmVlMDA0ZTg0M2I4ZDdmZjAxNTA3ZTlmOGU5YTYzNWI1MjZlOWQ1ODRmNzY1MDA4YjRiZDYifQ"
         },
         {
-          "resource": "repo://src/whatsapp.rs#L314-L367",
+          "resource": "repo://src/whatsapp.rs#L393-L446",
           "version": "repo-lines-v1:sha256:dae8e9fd58393c76159603a1143da95f40e9d77d62eaa8e9531737a4a58b8edb:eyJzZWxlY3RlZExpbmVDb3VudCI6NTQsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjgyNTI5M2Y3MjNjNGMzMjZmNDJlMDFmMzc1Nzk1ZTJjMjE4NzQxNGEyNjI5NDEzOTEwODc3Mjc3MzdlMWMzMDEiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjYwNjNjNGQxYWY4Y2Q4OWJiMGJkZDc2NTdmZmNiZjY4MDZhNjEwZDc5ZDZjOGQ0YzMwN2MyNDFkZjM5MDA1MjIiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjI1MjJlMWI3MWMwMDczNGUwMWUwYTJhN2ZjYjU1ODdjYWEwN2RkYzRlZjRjMTViYTEyNGFhN2M1ZWRlZTVjYzUifQ"
         }
       ]
@@ -63,11 +63,11 @@
       "statement": "While no session is running during or after a failed relink, sends, image downloads and own JID lookups fail or return nothing because WaHandle::client reports 'WhatsApp session not running'.",
       "evidence": [
         {
-          "resource": "repo://src/whatsapp.rs#L200-L210",
+          "resource": "repo://src/whatsapp.rs#L239-L249",
           "version": "repo-lines-v1:sha256:8ed098e34276e2c4b9ff74e786b647375c2d873d70640b609b0ae4bca9505394:eyJzZWxlY3RlZExpbmVDb3VudCI6MTEsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjEwYjNlNTU4ZDczYzgzNWI1NWVkY2U4NWUxNTMzYjM1ODczM2U1OWU4ZGNmYTJmZTM3N2FmNDBjZTcyZjY4OTUiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjE5ZTc2MzU1Y2YxMTk5NDgwYzNmOTk4MzZiMTQ4NmM4NDFkODkxZmFhZDkwNGM2MDhlMDkzMDIyZDM3NTdhZWUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImE3M2Y4MjFiY2U5NjYxNDdhMDEyMmRjYjVhZGExZmVjZjgxMWIyZmNiNjIwZDUwZGFiY2U2MTg0OTUzNjVhNWUifQ"
         },
         {
-          "resource": "repo://src/whatsapp.rs#L279-L295",
+          "resource": "repo://src/whatsapp.rs#L358-L374",
           "version": "repo-lines-v1:sha256:2716d1991bf1de21e1b70d5a1702f8e881af4787f4afd11837ef8767c63b07b3:eyJzZWxlY3RlZExpbmVDb3VudCI6MTcsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImE2OGU3Zjk2MjU1MTJjM2E1NzQ5NDIzZDA2MDc0ODRhZjQxODc4NThmZDhkNjhhMTcwMWQ1ZWI1ODA3MWU4ZTkiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImFkMjVmMDBjOGVlOTdlZDMzZGJiMTNiMzNiYmRjOWEyNjhjMzhkODZmMjlkYTQyYTg1YjBkN2U0NGE0YTYxNzciLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjFiY2ZkYmJhNTAwZmIyZWEyMTlhZTdiNDQ0NmUzZTU1NTA4YWViMWQ3ZjdjYmM4MGYxNTUyNjMzMzk5ZGY1MWMifQ"
         }
       ]
@@ -77,12 +77,12 @@
       "statement": "A relink resets only the WhatsApp session database; the history database with chats, messages and autoreply flags is not touched.",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L320-L354",
-          "version": "repo-lines-v1:sha256:f986a6b8380806257f348dc108fa11b5fbf0d7fca2d638d33cdedb965da8a9b6:eyJzZWxlY3RlZExpbmVDb3VudCI6MzUsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImIzZDgxYmQ3NmJjMThmZDc2YTBlMzE3M2FiZTI5Yzc5ZDZiMzYzZGUxMjhkZDEwOGI5ZDg4M2Q2OWVlNGJlNGQiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjM0YjVjYjQyNzgzMDViZTExYTkyOWY2MjYzOWFkYjMzMzI3NjczYTdjZmVmZDgxNTBjOGNhMjcwMmJiZjE5N2YiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImU1ZTg0YjQyYzc5NDMxZTNkOWI4Mjk4MGQwN2Y5NWQxMTBiYjQ1ZGUxZTI0NjlkYjcxMDJlNzJmODRiM2FjOWIifQ"
+          "resource": "repo://src/app.rs#L392-L428",
+          "version": "repo-lines-v1:sha256:94409c183f9efdd624cdf8192ed517ed1be590487358efc49e4df2c5e4c18e1d:eyJzZWxlY3RlZExpbmVDb3VudCI6MzcsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6Ijg5MTExNTQ5YTZkMjMyZjJiN2NjMTMxMDNhZjA5ZjM2ZDExNTFlMjRmNTU3MjJkNGQ1YmIzNDBkODNhZTNhZGMiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6Ijk5OWJkZWZlZWFjYTYzNDBlMDI5MGRjODZlODg5Yzg5ZDVlMTg5YWE1YmE0NGI1ODViM2I3MDczNGY0YTYyZDgiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImU1ZTg0YjQyYzc5NDMxZTNkOWI4Mjk4MGQwN2Y5NWQxMTBiYjQ1ZGUxZTI0NjlkYjcxMDJlNzJmODRiM2FjOWIifQ"
         },
         {
-          "resource": "repo://src/whatsapp.rs#L221-L237",
-          "version": "repo-lines-v1:sha256:63e137b22805f72a6e35c74bf1f9296c30d5dd1c696101d7b7db2f3dedb3b664:eyJzZWxlY3RlZExpbmVDb3VudCI6MTcsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjY4YjU3MmE2ZGZjZGNjZDc1YmE0ZDBkZDZiYmEzNjUwY2UwNDFkZDkxYzRjNjE1YTBmMDJhNDllMjNlNTA2OWQiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImIyNTdmMTI1NGQ0NjJmZjgyYTFmYTFmZjYyNzFlNzIzNTM1NTFkMDZlNjMxMDQ2OWYxMGZmZWUxZTY2ZmVlMjMiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6Ijk5OTU0Y2ZjYmI3ZmVlMDA0ZTg0M2I4ZDdmZjAxNTA3ZTlmOGU5YTYzNWI1MjZlOWQ1ODRmNzY1MDA4YjRiZDYifQ"
+          "resource": "repo://src/whatsapp.rs#L227-L243",
+          "version": "repo-lines-v1:sha256:39ca51764322f937c48570452b7b34acbff999b31779884a1137b4d295b19a9a:eyJzZWxlY3RlZExpbmVDb3VudCI6MTcsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjczOTlhZTU1YjExMzQyZjkxMTFmNjE0MTM1YWZkNjAxYTg3ZTQ0YmY5YTI1ODY1OTBiNWY0MmIwYmU1MTc0NjIiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjYwOGNiOWM4OThmMmE2YzA2MGM4ZWIxMGU4YTAwY2IxYTEzNjYyYjVmOTg5NTk2Zjc2ZDBjMDJiYmViNGIzZDkiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjgwYTVlOTNkNjIzNWZhNGI0ZDZjNjNhMmZlNzJiMjZkYzcyYmJiNDBhYzdiNzI5ZjYyNjgwMjcyYjkxZDBkNzgiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImE0MDMyZmY2NWFjNjFiNWY0NDBiZDY0YTU4YzZkYjFlZjk2ODkxYWJjMjI5OTkyMWI3ZTgwZmU3MGJjMzdjMmIifQ"
         }
       ]
     },
@@ -91,22 +91,40 @@
       "statement": "New pairing QR payloads switch the app to the Pairing screen and are exposed both to the TUI and, rendered as SVG, to the web panel state.",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L1171-L1184",
-          "version": "repo-lines-v1:sha256:643e53e7eee86239879c37d679254899bc317cdbf3cd55b8314e04ba1657043c:eyJzZWxlY3RlZExpbmVDb3VudCI6MTQsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImIzN2M0MzFhZmQ5NGMyNzU0NzJkNjliODNlMGM0MmRmMjU0NmQ2MDQ3NmM4ODg3Yzk3MDRmZWM2Y2U0MDkyMDMiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImRkZDFkZTJkMmJiNWE5ODAxZDE4MTAyYTQwYzlmOTc4NjRkNjNkYmEzMzkzMzdiZDVlYTU3Y2FiOGU4YTYwYzgiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImJlODM0YjUzNzA2ODgzYmQzMmMxMmViNmQ0OGEyMmFjZjlkNTIyYzIzOWJlNTE5NDZmNzA5YjIxZDNkNjIwMzIifQ"
+          "resource": "repo://src/app.rs#L1486-L1499",
+          "version": "repo-lines-v1:sha256:3aa43674de3e4e7b89e84724d3034c2a649dcbb71fccfc1ecd34eecf63c78af9:eyJzZWxlY3RlZExpbmVDb3VudCI6MTQsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImIzN2M0MzFhZmQ5NGMyNzU0NzJkNjliODNlMGM0MmRmMjU0NmQ2MDQ3NmM4ODg3Yzk3MDRmZWM2Y2U0MDkyMDMiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImRkZDFkZTJkMmJiNWE5ODAxZDE4MTAyYTQwYzlmOTc4NjRkNjNkYmEzMzkzMzdiZDVlYTU3Y2FiOGU4YTYwYzgiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImJlODM0YjUzNzA2ODgzYmQzMmMxMmViNmQ0OGEyMmFjZjlkNTIyYzIzOWJlNTE5NDZmNzA5YjIxZDNkNjIwMzIifQ"
         },
         {
-          "resource": "repo://src/app.rs#L406-L411",
-          "version": "repo-lines-v1:sha256:b1ff41a223bdf9bbf36663f8088fa2379ee28283923bdb2f0cbbc4f5adbdcaee:eyJzZWxlY3RlZExpbmVDb3VudCI6NiwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiNmQ4Y2YwMmQ4NTY5ZjVhNDdmNGI2ZjRmNzkzMGY3OTMxNzMzZWY0YTg0MzA4M2ZiZmMzMDM2Y2IzYmIyNzkwZCIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNTUyMmM0MmNkMTljNmQwM2Q4MDhjNDUyNzIwNTUyODI1YjdlYTFjNWI0OTFiMjE4NTExY2FjNzQxNWNmOWQ5YiIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiYzk0OTJlYzlhMzZlOGQ4NTQ2MTUyYjgyMmQ1N2ZkMjAwOTg5MTUwMzBhMjBkOWJkYTFkNWZhZDQ0ZjhmMWI1ZiIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiMTYxODU0MTVhYzdlMzU2MTg4MWU4NTViMTEwN2Y3NDYzNjIwZTI5NjM3MWI0NWU4ODMwNGNhMjYxODY0MDYxZCJ9"
+          "resource": "repo://src/app.rs#L480-L485",
+          "version": "repo-lines-v1:sha256:b1ff41a223bdf9bbf36663f8088fa2379ee28283923bdb2f0cbbc4f5adbdcaee:eyJzZWxlY3RlZExpbmVDb3VudCI6NiwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiNmQ4Y2YwMmQ4NTY5ZjVhNDdmNGI2ZjRmNzkzMGY3OTMxNzMzZWY0YTg0MzA4M2ZiZmMzMDM2Y2IzYmIyNzkwZCIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNTUyMmM0MmNkMTljNmQwM2Q4MDhjNDUyNzIwNTUyODI1YjdlYTFjNWI0OTFiMjE4NTExY2FjNzQxNWNmOWQ5YiIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiYzk0OTJlYzlhMzZlOGQ4NTQ2MTUyYjgyMmQ1N2ZkMjAwOTg5MTUwMzBhMjBkOWJkYTFkNWZhZDQ0ZjhmMWI1ZiIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiMjgzMzk3YjI3OGI0ODVlMzBhNTE4YjhhOTQwYjIxOWRlMTgwOTM4NmY3ZDFhMmVjMDIyYzkyOGJiZDgxMzljNSJ9"
         },
         {
-          "resource": "repo://src/web.rs#L364-L379",
-          "version": "repo-lines-v1:sha256:e6d58dec7c8a45140e76290e175a7b2f5a9b3c59f60b3e244064813974a3ed1d:eyJzZWxlY3RlZExpbmVDb3VudCI6MTYsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjMxYjQ2MDBlYjQ4M2JkNTEzNjA1ZTE0ODVkNmRjNmNjMDA3NjJkNzRiODNjNzg4OTQ2MGQzOWNkMGRjMzQ1YTgiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjE5ZTc2MzU1Y2YxMTk5NDgwYzNmOTk4MzZiMTQ4NmM4NDFkODkxZmFhZDkwNGM2MDhlMDkzMDIyZDM3NTdhZWUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjQ0NGIyYWRlMGYzNzRmOGZjZGYxNGM5MTQxYzNkNTMxYjcxMzgwN2ZkZTc0NzI1NTZiOWJlNDI1MjE5NzM4OWYifQ"
+          "resource": "repo://src/web.rs#L366-L386",
+          "version": "repo-lines-v1:sha256:56689af36fb87e205a1284ed6d20706d1f6804419cf4c3a55160df7b54fe7e02:eyJzZWxlY3RlZExpbmVDb3VudCI6MjEsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjMxYjQ2MDBlYjQ4M2JkNTEzNjA1ZTE0ODVkNmRjNmNjMDA3NjJkNzRiODNjNzg4OTQ2MGQzOWNkMGRjMzQ1YTgiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjljOWJlNmEwNDhiNjgyZDAxYmYwNzBjZDE1NGQ2M2RmMmI2MjJkZGVmMDRlZjc2YmYxMTE4M2E0YTgwMjEwMDAiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjE5ZTc2MzU1Y2YxMTk5NDgwYzNmOTk4MzZiMTQ4NmM4NDFkODkxZmFhZDkwNGM2MDhlMDkzMDIyZDM3NTdhZWUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImQ2OWY1NjcyZmMyMTZhMzJiMDk3OTc1NGExZDUzNDViOTYwMzBhYmFjNjE3N2VmMzkxMjNmNDcyNjk1MTlhNmMifQ"
+        }
+      ]
+    },
+    {
+      "id": "claim_6fb0da24125a4da581d80a1380188894",
+      "statement": "After pairing completes, Connected sets wa_connected and the WhatsApp client schedules an unavailable presence 5 s later (and again after SelfPushNameUpdated) so the phone keeps its notifications; LoggedOut clears wa_connected and returns to the connecting screen.",
+      "evidence": [
+        {
+          "resource": "repo://src/app.rs#L486-L503",
+          "version": "repo-lines-v1:sha256:9cb6624c69f34219abc6d01db1b1ec172c9175fe1543c71db877060197c8ca7c:eyJzZWxlY3RlZExpbmVDb3VudCI6MTgsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjJhMGVmOWE3MTY5OGU1ZTI3NjVhODk3NzUxZDI5MTNiNTFkMzViNTljODY2ZWNkNDA2MWI0YmI2NGYyNDY1M2UiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjU1MjJjNDJjZDE5YzZkMDNkODA4YzQ1MjcyMDU1MjgyNWI3ZWExYzViNDkxYjIxODUxMWNhYzc0MTVjZjlkOWIiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImZmYTc3NmFhMmIyNzIzNmY4MmE3YWNlMTkxNTJlYzdhOTkzNmM2N2I2OTRhMjg1MTZiMWVmZmY1ZGE4MzM2NDMiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjgzMTcyOTllN2IzOGE0MTQ1NGY2OGQwNGU0YWQ4ZmRmNWEzZTI4OTY3ZGVkYThhNmFjNGM3Zjk4ZjNkZmYxZGUifQ"
+        },
+        {
+          "resource": "repo://src/whatsapp.rs#L674-L678",
+          "version": "repo-lines-v1:sha256:c808a4b2f40500fb739cedcd414ec736ee2d5195bc46d5a5ee91bafa896bc005:eyJzZWxlY3RlZExpbmVDb3VudCI6NSwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiOGFhNWE2ZjgzMWMwNGVhMTQwYzE0N2MzMGVlOTMxNmIxMGU2ZDVkNmVmZjM2MTNhZWI1ZDYwYjY3MjlmNDM0MSIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiMGVhMDI1MTA1NjU0ZWJiNjRkNzMzMzA5MzMzMDI1OWVmNDViZjUzNjExYzY4NGIyOGQwMTI4MjVhODE2NWZhMyIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiNjk2OWZhNmNiMzIyNmMwN2U0NWI0MTNkNWU2NWNiNjc4ZTVmMGRiZDM2NTkwY2FkODBiMWFjYTE3NmZkYWI1MSIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiMDI5ODI1NGMwYmYzYmIxOGViZTBkOTg2NWMzY2RkMGI5ZTQwYjNiMWIwMjgwMWQyOWMxYjlkYmQzNmNjNDA0YSJ9"
+        },
+        {
+          "resource": "repo://src/whatsapp.rs#L719-L741",
+          "version": "repo-lines-v1:sha256:325ad7da5f94c5e2c3a2ce9b22ca961b2e5247222fb5434ab20779ea988122cb:eyJzZWxlY3RlZExpbmVDb3VudCI6MjMsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImIwMWU1OGYwZTM3MzU5ODEzMWEwMjYyNTA4NjhkYmNlNTIyYTc1NjM1OTgwMTljYTk0OWI4ZGI5NzJlOTU0OTEiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6Ijk5OWJkZWZlZWFjYTYzNDBlMDI5MGRjODZlODg5Yzg5ZDVlMTg5YWE1YmE0NGI1ODViM2I3MDczNGY0YTYyZDgiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImM2ZTYxOTdhNjY3YmI1ZjgyYzI4ZDdmYWM2OGIzYWE1YzU1YTk5NmQ2NGRkYmYyOTI3ZWQ3YjAyNzk5MDI0NWYifQ"
         }
       ]
     }
   ],
   "verification": {
     "by": "openwiki/0.7.2",
-    "at": "2026-10-23T21:26:24.156Z"
+    "at": "2026-10-25T12:54:53.684Z"
   }
 }

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

@@ -1,8 +1,8 @@
 {
-  "updatedAt": "2026-10-24T17:14:13.422Z",
+  "updatedAt": "2026-10-25T13:04:31.329Z",
   "command": "update",
-  "gitHead": "5c2fe04925ebbc998a25ef3427b9228fe16c419c",
-  "model": "deepseek-v4.1-flash:1017",
-  "status": "interrupted",
+  "gitHead": "38b73843b3ba2506f3fb725b5e332f69c377554e",
+  "model": "host-agent/claude",
+  "status": "complete",
   "language": "en"
 }

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

@@ -2,53 +2,53 @@
   "schemaVersion": 1,
   "pages": {
     "/openwiki/architecture/app-state.md": {
-      "pageVersion": "sha256:37292ba995a3a70e902fd75c163ea0039a8724324712200d9fa529205b98e321",
-      "completedBy": "openwiki/0.7.2",
-      "completedRunId": "16d279c7-60b4-4a76-84ef-65ff9d091a51",
-      "gitHead": "dc69f5fc7cd2bf7d732e4ccc8ff34398a48d2d2b",
-      "sourceFingerprint": "sha256:d4dd3e842183a686e381a839ed385a71d14b3f62e2199d238c83b5e38549d98d"
+      "pageVersion": "sha256:147e2ba13ec4a1f2727c1e580a0935503d6213e3aabb9bce1cf520e31c348e78",
+      "completedBy": "claude-code",
+      "completedRunId": "40b6b468-52b1-49c0-b0af-ece2c5f7c913",
+      "gitHead": "38b73843b3ba2506f3fb725b5e332f69c377554e",
+      "sourceFingerprint": "sha256:2a42afb58abb858537c2d57d1b16cbd056378f0ff8600fa2ec330101287435c2"
     },
     "/openwiki/architecture/runtime-and-event-loop.md": {
-      "pageVersion": "sha256:1c12bcb75120e45d7940b31833bbe96fe727146b35af7e2e965eff9da4c51980",
-      "completedBy": "opencode",
-      "completedRunId": "96dd7fcb-e901-4ad4-a53d-a3a9e5f45e4d",
-      "gitHead": "dc69f5fc7cd2bf7d732e4ccc8ff34398a48d2d2b",
-      "sourceFingerprint": "sha256:d4dd3e842183a686e381a839ed385a71d14b3f62e2199d238c83b5e38549d98d"
+      "pageVersion": "sha256:73780810fe3cd1b10a3d3d3a8fb56a8c9f3079e97a27a9bffda63c40a108fef4",
+      "completedBy": "claude-code",
+      "completedRunId": "40b6b468-52b1-49c0-b0af-ece2c5f7c913",
+      "gitHead": "38b73843b3ba2506f3fb725b5e332f69c377554e",
+      "sourceFingerprint": "sha256:2a42afb58abb858537c2d57d1b16cbd056378f0ff8600fa2ec330101287435c2"
     },
     "/openwiki/architecture/terminal-ui.md": {
+      "gitHead": "dc69f5fc7cd2bf7d732e4ccc8ff34398a48d2d2b",
+      "sourceFingerprint": "sha256:d4dd3e842183a686e381a839ed385a71d14b3f62e2199d238c83b5e38549d98d",
       "pageVersion": "sha256:b917b7bf26bb55ba79b12fcf2c7cfe5936d6463a6bd9c6bfe7eed55735c000d3",
       "completedBy": "opencode",
-      "completedRunId": "96dd7fcb-e901-4ad4-a53d-a3a9e5f45e4d",
-      "gitHead": "dc69f5fc7cd2bf7d732e4ccc8ff34398a48d2d2b",
-      "sourceFingerprint": "sha256:d4dd3e842183a686e381a839ed385a71d14b3f62e2199d238c83b5e38549d98d"
+      "completedRunId": "96dd7fcb-e901-4ad4-a53d-a3a9e5f45e4d"
     },
     "/openwiki/concepts/history-store.md": {
+      "gitHead": "dc69f5fc7cd2bf7d732e4ccc8ff34398a48d2d2b",
+      "sourceFingerprint": "sha256:d4dd3e842183a686e381a839ed385a71d14b3f62e2199d238c83b5e38549d98d",
       "pageVersion": "sha256:9d1f1a31451538c0c3bbc043ab35ce90cbffd32e26263470d8b43f7dc2486279",
       "completedBy": "openwiki/0.7.2",
-      "completedRunId": "16d279c7-60b4-4a76-84ef-65ff9d091a51",
-      "gitHead": "dc69f5fc7cd2bf7d732e4ccc8ff34398a48d2d2b",
-      "sourceFingerprint": "sha256:d4dd3e842183a686e381a839ed385a71d14b3f62e2199d238c83b5e38549d98d"
+      "completedRunId": "16d279c7-60b4-4a76-84ef-65ff9d091a51"
     },
     "/openwiki/integrations/ai-client.md": {
+      "gitHead": "dc69f5fc7cd2bf7d732e4ccc8ff34398a48d2d2b",
+      "sourceFingerprint": "sha256:d4dd3e842183a686e381a839ed385a71d14b3f62e2199d238c83b5e38549d98d",
       "pageVersion": "sha256:4927c894dab42fbe05e52e939703259666aa47947ef7945c623bfa2116a02e4d",
       "completedBy": "opencode",
-      "completedRunId": "96dd7fcb-e901-4ad4-a53d-a3a9e5f45e4d",
-      "gitHead": "dc69f5fc7cd2bf7d732e4ccc8ff34398a48d2d2b",
-      "sourceFingerprint": "sha256:d4dd3e842183a686e381a839ed385a71d14b3f62e2199d238c83b5e38549d98d"
+      "completedRunId": "96dd7fcb-e901-4ad4-a53d-a3a9e5f45e4d"
     },
     "/openwiki/integrations/web-control-panel.md": {
-      "pageVersion": "sha256:deebb3a40f405864bed483e185086a069b8cd23cd1464a3e108c07917ca9b04c",
-      "completedBy": "opencode",
-      "completedRunId": "96dd7fcb-e901-4ad4-a53d-a3a9e5f45e4d",
-      "gitHead": "dc69f5fc7cd2bf7d732e4ccc8ff34398a48d2d2b",
-      "sourceFingerprint": "sha256:d4dd3e842183a686e381a839ed385a71d14b3f62e2199d238c83b5e38549d98d"
+      "pageVersion": "sha256:428fec496886d09cde951dc002e97021c4d9fc425843d101d1d5d17cc397bcc6",
+      "completedBy": "claude-code",
+      "completedRunId": "40b6b468-52b1-49c0-b0af-ece2c5f7c913",
+      "gitHead": "38b73843b3ba2506f3fb725b5e332f69c377554e",
+      "sourceFingerprint": "sha256:2a42afb58abb858537c2d57d1b16cbd056378f0ff8600fa2ec330101287435c2"
     },
     "/openwiki/integrations/whatsapp-client.md": {
-      "gitHead": "dc69f5fc7cd2bf7d732e4ccc8ff34398a48d2d2b",
-      "sourceFingerprint": "sha256:d4dd3e842183a686e381a839ed385a71d14b3f62e2199d238c83b5e38549d98d",
-      "pageVersion": "sha256:4bfc94bcec7d0629fe535e916bfc05dd51ef04c1fe94a8b004f82f7f5cfb98d9",
-      "completedBy": "opencode",
-      "completedRunId": "96dd7fcb-e901-4ad4-a53d-a3a9e5f45e4d"
+      "pageVersion": "sha256:b6ea56337534d7b40808dce1aa32c6121c8c75af2f67d1860b5246e54b4d346b",
+      "completedBy": "claude-code",
+      "completedRunId": "40b6b468-52b1-49c0-b0af-ece2c5f7c913",
+      "gitHead": "38b73843b3ba2506f3fb725b5e332f69c377554e",
+      "sourceFingerprint": "sha256:2a42afb58abb858537c2d57d1b16cbd056378f0ff8600fa2ec330101287435c2"
     },
     "/openwiki/operations/configuration-and-data.md": {
       "gitHead": "dc69f5fc7cd2bf7d732e4ccc8ff34398a48d2d2b",
@@ -65,25 +65,25 @@
       "completedRunId": "96dd7fcb-e901-4ad4-a53d-a3a9e5f45e4d"
     },
     "/openwiki/testing/unit-tests.md": {
-      "gitHead": "dc69f5fc7cd2bf7d732e4ccc8ff34398a48d2d2b",
-      "sourceFingerprint": "sha256:d4dd3e842183a686e381a839ed385a71d14b3f62e2199d238c83b5e38549d98d",
-      "pageVersion": "sha256:50c64fbc1c28a488dc62c88f83e98a06ce449b71661690714c90854f832928e9",
-      "completedBy": "opencode",
-      "completedRunId": "96dd7fcb-e901-4ad4-a53d-a3a9e5f45e4d"
+      "pageVersion": "sha256:87e88f2284955c6a45419d031922bce94b2d30c29040e151fd42811374151dc0",
+      "completedBy": "claude-code",
+      "completedRunId": "40b6b468-52b1-49c0-b0af-ece2c5f7c913",
+      "gitHead": "38b73843b3ba2506f3fb725b5e332f69c377554e",
+      "sourceFingerprint": "sha256:2a42afb58abb858537c2d57d1b16cbd056378f0ff8600fa2ec330101287435c2"
     },
     "/openwiki/workflows/ai-autoreply.md": {
-      "gitHead": "dc69f5fc7cd2bf7d732e4ccc8ff34398a48d2d2b",
-      "sourceFingerprint": "sha256:d4dd3e842183a686e381a839ed385a71d14b3f62e2199d238c83b5e38549d98d",
-      "pageVersion": "sha256:51f277c84ac8b779ca6c470fcddd420be0463aa6c621620207ab047c32ec60ae",
-      "completedBy": "opencode",
-      "completedRunId": "96dd7fcb-e901-4ad4-a53d-a3a9e5f45e4d"
+      "pageVersion": "sha256:90d3dc1c89e757fdbd947c7d787a70192a1f423ea5575b17fd0b0cb5cfe6e505",
+      "completedBy": "claude-code",
+      "completedRunId": "40b6b468-52b1-49c0-b0af-ece2c5f7c913",
+      "gitHead": "38b73843b3ba2506f3fb725b5e332f69c377554e",
+      "sourceFingerprint": "sha256:2a42afb58abb858537c2d57d1b16cbd056378f0ff8600fa2ec330101287435c2"
     },
     "/openwiki/workflows/relink-and-pairing.md": {
-      "pageVersion": "sha256:4b312458a35b3e82b48821c6414d376325ab935da8d66022b63d67277491f3e9",
-      "completedBy": "opencode",
-      "completedRunId": "96dd7fcb-e901-4ad4-a53d-a3a9e5f45e4d",
-      "gitHead": "dc69f5fc7cd2bf7d732e4ccc8ff34398a48d2d2b",
-      "sourceFingerprint": "sha256:d4dd3e842183a686e381a839ed385a71d14b3f62e2199d238c83b5e38549d98d"
+      "pageVersion": "sha256:0033245928a8e464561a73b6aec4301ea2d0c9ccb6ffeea2847a0684680cda63",
+      "completedBy": "claude-code",
+      "completedRunId": "40b6b468-52b1-49c0-b0af-ece2c5f7c913",
+      "gitHead": "38b73843b3ba2506f3fb725b5e332f69c377554e",
+      "sourceFingerprint": "sha256:2a42afb58abb858537c2d57d1b16cbd056378f0ff8600fa2ec330101287435c2"
     }
   }
 }

+ 140 - 105
openwiki/architecture/app-state.md

@@ -1,17 +1,17 @@
 ---
 type: architecture
 title: Application state and event handling
-description: How the App struct in src/app.rs owns the application state and routes every AppEvent (keys, WhatsApp events, AI replies, image descriptions, audio transcriptions, reply timers, web panel commands, configuration reloads, relink outcomes), including the image and audio bookkeeping maps and sets, function-key handling, the reply orchestration order and the background tasks.
-tags: [app, state, events, keybindings, tokio, web-panel, relink, config-reload, transcription]
+description: How the App struct in src/app.rs owns the application state and routes every AppEvent (keys, WhatsApp events, AI replies, image descriptions, audio transcriptions, reply timers, web panel commands, configuration reloads, relink outcomes, avatar fetches), including the image and audio bookkeeping, the web panel avatar cache, function-key handling, the reply orchestration order and the background tasks.
+tags: [app, state, events, keybindings, tokio, web-panel, relink, config-reload, transcription, avatars]
 verified:
   - by: openwiki/0.7.2
-    at: 2026-10-24T16:42:08.550Z
+    at: 2026-10-25T12:54:53.684Z
 sources:
   - id: openwiki-source-9fec9d1af408501c9c455161
     resource: repo://src/app.rs
   - id: openwiki-source-ce0c4ea8cd3b15431fa691ce
     resource: repo://src/history.rs
-generated: { by: "openwiki/0.7.2", at: "2026-10-24T16:42:08.550Z" }
+generated: { by: "claude-code", at: "2026-10-25T12:54:53.684Z" }
 ---
 
 # Application state and event handling
@@ -25,7 +25,7 @@ and [web panel](../integrations/web-control-panel.md) activity.
 
 ## Events
 
-`AppEvent` (`src/app.rs:117-161`) is the single message type of the main
+`AppEvent` (`src/app.rs:125-171`) is the single message type of the main
 channel:
 
 | Variant | Producer | Meaning |
@@ -34,15 +34,17 @@ channel:
 | `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 | `Reply::Text`, `Reply::NoReply` (the model called the `no_reply` tool) or an error string. |
-| `ImagesDescribed { target, results }` | image description collector | One description (or error) per image, keyed by message id; `target` is a `DescriptionTarget` (`src/app.rs:163-169`) holding the chat and whether to answer it afterwards. |
+| `ImagesDescribed { target, results }` | image description collector | One description (or error) per image, keyed by message id; `target` is a `DescriptionTarget` (`src/app.rs:182-188`) holding the chat and whether to answer it afterwards. |
 | `AudiosTranscribed { chat_jid, then_reply, results, images }` | audio transcription collector | One transcription (or error) per audio, keyed by message id; `images` are the images postponed until the transcriptions are stored and `then_reply` 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: `Ok` carries the new message id. |
 | `Web(WebRequest)` | `wh-web` thread | A command from the [web control panel](../integrations/web-control-panel.md) with a oneshot reply channel. |
 | `ConfigReloaded(Result<Box<Config>, String>)` | configuration watcher task | A configuration file changed: the reloaded `Config` or the load error. |
 | `Relinked(Result<(), String>)` | relink task | Outcome of a relink: the new unpaired session started, or the error. |
+| `AvatarFetched { jid, url }` | avatar fetch task | The profile picture URL of one chat; `None` when it has no picture, hides it or the fetch failed. |
+| `AvatarFetchFinished { error }` | avatar fetch task | The avatar batch ended: `None` when complete, otherwise the error that interrupted it. |
 
-`handle_event` (`src/app.rs:319-351`) dispatches each variant; after an
+`handle_event` (`src/app.rs:352-390`) dispatches each variant; after an
 `AiReply` it always calls `finish_reply` to end the chat's pending state, and
 `AudiosTranscribed` is routed to `handle_audios_transcribed`. Only fatal
 history (SQLite) errors propagate as `Err`; every other failure is turned into
@@ -50,13 +52,13 @@ a `status` text shown in the footer.
 
 ## State
 
-`App` (`src/app.rs:208-267`) exposes UI fields read by the renderer: `screen`,
+`App` (`src/app.rs:228-292`) 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`
 (defined in `src/autoreply.rs`), the WhatsApp handle and a clone of the event
-sender, plus the reply bookkeeping:
+sender, plus the reply and web panel bookkeeping:
 
 | Field | Purpose |
 |---|---|
@@ -71,20 +73,24 @@ sender, plus the reply bookkeeping:
 | `pending_delete` | the chat awaiting a deletion confirmation |
 | `pending_relink` | a first F8 press awaits the second one that confirms the relink |
 | `relinking` | a relink is running; further relink requests are ignored until it ends |
+| `avatars` | in-memory cache of web panel profile pictures by chat JID (`CachedAvatar`: URL or `None`, plus fetch instant) |
+| `avatar_fetch_running` | an avatar fetch task is running (at most one at a time) |
+| `wa_connected` | the WhatsApp client is currently connected; gates the avatar fetch |
 
-- `Screen` (`src/app.rs:171-177`): `Connecting` → `Pairing` (QR shown) → `Main`.
-- `Focus` (`src/app.rs:179-184`): `List` or `Input`.
-- `ListMode` (`src/app.rs:186-191`): `Chats` or `Contacts`.
+- `Screen` (`src/app.rs:191-197`): `Connecting` → `Pairing` (QR shown) → `Main`.
+- `Focus` (`src/app.rs:199-204`): `List` or `Input`.
+- `ListMode` (`src/app.rs:206-211`): `Chats` or `Contacts`.
 
-`App::new` (`src/app.rs:269-317`) takes an `AppDeps` (`src/app.rs:193-199`) —
+`App::new` (`src/app.rs:299-345`) takes an `AppDeps` (`src/app.rs:213-219`) —
 the configuration, history, WhatsApp handle and event sender — 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, an empty filter, no open chat, no pending deletion or relink and empty
-image/audio bookkeeping, and creates the `ReplyGate` with the current time, so
+mode, an empty filter, no open chat, no pending deletion or relink, empty
+image/audio bookkeeping, an empty avatar cache with no fetch running and
+`wa_connected` false, and creates the `ReplyGate` with the current time, so
 that backlog messages are never auto-answered live.
 
-`build_ai` (`src/app.rs:1874-1887`) returns no client when the system prompt
+`build_ai` (`src/app.rs:1953-1966`) 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
@@ -94,7 +100,7 @@ il prompt in SOUL.md` — and reports a client build error with the
 
 The [configuration watcher](../operations/configuration-and-data.md#live-reload)
 sends `ConfigReloaded` when `config.toml`, `SOUL.md` or `VISION.md` change.
-`apply_reloaded_config` (`src/app.rs:417-434`) keeps the current configuration
+`apply_reloaded_config` (`src/app.rs:462-474`) keeps the current configuration
 and shows `Configurazione non ricaricata: <error>` when the load failed;
 otherwise it replaces `config`, rebuilds the AI client with `build_ai` (replies
 already running keep the client they were started with) and shows
@@ -104,17 +110,20 @@ delays, retries and `transcribe_*` settings without a restart.
 
 ## WhatsApp events
 
-`handle_wa_event` (`src/app.rs:436-463`):
+`handle_wa_event` (`src/app.rs:480-509`):
 
 - `Qr` → switch to `Pairing` and store the QR payload.
-- `Connected` → switch to `Main`, clear the QR, status `Connesso`, refresh own JIDs.
-- `Disconnected` / `Error` → copy the text into `status` only.
-- `LoggedOut` → back to `Connecting`, clear the QR, status `Sessione scollegata`.
+- `Connected` → set `wa_connected`, switch to `Main`, clear the QR, status
+  `Connesso`, refresh own JIDs.
+- `Disconnected` → clear `wa_connected` and copy the text into `status`.
+- `Error` → copy the text into `status` only (the connection flag is kept).
+- `LoggedOut` → clear `wa_connected`, back to `Connecting`, clear the QR,
+  status `Sessione scollegata`.
 - `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:465-477`).
-- `Message { message, image, audio }` → `on_message` (`src/app.rs:479-499`).
+  refresh the open chat name (`src/app.rs:515-523`).
+- `Message { message, image, audio }` → `on_message` (`src/app.rs:531-550`).
 
 `on_message` saves the message and stops immediately when
 `History::save_message` reports it was not new, so history sync replays never
@@ -123,7 +132,7 @@ open conversation and then go through `maybe_autoreply`, which keeps the
 message's image and/or audio when the reply is allowed (see
 [AI autoreply](../workflows/ai-autoreply.md)).
 
-Own JIDs are fetched by `refresh_own_jids` (`src/app.rs:1030-1042`), which
+Own JIDs are fetched by `refresh_own_jids` (`src/app.rs:1084-1096`), which
 replaces the gate's JIDs only when `WaHandle::own_jids` reports a non-empty set,
 so `own_jids_known` stays false and the refresh is retried later while the
 device is not paired (or no session is running during a relink). It is called
@@ -135,19 +144,19 @@ F8 (twice) and the web panel's `Relink` command replace the WhatsApp session
 with a new unpaired one; the full flow is in
 [WhatsApp relink and pairing](../workflows/relink-and-pairing.md). In `App`:
 
-- `request_relink` (`src/app.rs:390-403`) cancels a pending chat deletion; the
+- `request_relink` (`src/app.rs:435-443`) cancels a pending chat deletion; the
   first F8 sets `pending_relink` and shows
   `Cancellare la sessione WhatsApp e rifare l'abbinamento? F8 conferma, Esc annulla`,
   the second calls `start_relink`.
-- `cancel_pending_relink` (`src/app.rs:405-415`) clears the flag on any other
+- `cancel_pending_relink` (`src/app.rs:449-455`) clears the flag on any other
   key and shows `Nuovo abbinamento annullato`.
-- `start_relink` (`src/app.rs:353-375`) is ignored while a relink runs;
-  otherwise it switches to `Connecting`, clears the QR, shows
-  `Cancellazione della sessione in corso...`, resets `own_jids_known` (the new
-  pairing may be another account) and spawns a task that runs
+- `start_relink` (`src/app.rs:399-415`) is ignored while a relink runs;
+  otherwise it clears `wa_connected`, switches to `Connecting`, clears the QR,
+  shows `Cancellazione della sessione in corso...`, resets `own_jids_known`
+  (the new pairing may be another account) and spawns a task that runs
   `WaHandle::relink` and sends `Relinked`. The web command calls it directly,
   without the two-press confirmation (the browser asks instead).
-- `finish_relink` (`src/app.rs:377-388`) clears `relinking` and shows
+- `finish_relink` (`src/app.rs:422-428`) clears `relinking` and shows
   `Sessione cancellata: scansiona il nuovo QR` or
   `Nuovo abbinamento fallito: <error>`; the screen then follows the new
   session's `Qr` / `Connected` events.
@@ -155,8 +164,8 @@ with a new unpaired one; the full flow is in
 ## Autoreply orchestration
 
 A chat is "enabled" when the global autoreply is on **or** its per-chat flag
-is on (`autoreply_enabled`, `src/app.rs:854-860`). `ai_setup`
-(`src/app.rs:983-989`) returns nothing, and so stops every path, when the AI
+is on (`autoreply_enabled`, `src/app.rs:904-910`). `ai_setup`
+(`src/app.rs:1033-1039`) 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:
 
@@ -191,58 +200,59 @@ flowchart TD
 How a scheduled reply moves from the WhatsApp message to the AI request, with
 voice-note transcriptions always completing before image descriptions.
 
-- **Schedule** — `maybe_autoreply` (`src/app.rs:501-532`) calls
+- **Schedule** — `maybe_autoreply` (`src/app.rs:555-580`) calls
   `schedule_reply` when `ReplyGate::should_schedule` allows it: the chat is
-  marked pending and `spawn_reply_timer` (`src/app.rs:1581-1591`) emits
+  marked pending and `spawn_reply_timer` (`src/app.rs:1660-1670`) emits
   `ReplyDue` after `reply_delay_seconds`. A chat already pending gets no second
   timer, so messages arriving meanwhile are answered together. An image or an
   audio sent by an allowed sender in an enabled chat is kept by `keep_image`
-  (`src/app.rs:544-564`) or `keep_audio` (`src/app.rs:583-609`): in
+  (`src/app.rs:596-612`) or `keep_audio` (`src/app.rs:636-657`): in
   `pending_images`/`pending_audios` for a pending chat, in the backlog maps
   for an offline message before the catch-up, or processed at once without
   reply (`describe_late_image`, `transcribe_late_audio`,
-  `src/app.rs:566-626`) after it. A live image or audio always finds its chat
+  `src/app.rs:617-676`) after it. A live image or audio always finds its chat
   pending, because scheduling runs first in `maybe_autoreply`; audios are kept
   only when transcription is configured.
-- **Reply due** — `handle_reply_due` (`src/app.rs:628-641`) takes the kept
+- **Reply due** — `handle_reply_due` (`src/app.rs:680-697`) takes the kept
   images *and* audios and, if the chat is still pending, calls
-  `prepare_reply` (`src/app.rs:643-679`). `reply_context`
-  (`src/app.rs:862-880`) returns nothing when the autoreply was disabled, the
+  `prepare_reply` (`src/app.rs:702-729`). `reply_context`
+  (`src/app.rs:914-930`) 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 audios still lacking a
   transcription go first to `spawn_transcriptions`
-  (`src/app.rs:681-715`), which passes the postponed images and
+  (`src/app.rs:734-763`), which passes the postponed images and
   `then_reply: true`, and the reply follows `AudiosTranscribed`; with no
   audios to transcribe, images still lacking a description go to
   `spawn_descriptions` (reply follows on `ImagesDescribed`), or `start_reply`
   runs at once.
-- **Transcriptions** — `handle_audios_transcribed` (`src/app.rs:772-829`)
+- **Transcriptions** — `handle_audios_transcribed` (`src/app.rs:830-878`)
   removes the ids from `transcribing` and appends each transcription to the
   text of its message as `[messaggio vocale] <text>`
   (`History::update_message_text`), sets one status for failures
   (`Trascrizione vocale fallita (N): <first error>`), reloads the open chat so
   the text appears, then describes the postponed images (whose
   `ImagesDescribed` sends the reply) or starts the reply itself.
-- **Descriptions** — `handle_images_described` (`src/app.rs:738-770`) stores
+- **Descriptions** — `handle_images_described` (`src/app.rs:793-825`) 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 (or ends the pending
   state if it is no longer wanted).
-- **Start** — `start_reply` (`src/app.rs:1000-1028`) records the last context
+- **Start** — `start_reply` (`src/app.rs:1056-1077`) records the last context
   message in `answered_up_to`, marks own mentions on every context message,
-  reads the chat name and the stored image descriptions, builds the turn
-  context (self-chat flag, group flag, chat name, descriptions), builds the
-  turns and calls `spawn_ai_request`.
-- **Finish** — `finish_reply` (`src/app.rs:831-852`) clears the pending state
+  reads the chat name and the stored image descriptions, formats today's
+  local date with `today_label`, builds the turn context (self-chat flag,
+  group flag, chat name, descriptions, today's date), builds the turns and
+  calls `spawn_ai_request`.
+- **Finish** — `finish_reply` (`src/app.rs:883-900`) 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 and **keeps** the
   images and audios received meanwhile; otherwise it drops the kept images and
   audios of the chat.
-- **Startup catch-up** — `on_offline_sync_completed` (`src/app.rs:920-930`)
+- **Startup catch-up** — `on_offline_sync_completed` (`src/app.rs:970-980`)
   runs `catch_up` once per process (`catch_up_done`). `catch_up`
-  (`src/app.rs:932-962`) drains the backlog maps, asks `catch_up_chats`
-  (`src/app.rs:964-981`) for the enabled chats (all chats when the global
+  (`src/app.rs:986-1010`) drains the backlog maps, asks `catch_up_chats`
+  (`src/app.rs:1015-1029`) 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`, marks each pending and calls `prepare_reply` with
   that chat's offline images and audios (no reply delay); the status becomes
@@ -253,32 +263,31 @@ voice-note transcriptions always completing before image descriptions.
 
 Voice notes take the same route as images, one step earlier:
 
-- **Gate** — `transcription_enabled` (`src/app.rs:717-724`) is true only when
+- **Gate** — `transcription_enabled` (`src/app.rs:768-773`) is true only when
   `transcribe_key` is set (URL, model and language always have configured
   defaults). `keep_audio` returns immediately when it is not, because nothing
   would consume the kept audio, and `audios_to_transcribe`
-  (`src/app.rs:900-918`) yields nothing.
+  (`src/app.rs:952-966`) yields nothing.
 - **Selection** — `audios_to_transcribe` shares `select_media_to_describe`
-  (`src/app.rs:1647-1676`, with `descriptions_apply`,
-  `src/app.rs:1850-1857`) with images: it keeps the audios whose message is in
+  (`src/app.rs:1729-1752`, with `descriptions_apply`,
+  `src/app.rs:1929-1932`) with images: it keeps the audios whose message is in
   the AI context, skips those already being transcribed or whose text is no
   longer the plain voice placeholder (that is, already transcribed), and keeps
   at most the last `MAX_AUDIOS_PER_REPLY` (`3`, `src/app.rs:74-77`) in arrival
   order, dropping older ones for good to cap the cost of bursts. The
   equivalent image limit is `MAX_IMAGES_PER_REPLY` (`5`).
-- **Spawning** — `spawn_transcriptions` (`src/app.rs:681-715`) builds a
+- **Spawning** — `spawn_transcriptions` (`src/app.rs:734-763`) builds a
   `TranscriptionJob` from `transcribe_url`, `transcribe_key`,
   `transcribe_model` and `transcribe_language` with an empty MIME type, adds
   the message ids to `transcribing` and calls `spawn_audio_transcriptions`
-  (`src/app.rs:1786-1830`): one task per audio run in parallel, each
+  (`src/app.rs:1870-1908`): one task per audio run in parallel, each
   downloading the media with up to `MEDIA_DOWNLOAD_ATTEMPTS` (`3`) attempts,
   a `MEDIA_DOWNLOAD_TIMEOUT_SECONDS` (`60`) timeout each and growing retry
-  waits (`download_audio_with_retry`, `src/app.rs:1758-1784`), then calling the
+  waits (`download_audio_with_retry`, `src/app.rs:1839-1866`), then calling the
   OpenAI-compatible transcriber with the downloaded MIME type
-  (`transcribe_one_audio`, `src/app.rs:1832-1848`, see
-<!-- openwiki: broken internal link [../integrations/audio-transcription.md] file "../integrations/audio-transcription.md" does not exist. Fix the href or restore the target, then delete this comment. -->
-  [Audio transcription](../integrations/audio-transcription.md)); one
-  collector task waits for all of them and emits a single
+  (`transcribe_one_audio`, `src/app.rs:1912-1926`, see
+  [AI client: audio transcription](../integrations/ai-client.md#audio-transcription));
+  one collector task waits for all of them and emits a single
   `AudiosTranscribed`, a failed audio never stopping the others and the bytes
   staying in memory.
 - **Storage and statuses** — transcriptions are written by
@@ -287,7 +296,7 @@ Voice notes take the same route as images, one step earlier:
   the audio download prefix is `Download audio fallito`
   (`src/app.rs:70-71`) and the failure status is
   `Trascrizione vocale fallita (N): <first error>`
-  (`transcription_failure_status`, `src/app.rs:1710-1718`).
+  (`transcription_failure_status`, `src/app.rs:1789-1796`).
 
 ### Ordering invariant
 
@@ -303,7 +312,7 @@ message arrived after the answered one — otherwise both maps are emptied.
 ## Key bindings
 
 Commands use function keys (`src/app.rs:24-33`); `handle_key`
-(`src/app.rs:1122-1167`) dispatches them:
+(`src/app.rs:1179-1220`) dispatches them:
 
 | Key | Where | Action |
 |---|---|---|
@@ -323,12 +332,12 @@ Commands use function keys (`src/app.rs:24-33`); `handle_key`
 | Delete (Canc) | list focus, chat mode | Ask to delete the selected chat; a second Delete confirms. |
 | characters / Backspace | input focus | Edit the input line. |
 | Esc | input focus | Return focus to the list. |
-| Enter | input focus | Send the trimmed input (`submit_input`, `src/app.rs:1317-1329`). |
+| Enter | input focus | Send the trimmed input (`submit_input`, `src/app.rs:1370-1382`). |
 
 F10 and F8 are checked first; any other key cancels a pending relink (Esc is
 then consumed). Non-main screens ignore every remaining key. 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:1932-1939`)
+type into the input or the filter; `is_command` (`src/app.rs:2032-2039`)
 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 repaint the whole terminal (see
@@ -337,119 +346,145 @@ list mode with F2 also makes the event loop repaint the whole terminal (see
 ### List filter
 
 Typing while the list has focus edits `filter` (`handle_list_key`,
-`edit_filter`, `src/app.rs:1169-1211`). `visible_chats` / `visible_contacts`
-(`src/app.rs:1285-1299`) keep the entries whose name contains the filter,
-case-insensitively (`matches_filter`, `src/app.rs:1526-1532`); selection,
+`edit_filter`, `src/app.rs:1224-1263`). `visible_chats` / `visible_contacts`
+(`src/app.rs:1338-1352`) keep the entries whose name contains the filter,
+case-insensitively (`matches_filter`, `src/app.rs:1604-1608`); selection,
 opening, F3 and deletion all work on the filtered list.
 
 ### Local chat deletion
 
-`request_delete` (`src/app.rs:1225-1247`) works only in chat mode: the first
+`request_delete` (`src/app.rs:1279-1300`) 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:1249-1272`), which removes
+the same chat calls `confirm_delete` (`src/app.rs:1304-1324`), which removes
 the chat from the history (messages, image descriptions and row, hence also
 its per-chat flag; no WhatsApp call, the contact is kept), takes its pending
 reply, drops its pending and backlog images, pending and backlog audios and
 answered snapshot, closes it if open and shows `Chat <name> eliminata`. Any
 other key (Tab, F2, F3, F4, F8, list keys, Ctrl chords) cancels the pending
 deletion with `Eliminazione annullata` (`cancel_pending_delete`,
-`src/app.rs:1213-1223`).
+`src/app.rs:1266-1275`).
 
 ### Opening and toggling
 
-`selected_jid` (`src/app.rs:1462-1473`) returns the JID on the selected row of
+`selected_jid` (`src/app.rs:1540-1550`) returns the JID on the selected row of
 the active filtered list (chat or contact), or nothing when it is empty.
-`open_selected` (`src/app.rs:1446-1460`) uses it to load display name,
+`open_selected` (`src/app.rs:1524-1537`) uses it to load display name,
 per-chat autoreply flag and the last `MESSAGES_SHOWN` (200) messages, then
 moves focus to the input. The `reload_*` helpers
-(`src/app.rs:1486-1515`) keep `selected` inside the active filtered list.
+(`src/app.rs:1564-1593`) keep `selected` inside the active filtered list.
 
-`toggle_autoreply` (F3, `src/app.rs:1342-1357`) picks the target from the
+`toggle_autoreply` (F3, `src/app.rs:1397-1411`) picks the target from the
 focus — `selected_jid` with list focus, `open_chat` with input focus — and
-delegates to `toggle_chat_autoreply` (`src/app.rs:1359-1378`), which always
+delegates to `toggle_chat_autoreply` (`src/app.rs:1414-1427`), which always
 allows disabling and updates `open_chat_autoreply` when the target is the open
 chat. Enabling goes through `can_enable_autoreply`
-(`src/app.rs:1889-1902`), which first requires an AI client and then a chat
+(`src/app.rs:1968-1976`), 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:1427-1444`) refuses to enable
+`toggle_global_autoreply` (F4, `src/app.rs:1507-1522`) 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:1380-1397`) applies a `WebCommand` and then
-always sends back a `WebState` snapshot on the request's oneshot channel:
+`handle_web_request` (`src/app.rs:1434-1447`) applies a `WebCommand`, then
+calls `maybe_start_avatar_fetch` and 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:1399-1410`): if the JID is
+- `ToggleChat(jid)` → `toggle_web_chat` (`src/app.rs:1478-1484`): if the JID is
   not among the panel's chats the status becomes `Chat non trovata`,
   otherwise `toggle_chat_autoreply` (same rules as F3);
 - `Relink` → `start_relink` (no second confirmation; ignored while a relink runs);
 - `Quit` → `should_quit = true`.
 
-`web_state` (`src/app.rs:1412-1425`) returns the global flag, the toggleable
-chats (`web_chats`, `src/app.rs:1916-1930` — the TUI chat list in the same
-order, restricted to chats that support autoreply), the status line, the
-current screen as `connecting` / `pairing` / `main` (`web_screen`,
-`src/app.rs:1904-1914`) and, while a QR payload is stored, the QR code
-rendered as SVG by `web::qr_svg`. See
+`web_state` (`src/app.rs:1491-1499`) returns the global flag, the toggleable
+chats (`web_chats`, `src/app.rs:1995-2006` — the TUI chat list in the same
+order, restricted to chats that support autoreply, each with its cached avatar
+URL), the status line, the current screen as `connecting` / `pairing` /
+`main` (`web_screen`, `src/app.rs:1982-1988`) and, while a QR payload is
+stored, the QR code rendered as SVG by `web::qr_svg`. See
 [Web control panel](../integrations/web-control-panel.md).
 
+### Avatar cache
+
+Profile pictures for the web panel are fetched lazily, driven by the panel's
+own polling, and live only in memory:
+
+- **Trigger** — `maybe_start_avatar_fetch` (`src/app.rs:1456-1471`) does
+  nothing unless the screen is `Main`, `wa_connected` is true and no fetch is
+  running. It collects the JIDs of the web chats whose avatar is missing or
+  older than `AVATAR_TTL_SECONDS` (86 400 s, one day, `src/app.rs:116-119`)
+  and, when any, sets `avatar_fetch_running` and spawns `fetch_avatars`.
+- **Fetch** — `fetch_avatars` (`src/app.rs:2015-2030`) asks
+  `WaHandle::profile_picture_url` for one chat at a time, sleeping
+  `AVATAR_FETCH_INTERVAL_SECONDS` (2 s) between requests to avoid WhatsApp
+  rate limits, and sends `AvatarFetched` per chat. On the first error it
+  caches that chat without a picture (so it is not retried before the TTL),
+  stops and sends `AvatarFetchFinished` with the error; otherwise it ends with
+  `AvatarFetchFinished { error: None }`.
+- **Storage** — `AvatarFetched` inserts a `CachedAvatar` stamped with the
+  current `Instant`; `AvatarFetchFinished` clears `avatar_fetch_running` and,
+  on error, shows `Foto profilo non caricate: <error>`. A "no picture" entry
+  expires after the same TTL, which therefore doubles as the retry delay after
+  a failure. The cache is not persisted, so it is rebuilt after each restart.
+
 ## Sending and background tasks
 
 All network work runs in tokio tasks that report back through the event
 channel; `send_event` ignores a closed channel because that only happens on
-shutdown (`src/app.rs:1572-1579`).
+shutdown (`src/app.rs:1650-1657`).
 
-- `submit_input` (`src/app.rs:1317-1329`) ignores blank input or no open chat,
+- `submit_input` (`src/app.rs:1370-1382`) ignores blank input or no open chat,
   clears the input and calls `spawn_send` with `is_ai: false`.
-- `spawn_send` (`src/app.rs:1859-1872`) calls `WaHandle::send_text` and emits
+- `spawn_send` (`src/app.rs:1937-1950`) calls `WaHandle::send_text` and emits
   `Sent`; during a relink no session is running, so sends fail with a send
   error.
-- `handle_sent` (`src/app.rs:1092-1120`) stores a successful send as a
+- `handle_sent` (`src/app.rs:1145-1175`) stores a successful send as a
   `from_me` message (sender `OWN_SENDER_JID` = empty, `is_ai` preserved) and
   reloads the views; a failure sets `Invio fallito: …`.
-- `spawn_ai_request` (`src/app.rs:1593-1611`) spawns the HTTP request task and
+- `spawn_ai_request` (`src/app.rs:1674-1692`) spawns the HTTP request task and
   a waiter that always sends `AiReply`, also when the request task fails
   unexpectedly, so the chat never stays pending.
-- `spawn_image_descriptions` (`src/app.rs:1613-1645`) runs one task per image
+- `spawn_image_descriptions` (`src/app.rs:1696-1725`) 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.
-- `spawn_audio_transcriptions` (`src/app.rs:1786-1830`) does the same for
+- `spawn_audio_transcriptions` (`src/app.rs:1870-1908`) does the same for
   audios, emitting a single `AudiosTranscribed` that also carries the
   postponed images (see [Audio transcription path](#audio-transcription-path)).
-- `handle_ai_reply` (`src/app.rs:1044-1056`) shows
+- `handle_ai_reply` (`src/app.rs:1099-1110`) 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:1058-1077`), which sends it only if the chat is still enabled
+  (`src/app.rs:1113-1130`), which sends it only if the chat is still enabled
   and the owner did not answer by hand meanwhile (`owner_answered_meanwhile`,
-  `src/app.rs:1079-1090`); otherwise it shows
+  `src/app.rs:1133-1142`); otherwise it shows
   `Risposta AI scartata: autoreply disattivato` or
   `Risposta AI scartata: hai già risposto`.
 - The relink task (see [Relink](#relink)) runs `WaHandle::relink` and emits
   `Relinked`.
+- The avatar fetch task (see [Avatar cache](#avatar-cache)) emits
+  `AvatarFetched` and `AvatarFetchFinished`.
 
 ## Tests
 
-Unit tests at `src/app.rs:1941` onward cover `can_enable_autoreply` (private,
+Unit tests at `src/app.rs:2041` onward cover `can_enable_autoreply` (private,
 self-chat LID, groups allowed; broadcasts/newsletters and missing AI refused),
-`web_chats` filtering and order, `descriptions_apply`,
-`select_media_to_describe` (last five images and last three audios, skip known
-and out-of-context, empty when not enabled), `arrived_after`,
-`manual_reply_after`, `description_failure_status`,
+`web_chats` filtering and order (with an empty avatar cache),
+`descriptions_apply`, `select_media_to_describe` (last five images and last
+three audios, skip known and out-of-context, empty when not enabled),
+`arrived_after`, `manual_reply_after`, `description_failure_status`,
 `transcription_failure_status`, the voice-note text, `matches_filter`,
 selection clamping/stepping and page steps, the toggle helpers, `is_command`
 and the fixed function-key codes (F2, F3, F4, F10; F8 is not part of that
-check). Relink, configuration reload, the transcription handlers and F3
-targeting have no `App`-level tests. See
+check). Relink, configuration reload, the transcription handlers, the avatar
+cache and F3 targeting have no `App`-level tests. See
 [Unit tests](../testing/unit-tests.md).

+ 1 - 1
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 owns the application state and routes every AppEvent (keys, WhatsApp events, AI replies, image descriptions, audio transcriptions, reply timers, web panel commands, configuration reloads, relink outcomes), including the image and audio bookkeeping maps and sets, function-key handling, the reply orchestration order and the background tasks.
+- [Application state and event handling](app-state.md) - How the App struct in src/app.rs owns the application state and routes every AppEvent (keys, WhatsApp events, AI replies, image descriptions, audio transcriptions, reply timers, web panel commands, configuration reloads, relink outcomes, avatar fetches), including the image and audio bookkeeping, the web panel avatar cache, function-key handling, the reply orchestration order and the 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, the configuration watcher task, the draw/recv TUI loop and how the screen is repainted and wiped on exit.
 - [Terminal UI rendering](terminal-ui.md) - How src/ui.rs draws the connecting, QR pairing and main screens with ratatui, including the F8/F10 key help on the connecting and pairing screens, the filtered list with global autoreply marker, message formatting, wrapping, the input box and the footer with key help and build version.

+ 37 - 25
openwiki/architecture/runtime-and-event-loop.md

@@ -10,10 +10,10 @@ sources:
     resource: repo://src/config_watch.rs
   - id: openwiki-source-b55a21a31ede1b56cd31a6a6
     resource: repo://src/main.rs
-generated: { by: "openwiki/0.7.2", at: "2026-10-24T16:42:08.550Z" }
+generated: { by: "claude-code", at: "2026-10-25T12:54:53.684Z" }
 verified:
   - by: openwiki/0.7.2
-    at: 2026-10-23T21:26:24.156Z
+    at: 2026-10-25T12:54:53.684Z
 ---
 
 # Runtime and event loop
@@ -24,19 +24,30 @@ 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:32`), which the [terminal UI](terminal-ui.md) shows at the
-bottom right.
+`build.rs` runs two git commands at compile time through one helper,
+`git_output` (`build.rs:32-43`), and exports their trimmed output as
+compile-time environment variables (`build.rs:20-30`):
+
+| Variable | Command | Fallback |
+|---|---|---|
+| `WH_GIT_HASH` | `git rev-parse --short HEAD` | `unknown` |
+| `WH_BUILD_NUMBER` | `git rev-list --count HEAD` (commit count) | `0` |
+
+The fallback applies when git is missing, fails or prints nothing (for
+example when building outside a repository). It asks cargo to rebuild when
+`.git/HEAD`, `.git/refs` or `.git/packed-refs` change, so both values follow
+the checked-out commit. `main.rs` combines them into `BUILD_VERSION` =
+`<major>.<minor>.<build number> (<hash>)` (`src/main.rs:31-42`): only the
+major and minor parts of the package version are used, and the patch position
+is replaced by the commit count (with package version `0.1.0`, the 22nd
+commit builds as `0.1.22 (<hash>)`). The
+[terminal UI](terminal-ui.md) shows it at the bottom right.
 
 ## Startup sequence
 
-`main` (`src/main.rs:44-72`) runs on the multi-threaded tokio runtime
+`main` (`src/main.rs:53-82`) runs on the multi-threaded tokio runtime
 (`#[tokio::main]`). If any command-line argument is `--version` or `-V`
-(`wants_version`, `src/main.rs:78-80`) it only prints `wh <BUILD_VERSION>`
+(`wants_version`, `src/main.rs:88-90`) it only prints `wh <BUILD_VERSION>`
 and exits, without reading the configuration or touching the terminal.
 Otherwise it:
 
@@ -51,12 +62,12 @@ Otherwise it:
    `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:100-105`): nothing happens when
+   with `start_web_panel` (`src/main.rs:110-115`): nothing happens when
    `web_port` is unset, otherwise `web::start(port, app_tx)` binds the port.
 7. Spawns the configuration watcher with
-   `config_watch::spawn(config.source_files(), app_tx)` (`src/main.rs:61`).
+   `config_watch::spawn(config.source_files(), app_tx)` (`src/main.rs:71`).
 8. Builds `App` from `AppDeps { config, history, wa, events }`
-   (`src/main.rs:63-68`).
+   (`src/main.rs:73-78`).
 9. Spawns the input thread.
 10. Runs the TUI until quit.
 
@@ -81,7 +92,7 @@ so a changed `web_port` only applies after a restart.
      app_rx --> event_loop: draw ui::render -> recv -> app.handle_event
 ```
 
-- **WhatsApp forwarder** (`src/main.rs:86-94`): a tokio task that wraps each
+- **WhatsApp forwarder** (`src/main.rs:96-104`): a tokio task that wraps each
   `WaEvent` in `AppEvent::Wa` and stops when either channel side closes. The
   `WaEvent` channel outlives a relink: the new session started by
   `WaHandle::relink` sends into the same sender, so the forwarder keeps
@@ -97,27 +108,28 @@ so a changed `web_port` only applies after a restart.
   `AppEvent::ConfigReloaded` with the new configuration or the error text. It
   ends when the receiver is dropped. See
   [Configuration and data](../operations/configuration-and-data.md#live-reload).
-- **Input thread** (`src/main.rs:111-142`): a named OS thread (`wh-input`)
+- **Input thread** (`src/main.rs:121-152`): 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:149-155`): only `KeyEventKind::Press` keys
+- **Event mapping** (`src/main.rs:159-165`): 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 descriptions,
-  audio transcriptions, sends, reply-delay timers, relink) hold a clone of the
-  sender and report with `AiReply` / `ImagesDescribed` / `AudiosTranscribed` /
-  `Sent` / `ReplyDue` / `Relinked`.
+  audio transcriptions, sends, reply-delay timers, relink, web panel avatar
+  fetches) hold a clone of the sender and report with `AiReply` /
+  `ImagesDescribed` / `AudiosTranscribed` / `Sent` / `ReplyDue` / `Relinked` /
+  `AvatarFetched` / `AvatarFetchFinished`.
 
 ## TUI loop
 
-`run_tui` (`src/main.rs:161-167`) calls `ratatui::init`, runs the loop, then
-wipes the screen with `clear_screen` (`src/main.rs:174-176`: clear all and
+`run_tui` (`src/main.rs:171-177`) calls `ratatui::init`, runs the loop, then
+wipes the screen with `clear_screen` (`src/main.rs:184-186`: clear all and
 move the cursor to the top-left, with no cursor-position query) and always
 calls `ratatui::restore`, so raw mode and the alternate screen are left even
 on errors and terminals without an alternate screen are not left showing the
 UI. The loop error wins over a clear error (`result.and(cleared)`).
 
-`event_loop` (`src/main.rs:183-206`) repeats while `!app.should_quit`:
+`event_loop` (`src/main.rs:193-216`) 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.
@@ -137,6 +149,6 @@ 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. `src/config_watch.rs` tests that modification-time snapshots
+and that `BUILD_VERSION` is `<major>.<minor>.` followed by an all-digit build
+number and a commit hash in parentheses. `src/config_watch.rs` tests that modification-time snapshots
 track a file's creation and deletion. See [Unit tests](../testing/unit-tests.md).

+ 2 - 2
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 — 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 — plus the OpenAI-compatible audio transcription client in src/transcribe.rs.
-- [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 (including POST /api/relink), the custom-header guard on POST, the request/reply bridge to App through AppEvent::Web, the state with screen and pairing QR as SVG, the quit and relink flows, the browser page with its pairing card and its security caveats.
-- [WhatsApp client integration](whatsapp-client.md) - How src/whatsapp.rs wraps whatsapp-rust — the replaceable session behind WaHandle, session startup and relink (logout, session database reset, new unpaired client), translation of raw events into WaEvent, JID canonicalization, contact names, history sync, message rendering (including voice-note placeholders and audio references), image/audio download and sending.
+- [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 (including POST /api/relink), the custom-header guard on POST, the request/reply bridge to App through AppEvent::Web, the state with screen, pairing QR as SVG and per-chat profile picture URLs, the lazy avatar fetch, the quit and relink flows, the browser page with its pairing card and round avatars, and its security caveats.
+- [WhatsApp client integration](whatsapp-client.md) - How src/whatsapp.rs wraps whatsapp-rust — the replaceable session behind WaHandle, session startup and relink (logout, session database reset, new unpaired client), translation of raw events into WaEvent, the scheduled "unavailable" presence that keeps phone notifications working, JID canonicalization, contact names, history sync, message rendering (including voice-note placeholders and audio references), image/audio download, profile picture URLs and sending.

+ 75 - 36
openwiki/integrations/web-control-panel.md

@@ -1,8 +1,8 @@
 ---
 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 (including POST /api/relink), the custom-header guard on POST, the request/reply bridge to App through AppEvent::Web, the state with screen and pairing QR as SVG, the quit and relink flows, the browser page with its pairing card and its security caveats.
-tags: [web, http, tiny_http, control-panel, security, autoreply, qr-code, relink]
+description: The optional HTTP control panel in src/web.rs and src/web_page.html, enabled by web_port — routes (including POST /api/relink), the custom-header guard on POST, the request/reply bridge to App through AppEvent::Web, the state with screen, pairing QR as SVG and per-chat profile picture URLs, the lazy avatar fetch, the quit and relink flows, the browser page with its pairing card and round avatars, and its security caveats.
+tags: [web, http, tiny_http, control-panel, security, autoreply, qr-code, relink, avatars]
 sources:
   - id: openwiki-source-9fec9d1af408501c9c455161
     resource: repo://src/app.rs
@@ -12,19 +12,20 @@ sources:
     resource: repo://src/web_page.html
   - id: openwiki-source-7b75006986d8b0337adc6ca0
     resource: repo://src/web.rs
-generated: { by: "openwiki/0.7.2", at: "2026-10-24T16:42:08.550Z" }
+generated: { by: "claude-code", at: "2026-10-25T12:54:53.684Z" }
 verified:
   - by: openwiki/0.7.2
-    at: 2026-10-23T21:26:24.156Z
+    at: 2026-10-25T12:54:53.684Z
 ---
 
 # Web control panel
 
 `src/web.rs` is an optional, minimal HTTP server that lets a browser (for
-example a phone on the same network) see the chats, toggle the global and
-per-chat AI autoreply, pair WhatsApp by scanning the QR code shown in the
-page, request a new pairing, or quit the bot. It is off unless `web_port` is
-set in `config.toml` (see [Configuration and data](../operations/configuration-and-data.md)).
+example a phone on the same network) see the chats with their profile
+pictures, toggle the global and per-chat AI autoreply, pair WhatsApp by
+scanning the QR code shown in the page, request a new pairing, or quit the
+bot. It is off unless `web_port` is set in `config.toml` (see
+[Configuration and data](../operations/configuration-and-data.md)).
 It has **no authentication** and listens on **all interfaces** (`0.0.0.0`,
 `src/web.rs:17-19`).
 
@@ -32,13 +33,13 @@ It has **no authentication** and listens on **all interfaces** (`0.0.0.0`,
 
 `main` calls `start_web_panel`, which calls `web::start(port, app_tx)` only
 when `web_port` is set ([Runtime and event loop](../architecture/runtime-and-event-loop.md)).
-`start` (`src/web.rs:191-203`) binds a `tiny_http::Server` on
+`start` (`src/web.rs:193-205`) binds a `tiny_http::Server` on
 `0.0.0.0:<port>` and spawns a detached OS thread named `wh-web`. A bind
 failure aborts startup with `cannot start web server on port <port>`. The
 port is read once: unlike the other settings, a changed `web_port` is not
 applied by the configuration live reload and needs a restart.
 
-`serve` (`src/web.rs:205-217`) handles requests **one at a time** on that
+`serve` (`src/web.rs:207-219`) handles requests **one at a time** on that
 thread and stops after a request once the application channel is closed.
 `tiny_http` is the only extra dependency for the server; the `qrcode` crate
 (already used by the TUI) is built with its `svg` feature for the pairing
@@ -46,8 +47,8 @@ QR (`Cargo.toml`).
 
 ## Routes
 
-`resolve_route` (`src/web.rs:352-367`) matches method and path; the query
-string is ignored (`path_without_query`, `src/web.rs:344-348`):
+`resolve_route` (`src/web.rs:354-369`) matches method and path; the query
+string is ignored (`path_without_query`, `src/web.rs:346-350`):
 
 | Method | Path | Effect | Response |
 |---|---|---|---|
@@ -59,11 +60,13 @@ string is ignored (`path_without_query`, `src/web.rs:344-348`):
 | POST | `/api/quit` | `WebCommand::Quit` | 200 `{"status":"Bot in chiusura"}` |
 | other | other | — | 404 `non trovato` |
 
-The JSON state is `WebState` (`src/web.rs:125-136`):
+The JSON state is `WebState` (`src/web.rs:127-138`), whose chats are
+`WebChat` entries (`src/web.rs:114-123`):
 
 ```json
 { "global_autoreply": false,
-  "chats": [ { "jid": "...", "name": "...", "autoreply": true } ],
+  "chats": [ { "jid": "...", "name": "...", "autoreply": true,
+               "avatar_url": "https://... | null" } ],
   "status": "<TUI status line>",
   "screen": "connecting | pairing | main",
   "qr_svg": "<svg ...>...</svg>" }
@@ -71,10 +74,12 @@ The JSON state is `WebState` (`src/web.rs:125-136`):
 
 `screen` mirrors the TUI screen (`SCREEN_CONNECTING`, `SCREEN_PAIRING`,
 `SCREEN_MAIN`, `src/web.rs:35-39`). `qr_svg` is `null` unless the app holds
-a pairing QR payload; then `qr_svg` (`src/web.rs:369-384`) renders it as an
+a pairing QR payload; then `qr_svg` (`src/web.rs:371-386`) renders it as an
 SVG document with 6-pixel black modules on a white quiet zone. A payload
 that cannot be encoded yields `null` (the TUI pairing screen shows the
-encoding error).
+encoding error). `avatar_url` is the WhatsApp CDN URL of the chat's profile
+picture preview, or `null` while it has not been fetched yet or when the chat
+has no picture or hides it (see [Profile pictures](#profile-pictures)).
 
 ### Request checks
 
@@ -82,19 +87,19 @@ encoding error).
   must carry the header `X-Wh-Request: 1` (name case-insensitive, value
   exact); otherwise the server answers 403 `richiesta non autorizzata` and
   forwards nothing (`requires_request_header`, `handle_request`,
-  `has_request_header`, `src/web.rs:171-174`, `src/web.rs:219-236`,
-  `src/web.rs:386-395`). A cross-site page cannot add a custom header
+  `has_request_header`, `src/web.rs:173-176`, `src/web.rs:221-238`,
+  `src/web.rs:388-397`). A cross-site page cannot add a custom header
   without a CORS preflight, which the server never approves, so this blocks
   simple cross-site form/fetch requests (CSRF). It is **not**
   authentication: anyone who can reach the port can send the header.
 - **Body limit** — `/api/chat` reads at most 1024 bytes
   (`MAX_BODY_BYTES`); an oversized, empty (after trimming), unreadable or
   non-UTF-8 body yields 400 `corpo della richiesta non valido`
-  (`read_limited_body`, `src/web.rs:397-414`).
+  (`read_limited_body`, `src/web.rs:399-412`).
 
 ## Bridge to the application
 
-The server never touches application state. `forward` (`src/web.rs:279-292`)
+The server never touches application state. `forward` (`src/web.rs:281-294`)
 sends `AppEvent::Web(WebRequest { command, reply })` with a tokio `oneshot`
 sender and blocks the web thread (`blocking_recv`) until `App` answers.
 `App::handle_web_request` runs inside the normal event loop, so web commands
@@ -111,6 +116,8 @@ are serialised with keys and WhatsApp events
   asks for confirmation instead of the TUI's double F8); it is ignored while
   a relink is already running. See
   [WhatsApp relink and pairing](../workflows/relink-and-pairing.md);
+- after every command (`State` included) `App` may start the avatar fetch
+  described below;
 - the reply is always the state after the command; refusals are visible
   only through `status`.
 
@@ -122,9 +129,30 @@ flag is reported separately.
 If the event channel is closed or the reply is dropped, the server answers
 503 `bot non disponibile`; a serialisation failure gives 500.
 
+### Profile pictures
+
+Avatars are fetched lazily by `App` and driven by the panel's own requests,
+so nothing is fetched while no browser is open
+([Application state: avatar cache](../architecture/app-state.md#avatar-cache)):
+
+- each web request calls `maybe_start_avatar_fetch`, which starts one
+  background batch only on the main screen, while WhatsApp is connected and
+  when no batch is already running;
+- the batch covers the panel's chats whose cached avatar is missing or older
+  than one day, and asks
+  [`WaHandle::profile_picture_url`](whatsapp-client.md#profile-pictures)
+  for one chat every 2 seconds to stay below WhatsApp rate limits;
+- the first error stops the batch, caches that chat without picture until
+  the TTL expires and shows `Foto profilo non caricate: <error>`;
+- the URLs appear in later state replies as they arrive; the cache lives in
+  memory only and is rebuilt after a restart.
+
+The server only passes the URL along: the browser loads the picture directly
+from the WhatsApp CDN.
+
 ### Quit
 
-`handle_quit` (`src/web.rs:261-277`) first writes the 200 response, then
+`handle_quit` (`src/web.rs:263-279`) first writes the 200 response, then
 sends `WebCommand::Quit` without waiting for a reply, because the process
 exits as soon as `App` sets `should_quit` and the event loop ends. The TUI
 terminal is restored as on F10.
@@ -133,55 +161,66 @@ terminal is restored as on F10.
 
 `src/web_page.html` is a self-contained page (inline CSS and JS, Italian
 UI) with a global "AI tutte" switch, a "Nuovo QR" button, a "Chiudi bot"
-button, a pairing card, one switch per chat and a status bar:
+button, a pairing card, one row per chat (avatar, name, switch) and a status
+bar:
 
 - it polls `GET /api/state` every 5 s while the tab is visible — every 2 s
   while `screen` is not `main`, so a rotated QR shows up quickly
   (`adjustPolling`) — and refreshes when it becomes visible again
-  (`src/web_page.html:382-383`, `src/web_page.html:623-629`,
-  `src/web_page.html:742-763`);
-- the pairing card (`renderPairing`, `src/web_page.html:604-622`) is hidden
+  (`src/web_page.html:416-417`, `src/web_page.html:707-713`,
+  `src/web_page.html:826-847`);
+- the pairing card (`renderPairing`, `src/web_page.html:688-706`) is hidden
   on `main`; otherwise it shows "Connessione a WhatsApp in corso...", "In
   attesa del codice QR..." or, with a QR, "Collega WhatsApp: scansiona il
   codice QR", the QR image (the SVG as a `data:` URL, replaced only when it
   changes) and the WhatsApp linking steps;
+- each chat row has a 40 px round avatar (`createAvatar`, `updateAvatar`,
+  `src/web_page.html:548-624`): the upper-cased first character of the name
+  on a grey circle, covered by the profile picture once `avatar_url` is set.
+  The image is loaded lazily with `referrerPolicy = "no-referrer"`, its `src`
+  changes only when the URL changes (polling never reloads it), and a failed
+  load (for example an expired CDN URL) hides it so the initial shows again;
 - every POST carries `X-Wh-Request: 1`; the chat toggle sends the JID as a
-  `text/plain` body (`buildHeaders`, `src/web_page.html:445-450`);
+  `text/plain` body (`buildHeaders`, `src/web_page.html:479-484`);
 - while a toggle is in flight its switch is disabled and keeps the user's
   position; polling is paused, and responses are applied in request order
   through a ticket counter so a stale poll cannot overwrite a newer toggle
-  result (`applyState`, `runToggle`, `src/web_page.html:647-701`);
+  result (`applyState`, `runToggle`, `src/web_page.html:731-785`);
 - names are inserted with `textContent` only;
-- "Nuovo QR" (`relinkBot`, `src/web_page.html:707-718`) asks for
+- "Nuovo QR" (`relinkBot`, `src/web_page.html:791-802`) asks for
   confirmation (the device will be unlinked from the phone), posts
   `/api/relink`, applies the returned state (the pairing card appears) and
   keeps the button disabled while the request runs;
 - quit asks for confirmation (`Chiudere il bot?`) and then shows a
   full-screen "Bot chiuso" overlay and stops polling; a network error after
   a quit request is treated as the bot having exited
-  (`src/web_page.html:485-511`, `src/web_page.html:724-737`).
+  (`src/web_page.html:519-545`, `src/web_page.html:808-821`).
 
 ## Security and operations
 
 - No authentication and no TLS: anyone who can reach `<host>:<web_port>` can
-  read chat names and JIDs, toggle autoreplies, stop the bot, unlink the
-  WhatsApp device with `/api/relink` and, while pairing, read the QR code
-  and link their own phone. Enable it only on a trusted network, or restrict
-  the port with the host firewall.
+  read chat names, JIDs and profile picture URLs, toggle autoreplies, stop
+  the bot, unlink the WhatsApp device with `/api/relink` and, while pairing,
+  read the QR code and link their own phone. Enable it only on a trusted
+  network, or restrict the port with the host firewall.
 - The server binds to every interface; there is no setting for the bind
   address.
 - `web_port = 0` is rejected at startup; values above 65535 or negative are
   rejected by TOML parsing.
 - Requests are served sequentially and each API request waits for the event
   loop, so a slow request delays the others. A relink request returns as
-  soon as the relink task is spawned, not when it completes.
+  soon as the relink task is spawned, not when it completes; the avatar
+  fetch likewise runs in the background and never delays a reply.
+- Opening the panel makes the bot issue profile picture requests to
+  WhatsApp (one every 2 seconds, at most once a day per chat).
 
 ## Tests
 
-`src/web.rs:415-502` covers query stripping, route resolution (known routes
+`src/web.rs:414-505` covers query stripping, route resolution (known routes
 including `/api/relink`, wrong method or path, `GET /api/relink` → not
 found), the custom header check (any case accepted, missing or wrong value
 rejected), which routes require the header (relink included), `qr_svg`
 producing an SVG document, and body reading (trimming, empty/invalid UTF-8
 rejection, the 1024-byte limit). `web_chats` filtering is tested in
-`src/app.rs`. See [Unit tests](../testing/unit-tests.md).
+`src/app.rs`; the avatar fetch and the page's avatar rendering have no tests.
+See [Unit tests](../testing/unit-tests.md).

+ 87 - 47
openwiki/integrations/whatsapp-client.md

@@ -1,14 +1,17 @@
 ---
 type: integration
 title: WhatsApp client integration
-description: How src/whatsapp.rs wraps whatsapp-rust — the replaceable session behind WaHandle, session startup and relink (logout, session database reset, new unpaired client), translation of raw events into WaEvent, JID canonicalization, contact names, history sync, message rendering (including voice-note placeholders and audio references), image/audio download and sending.
-tags: [whatsapp, whatsapp-rust, events, jid, history-sync, relink, session, audio]
+description: How src/whatsapp.rs wraps whatsapp-rust — the replaceable session behind WaHandle, session startup and relink (logout, session database reset, new unpaired client), translation of raw events into WaEvent, the scheduled "unavailable" presence that keeps phone notifications working, JID canonicalization, contact names, history sync, message rendering (including voice-note placeholders and audio references), image/audio download, profile picture URLs and sending.
+tags: [whatsapp, whatsapp-rust, events, jid, history-sync, relink, session, audio, presence, avatars]
 sources:
   - id: openwiki-source-651d1fb6c9e49916a916ab51
     resource: repo://Cargo.toml
   - id: openwiki-source-7ea5eac45e5030178a6ea05b
     resource: repo://src/whatsapp.rs
-generated: { by: "opencode", at: "2026-10-24T16:39:50.206Z" }
+generated: { by: "claude-code", at: "2026-10-25T12:54:53.684Z" }
+verified:
+  - by: openwiki/0.7.2
+    at: 2026-10-25T12:54:53.684Z
 ---
 
 # WhatsApp client integration
@@ -21,8 +24,8 @@ generated: { by: "opencode", at: "2026-10-24T16:39:50.206Z" }
 - a stream of plain `WaEvent` values sent to the
   [event loop](../architecture/runtime-and-event-loop.md);
 - a cloneable `WaHandle` used by [`App`](../architecture/app-state.md) to
-  send text, download images and audios, query its own JIDs and replace the
-  session with a new unpaired one (`relink`).
+  send text, download images and audios, fetch profile picture URLs, query
+  its own JIDs and replace the session with a new unpaired one (`relink`).
 
 `rusqlite` is pinned so that it shares `libsqlite3-sys` with the `diesel`
 backend of `whatsapp-rust-sqlite-storage` (`Cargo.toml:17-18`); the module
@@ -30,23 +33,23 @@ also uses it directly to empty the session database during a relink.
 
 ## Session and handle
 
-`WaHandle` (`src/whatsapp.rs:196-203`) holds the running `Session` in an
+`WaHandle` (`src/whatsapp.rs:202-209`) holds the running `Session` in an
 `Arc<Mutex<Option<Session>>>`, the `session.db` path and the `WaEvent`
 sender shared by every session. Clones share the same slot, so a relink
-started from one clone is seen by all. A `Session` (`src/whatsapp.rs:206-213`)
+started from one clone is seen by all. A `Session` (`src/whatsapp.rs:212-219`)
 is one client (`Arc<Client>`), the abort handle of its translator task and a
 `watch` receiver that becomes `true` when the client run loop stops.
 
 Every client operation goes through `WaHandle::client`
-(`src/whatsapp.rs:238-243`), which fails with
+(`src/whatsapp.rs:244-249`), which fails with
 `WhatsApp session not running` while the slot is empty — during a relink or
 after a failed one. The lock recovers from poisoning (`lock_session`).
 
 ## Startup
 
-`start(session_db, events)` (`src/whatsapp.rs:358-370`) calls
+`start(session_db, events)` (`src/whatsapp.rs:384-396`) calls
 `open_session` and wraps the result in a new `WaHandle`. `open_session`
-(`src/whatsapp.rs:430-471`):
+(`src/whatsapp.rs:456-496`):
 
 1. requires a UTF-8 session path and creates its parent directory;
 2. opens the session store with `SqliteStore::new` (`session.db`, see
@@ -56,11 +59,11 @@ after a failed one. The lock recovers from poisoning (`lock_session`).
 5. when the bot task ends, emits `Disconnected("client stopped")` and marks
    the session stopped on its `watch` channel.
 
-`EventForwarder` (`src/whatsapp.rs:472-495`) only pushes raw events, in
+`EventForwarder` (`src/whatsapp.rs:498-521`) only pushes raw events, in
 order, into an internal unbounded channel; its interest set is
-`SUBSCRIBED_KINDS` (`src/whatsapp.rs:58-75`), which includes
-`OfflineSyncCompleted` and `AppStateSyncFailed`. All translation happens in
-`Translator`, which also owns a name cache.
+`SUBSCRIBED_KINDS` (`src/whatsapp.rs:59-79`), which includes
+`OfflineSyncCompleted`, `AppStateSyncFailed` and `SelfPushNameUpdated`. All
+translation happens in `Translator`, which also owns a name cache.
 
 ```
 whatsapp-rust Bot --raw events--> EventForwarder --chan--> Translator --WaEvent--> App
@@ -68,19 +71,19 @@ whatsapp-rust Bot --raw events--> EventForwarder --chan--> Translator --WaEvent-
 
 ## Relink
 
-`WaHandle::relink` (`src/whatsapp.rs:261-270`) replaces the session with a
+`WaHandle::relink` (`src/whatsapp.rs:267-276`) replaces the session with a
 new, unpaired one so a new QR code is offered (end-to-end flow:
 [WhatsApp relink and pairing](../workflows/relink-and-pairing.md)):
 
 1. takes the current session out of the slot (the slot stays empty) and
-   calls `Session::stop` (`src/whatsapp.rs:223-231`): `logout` (deregisters
+   calls `Session::stop` (`src/whatsapp.rs:229-237`): `logout` (deregisters
    the device on the phone when connected, best effort, and disconnects),
    waits up to 10 s (`STOP_TIMEOUT_SECONDS`) for the run loop to stop, then
    aborts the translator task;
-2. `reset_session_files` (`src/whatsapp.rs:374-386`) deletes `session.db`
+2. `reset_session_files` (`src/whatsapp.rs:400-412`) deletes `session.db`
    and its `-wal` / `-shm` side files (missing files are fine). When the
    deletion fails — on Windows the library keeps the database open until the
-   process exits — `clear_session_tables` (`src/whatsapp.rs:407-428`)
+   process exits — `clear_session_tables` (`src/whatsapp.rs:433-454`)
    instead deletes every row of every table except SQLite internals and the
    `__diesel_schema_migrations` record, in one transaction with a 5 s busy
    timeout. The stopped client writes nothing more afterwards;
@@ -93,7 +96,7 @@ successful relink (sends then fail with `WhatsApp session not running`).
 
 ## WaEvent
 
-`WaEvent` (`src/whatsapp.rs:109-135`):
+`WaEvent` (`src/whatsapp.rs:115-141`):
 
 | Variant | Meaning |
 |---|---|
@@ -106,7 +109,7 @@ successful relink (sends then fail with `WhatsApp session not running`).
 | `Contact(Contact)` | A new or better contact name |
 | `Error(String)` | Non-fatal problem shown in the status line |
 
-`Translator::handle_event` (`src/whatsapp.rs:641-690`) maps raw events to
+`Translator::handle_event` (`src/whatsapp.rs:667-717`) maps raw events to
 these variants (`Event::OfflineSyncCompleted` becomes
 `WaEvent::OfflineSyncCompleted`). Failures become `Error` texts such as
 `pairing QR codes exhausted; request a new QR to pair again` (F8 or the web
@@ -114,37 +117,60 @@ panel's "Nuovo QR"), `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:1031-1041`), `stream error: ...` and
+`src/whatsapp.rs:1085-1095`), `stream error: ...` and
 `could not decrypt a message from ...`; `StreamReplaced` becomes
-`Disconnected("stream replaced by another session")`.
+`Disconnected("stream replaced by another session")`. `Connected` and
+`SelfPushNameUpdated` additionally schedule the offline presence described
+below.
 
 `AppStateSyncFailed` is reported only when at least one collection failed
-**fatally** (`is_fatal_sync_failure`, `src/whatsapp.rs:1043-1050`), as
+**fatally** (`is_fatal_sync_failure`, `src/whatsapp.rs:1097-1104`), as
 `app state sync incomplete (fatal: [...], retryable: [...])`; retryable or
 skipped collections are retried by the library on a forced reconnect and are
 not shown.
 
+## Presence and phone notifications
+
+The library marks the account *available* on every (re)connect and push-name
+update, and while a linked device is available WhatsApp suppresses push
+notifications on the phone. `schedule_offline_presence`
+(`src/whatsapp.rs:719-741`) undoes this: it spawns a task that waits
+`PRESENCE_SETTLE_SECONDS` (5 s, `src/whatsapp.rs:81-83`), returns silently if
+the client is no longer connected, and otherwise sends an "unavailable"
+presence with `client.presence().set_unavailable()`.
+
+- It runs after every `Event::Connected` (after emitting
+  `WaEvent::Connected`) and after every `Event::SelfPushNameUpdated`.
+- On the first pairing the push name is not known yet, so the call fails with
+  `PresenceError::PushNameEmpty`; that error is ignored because the later
+  `SelfPushNameUpdated` event schedules the presence again.
+- Any other failure is reported as
+  `failed to set unavailable presence: <error>` (`WaEvent::Error`).
+
+The bot therefore appears offline to contacts and the phone keeps receiving
+notifications, while messages are still delivered to the bot.
+
 ## JIDs
 
 - JIDs are stored as `user@server` without device suffix
-  (`jid_to_string` uses `to_non_ad_string`, `src/whatsapp.rs:1060-1069`).
-- `canonical_jid` (`src/whatsapp.rs:594-614`) prefers the phone-number (PN)
+  (`jid_to_string` uses `to_non_ad_string`, `src/whatsapp.rs:1114-1123`).
+- `canonical_jid` (`src/whatsapp.rs:620-640`) 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:339-348`) returns this account's PN
+- `WaHandle::own_jids` (`src/whatsapp.rs:365-374`) returns this account's PN
   and LID; it is empty before pairing completes and while no session is
   running.
 
 ## Live messages
 
-`handle_live_message` (`src/whatsapp.rs:703-755`):
+`handle_live_message` (`src/whatsapp.rs:757-809`):
 
 - 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:1052-1058`);
+  and empty names, `src/whatsapp.rs:1106-1112`);
 - attaches an `ImageRef` (`image_ref_of`) only when the rendered text is an
   image text, and an `AudioRef` (`audio_ref_of`) only when it is a voice
   note;
@@ -157,10 +183,10 @@ rendered, and the [AI autoreply](../workflows/ai-autoreply.md) only rewrites
 ## Contact names
 
 Names come from two sources ranked `Push < Saved` (`NameSource`,
-`src/whatsapp.rs:497-503`): push names from messages and `PushNameUpdate`
-(`src/whatsapp.rs:779-783`), saved names from `ContactUpdate` (full name,
-else first name, `src/whatsapp.rs:760-774`) and from history sync.
-`note_name` (`src/whatsapp.rs:557-579`) ignores empty names, never lets a
+`src/whatsapp.rs:523-529`): push names from messages and `PushNameUpdate`
+(`src/whatsapp.rs:833-838`), saved names from `ContactUpdate` (full name,
+else first name, `src/whatsapp.rs:814-828`) and from history sync.
+`note_name` (`src/whatsapp.rs:583-605`) 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.
@@ -168,11 +194,11 @@ persists them.
 ## History sync
 
 Each `HistorySync` chunk is processed in its own task
-(`handle_history`, `src/whatsapp.rs:789-813`). Decoding (inflate + protobuf)
-runs in `spawn_blocking` via `extract_history` (`src/whatsapp.rs:939-984`).
-`emit_history` (`src/whatsapp.rs:815-836`) then emits push names, inline
+(`handle_history`, `src/whatsapp.rs:843-867`). Decoding (inflate + protobuf)
+runs in `spawn_blocking` via `extract_history` (`src/whatsapp.rs:993-1038`).
+`emit_history` (`src/whatsapp.rs:869-890`) then emits push names, inline
 address-book contacts, conversation names (`emit_history_conversation`,
-`src/whatsapp.rs:843-884`) and every renderable message.
+`src/whatsapp.rs:897-938`) and every renderable message.
 History messages never carry an `ImageRef` or `AudioRef` (so live-only media
 handling never applies to them); the sender is the
 own PN for own messages, otherwise the group participant or the chat.
@@ -181,46 +207,60 @@ Undecodable conversations are reported as
 
 ## Message rendering
 
-`describe_message` (`src/whatsapp.rs:1071-1083`) turns a protobuf message into
+`describe_message` (`src/whatsapp.rs:1125-1137`) 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:1091-1103`);
+   `src/whatsapp.rs:1145-1157`);
 3. caption of other media;
-4. `describe_other` (`src/whatsapp.rs:1111-1129`): protocol, reaction,
+4. `describe_other` (`src/whatsapp.rs:1165-1183`): 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:1169-1203`): `is_placeholder` (only `[media]` and
+(`src/whatsapp.rs:1223-1262`): `is_placeholder` (only `[media]` and
 `[messaggio non supportato]`; the voice and image placeholders are **not**
 placeholders in this sense), `is_voice_note`, `is_image` and
 `image_caption`.
 
 ## Sending and downloading
 
-- `WaHandle::send_text(chat_jid, text)` (`src/whatsapp.rs:277-290`) rejects
+- `WaHandle::send_text(chat_jid, text)` (`src/whatsapp.rs:283-296`) rejects
   empty text, parses the JID and returns the new message id; it fails while
   no session is running.
-- `WaHandle::download_image(&ImageRef)` (`src/whatsapp.rs:297-310`) downloads
+- `WaHandle::download_image(&ImageRef)` (`src/whatsapp.rs:303-316`) downloads
   and decrypts the image into memory (`DownloadedImage`), falling back to
   `image/jpeg` when the MIME type is blank. `WaHandle::download_audio`
-  (`src/whatsapp.rs:317-330`) does the same for audios
+  (`src/whatsapp.rs:323-336`) does the same for audios
   (`DownloadedAudio`, fallback `audio/ogg`). Media is never written to disk
   by this module, and the `Debug` impls of `ImageRef`/`AudioRef` omit key
-  material (`src/whatsapp.rs:144-154`, `src/whatsapp.rs:172-182`).
+  material (`src/whatsapp.rs:150-160`, `src/whatsapp.rs:178-188`).
+
+## Profile pictures
+
+`WaHandle::profile_picture_url(chat_jid)` (`src/whatsapp.rs:338-357`) parses
+the JID (private chat or group, `user@server` form) and sends one IQ request
+through `client.contacts().get_profile_picture(&jid, true)`, asking for the
+small preview. It returns `Some(url)` with the WhatsApp CDN URL, `None` when
+the chat has no picture or hides it for privacy, and an error for an invalid
+JID, while no session is running, or on a network failure
+(`failed to fetch the profile picture of <jid>`). Only the URL is returned;
+the picture itself is never downloaded by the bot. It is used by the
+[web panel avatar fetch](web-control-panel.md#profile-pictures), which paces
+the calls to avoid rate limits.
 
 ## Tests
 
-`src/whatsapp.rs:1216` onward covers `is_fatal_sync_failure`, text and
+`src/whatsapp.rs:1270` onward covers `is_fatal_sync_failure`, text and
 extended text, captions and placeholders, image and voice rendering
 (including the audio-message placeholder and `audio_ref_of`), image
 references, placeholder helpers, silent reactions/protocol messages, JID
 device-suffix removal and push-name filtering. An `#[ignore]`d live test,
 `relink_deletes_the_session_and_offers_a_new_qr`
-(`src/whatsapp.rs:1241-1259`), connects to the WhatsApp servers, waits for a
+(`src/whatsapp.rs:1293-1313`), connects to the WhatsApp servers, waits for a
 QR, relinks and checks that the second QR carries new device keys; run it
-with `cargo test -- --ignored`. See [Unit tests](../testing/unit-tests.md).
+with `cargo test -- --ignored`. The offline presence and
+`profile_picture_url` have no tests. See [Unit tests](../testing/unit-tests.md).

+ 3 - 0
openwiki/operations/configuration-and-data.md

@@ -21,6 +21,9 @@ sources:
   - id: openwiki-source-b55a21a31ede1b56cd31a6a6
     resource: repo://src/main.rs
 generated: { by: "opencode", at: "2026-10-24T16:39:50.206Z" }
+verified:
+  - by: openwiki/0.7.2
+    at: 2026-10-24T16:39:50.206Z
 ---
 
 # Configuration and data files

+ 3 - 0
openwiki/quickstart.md

@@ -15,6 +15,9 @@ sources:
   - id: openwiki-source-b55a21a31ede1b56cd31a6a6
     resource: repo://src/main.rs
 generated: { by: "opencode", at: "2026-10-24T16:39:50.206Z" }
+verified:
+  - by: openwiki/0.7.2
+    at: 2026-10-24T16:39:50.206Z
 ---
 
 # Quickstart

+ 14 - 10
openwiki/testing/unit-tests.md

@@ -28,7 +28,10 @@ sources:
     resource: repo://src/web.rs
   - id: openwiki-source-7ea5eac45e5030178a6ea05b
     resource: repo://src/whatsapp.rs
-generated: { by: "opencode", at: "2026-10-24T16:39:50.206Z" }
+generated: { by: "claude-code", at: "2026-10-25T12:54:53.684Z" }
+verified:
+  - by: openwiki/0.7.2
+    at: 2026-10-25T12:54:53.684Z
 ---
 
 # Unit tests
@@ -49,7 +52,7 @@ cargo test -- --ignored           # only the ignored live test (needs network)
 ```
 
 The live test `whatsapp::tests::relink_deletes_the_session_and_offers_a_new_qr`
-(`src/whatsapp.rs:1238-1258`) is skipped by a plain `cargo test`. It starts a
+(`src/whatsapp.rs:1293-1313`) is skipped by a plain `cargo test`. It starts a
 real client on a per-process `target/wh-relink-test-<pid>.db`, waits up to
 60 s for a pairing QR from the WhatsApp servers, calls `WaHandle::relink`,
 waits for the next QR and asserts that the device keys in the two QR
@@ -61,21 +64,22 @@ exits. See [WhatsApp relink and pairing](../workflows/relink-and-pairing.md).
 
 | Module | Tests | Location | Covers |
 |---|---|---|---|
-| `autoreply` | 55 | `src/autoreply.rs:551+` | 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` |
+| `autoreply` | 58 | `src/autoreply.rs:529+` | 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, `today_label` (local `dd/mm/yyyy` date, empty for an unresolvable timestamp), `build_turns` (system turn naming private chat, group or self-chat, the `Oggi è <date>.` line when a date is set, 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` | 41 | `src/config.rs:410+` | defaults, full files, ignored legacy keys (`system_prompt`, cooldown keys, `transcribe_command`), blank values, trimming, base URL normalisation, `Debug` masking, DB and AI log paths, `ai_log`, `reply_delay_seconds` (default, custom and `0`, negatives rejected), catch-up and `ai_retries` defaults/custom values/negative rejection, `web_port` (unset, valid, `0` and out of range rejected), transcription keys (unset disables with defaults, trimmed, blank falls back), vision model, 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:589+` | 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` | 23 + 1 ignored | `src/whatsapp.rs:1216+` | ignored live relink test (new QR with new device keys), fatal-only app state sync reporting, message rendering and placeholders, captions, image and audio references, voice/image detection, silent messages, JID device suffix, push-name filtering |
-| `app` | 23 | `src/app.rs:1941+` | autoreply enable rules (private, self-chat LID, groups; broadcasts, newsletters and missing AI refused), `web_chats` filtering and order, `descriptions_apply`, `select_media_to_describe` (last five images / last three audios in arrival order, known and out-of-context items skipped, empty when not enabled), `arrived_after`, `manual_reply_after`, description and transcription failure statuses, the voice-note text, list filter matching, selection, page steps and toggle helpers, AltGr handling, fixed function-key codes |
+| `whatsapp` | 23 + 1 ignored | `src/whatsapp.rs:1269+` | ignored live relink test (new QR with new device keys), fatal-only app state sync reporting, message rendering and placeholders, captions, image and audio references, voice/image detection, silent messages, JID device suffix, push-name filtering |
+| `app` | 23 | `src/app.rs:2041+` | autoreply enable rules (private, self-chat LID, groups; broadcasts, newsletters and missing AI refused), `web_chats` filtering and order (with an empty avatar cache), `descriptions_apply`, `select_media_to_describe` (last five images / last three audios in arrival order, known and out-of-context items skipped, empty when not enabled), `arrived_after`, `manual_reply_after`, description and transcription failure statuses, the voice-note text, list filter matching, selection, page steps and toggle helpers, AltGr handling, fixed function-key codes |
 | `history` | 19 | `src/history.rs:563+` | dedupe by message id, ordering and limits, `messages_since` (boundary, limit keeps most recent, ascending order), chat name fallbacks and rules, per-chat and global autoreply flags (including survival across incoming messages), `message_exists`, `update_message_text`, image descriptions (per chat, replaced, deleted with the chat), `delete_chat`, contacts, on-disk open |
 | `transcribe` | 17 | `src/transcribe.rs:299+` | multipart body shape (fields before file part, headers, terminator, bytes unchanged), language rules (`auto`/blank omitted, explicit sent once and trimmed), `audio_suffix` mappings and fallback, MIME sanitisation against header injection, boundary uniqueness, audio validation (empty, oversize), transcription parsing (trim, blank/missing text) |
 | `ui` | 15 | `src/ui.rs:579+` | 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` | 11 | `src/web.rs:411+` | query stripping, route resolution (including `POST /api/relink`, and `GET /api/relink` not found), `X-Wh-Request` header check (any case, missing/wrong value), which routes require it (relink included), `qr_svg` output is an SVG document, request body trimming, empty/invalid UTF-8 rejection and the 1024-byte limit |
-| `main` | 2 | `src/main.rs:208+` | only `--version` / `-V` request the version; `BUILD_VERSION` starts with the package version and names a commit |
+| `web` | 11 | `src/web.rs:413+` | query stripping, route resolution (including `POST /api/relink`, and `GET /api/relink` not found), `X-Wh-Request` header check (any case, missing/wrong value), which routes require it (relink included), `qr_svg` output is an SVG document, request body trimming, empty/invalid UTF-8 rejection and the 1024-byte limit |
+| `main` | 2 | `src/main.rs:218+` | only `--version` / `-V` request the version; `BUILD_VERSION` is `<major>.<minor>.` plus an all-digit build number (commit count) and a commit hash in parentheses |
 | `config_watch` | 1 | `src/config_watch.rs:54+` | modification-time snapshots are `None` for a missing file and change when the file is created and deleted |
 
-Total: 235 test functions — 234 `#[test]` run by `cargo test` plus the
-ignored `#[tokio::test]` live relink test. Relink, configuration reload and
-F3 targeting have no tests at the `App` level.
+Total: 238 test functions — 237 `#[test]` run by `cargo test` plus the
+ignored `#[tokio::test]` live relink test. Relink, configuration reload, the
+web panel avatar fetch and F3 targeting have no tests at the `App` level, and
+the WhatsApp offline presence and `profile_picture_url` are untested.
 
 ## Test isolation
 

+ 83 - 68
openwiki/workflows/ai-autoreply.md

@@ -1,7 +1,7 @@
 ---
 type: workflow
 title: AI auto-reply workflow
-description: End-to-end trace of the AI autoreply — enabling it per chat (F3 on the selected or open chat) 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, voice-note transcriptions and image descriptions processed once and reused, the startup catch-up, building the AI context from SOUL.md, the no_reply tool, retries and sending the reply.
+description: End-to-end trace of the AI autoreply — enabling it per chat (F3 on the selected or open chat) 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, voice-note transcriptions and image descriptions processed once and reused, the startup catch-up, building the AI context from SOUL.md with today's date, the no_reply tool, retries and sending the reply.
 tags: [ai, autoreply, workflow, vision, transcription, reply-gate, reply-delay, catch-up, image-description]
 sources:
   - id: openwiki-source-9fec9d1af408501c9c455161
@@ -14,7 +14,10 @@ sources:
     resource: repo://src/config.rs
   - id: openwiki-source-ce0c4ea8cd3b15431fa691ce
     resource: repo://src/history.rs
-generated: { by: "opencode", at: "2026-10-24T16:39:50.206Z" }
+generated: { by: "claude-code", at: "2026-10-25T12:54:53.684Z" }
+verified:
+  - by: openwiki/0.7.2
+    at: 2026-10-25T12:54:53.684Z
 ---
 
 # AI auto-reply workflow
@@ -40,7 +43,8 @@ WaEvent::Message { message, image, audio }
                                                -> AppEvent::AudiosTranscribed -> text stored
        images without description?  --yes--> download + describe_image (parallel, max 5)
                                                -> AppEvent::ImagesDescribed -> description stored
-  -> start_reply: messages_since(yesterday 00:00, 200) -> mark @[te] -> build_turns
+  -> start_reply: messages_since(yesterday 00:00, 200) -> mark @[te] -> today_label
+                  -> build_turns
   -> AiClient::complete (tools: [no_reply], up to ai_retries retries)
   -> AppEvent::AiReply
        NoReply -> status "AI: nessuna risposta necessaria"
@@ -53,23 +57,23 @@ WaEvent::OfflineSyncCompleted (first time only)
 ## 1. Enabling
 
 There are two independent switches, and a chat is **enabled** when either is
-on (`autoreply_enabled`, `src/app.rs:858-860`):
+on (`autoreply_enabled`, `src/app.rs:904-910`):
 
 - **Per chat** — F3 toggles the flag of the focused chat: the selected row
   of the (filtered) list when the list has focus, the open chat when the
   input has focus (`toggle_autoreply` / `toggle_chat_autoreply`,
-  `src/app.rs:1348-1378`), so a chat can be enabled without opening it; the
+  `src/app.rs:1397-1427`), so a chat can be enabled without opening it; 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 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:1894-1902`), otherwise the status shows
+  `src/app.rs:1968-1976`), 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:1433-1444`);
+  `global_autoreply` (`toggle_global_autoreply`, `src/app.rs:1507-1522`);
   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
@@ -82,7 +86,7 @@ never answered.
 The AI setup itself follows the configuration files live: when
 `config.toml`, `SOUL.md` or `VISION.md` change, the configuration is
 reloaded and the AI client rebuilt within about 2 seconds
-(`apply_reloaded_config`, `src/app.rs:422-434`, see
+(`apply_reloaded_config`, `src/app.rs:462-474`, see
 [Configuration](../operations/configuration-and-data.md#live-reload)).
 Replies already running keep the previous client; later schedules, contexts
 and requests use the new prompt, models, `reply_delay_seconds`,
@@ -90,66 +94,66 @@ and requests use the new prompt, models, `reply_delay_seconds`,
 leave the AI unconfigured, every autoreply path stops as described below,
 but the persisted flags are kept.
 
-`supports_autoreply` (`src/autoreply.rs:105-108`) accepts private chats and
+`supports_autoreply` (`src/autoreply.rs:111-114`) accepts private chats and
 groups:
 
 - private: JID server `s.whatsapp.net` or `lid` (`is_private_chat`,
-  `src/autoreply.rs:83-92`);
-- group: JID server `g.us` (`is_group_chat`, `src/autoreply.rs:94-103`);
+  `src/autoreply.rs:89-98`);
+- group: JID server `g.us` (`is_group_chat`, `src/autoreply.rs:100-109`);
 - broadcasts (including `status@broadcast`), newsletters and malformed JIDs
   are excluded.
 
 ## 2. Trigger and ReplyGate rules
 
 Only messages that `History::save_message` reports as new reach
-`maybe_autoreply` (`on_message`, `src/app.rs:485-499`; `maybe_autoreply`,
-`src/app.rs:509-532`), so duplicates and history-sync re-deliveries of
+`maybe_autoreply` (`on_message`, `src/app.rs:531-550`; `maybe_autoreply`,
+`src/app.rs:555-580`), 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:987-989`), refreshes the own JIDs if
+system prompt (`ai_setup`, `src/app.rs:1033-1039`), refreshes the own JIDs if
 unknown and computes `enabled` (global or per chat).
 
 `ReplyGate::should_schedule(message, enabled)`
-(`src/autoreply.rs:189-191`) returns `true` only when **all** hold
+(`src/autoreply.rs:195-197`) returns `true` only when **all** hold
 (`is_reply_candidate` and `meets_content_rules`,
-`src/autoreply.rs:247-265`):
+`src/autoreply.rs:253-282`):
 
 | 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 (`sender_allowed`, `src/autoreply.rs:178-180`) |
+| 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:184-186`) |
 | Supported chat | private chat or group (`supports_autoreply`) |
-| No backlog | `timestamp >= started_at`, the process start time (`is_backlog`, `src/autoreply.rs:198-200`) |
+| No backlog | `timestamp >= started_at`, the process start time (`is_backlog`, `src/autoreply.rs:204-206`) |
 | Has content | text not blank and not `[media]`/`[messaggio non supportato]`; voice notes and images do trigger |
 
 The self-chat is detected by comparing the chat JID with the account's own
 PN/LID JIDs, ignoring a `:N` device suffix (`is_self_chat`,
-`src/autoreply.rs:154-157`; `strip_device_suffix`,
-`src/autoreply.rs:299-309`). Groups follow exactly the same rules as private
+`src/autoreply.rs:160-163`; `strip_device_suffix`,
+`src/autoreply.rs:305-315`). 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:539-542`) marks
-the chat pending in the gate (`mark_pending`, `src/autoreply.rs:208-210`)
-and `spawn_reply_timer` (`src/app.rs:1586-1591`) emits
+When `should_schedule` passes, `schedule_reply` (`src/app.rs:585-590`) marks
+the chat pending in the gate (`mark_pending`, `src/autoreply.rs:214-216`)
+and `spawn_reply_timer` (`src/app.rs:1660-1670`) 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:634-641`) takes the images and audios kept
+`handle_reply_due` (`src/app.rs:680-697`) takes the images and audios kept
 for the chat and, if the chat is still pending (a local deletion removes the
-pending mark), calls `prepare_reply` (`src/app.rs:656-679`). Its
-`reply_context` (`src/app.rs:868-880`) drops the reply — and `finish_reply`
+pending mark), calls `prepare_reply` (`src/app.rs:702-729`). Its
+`reply_context` (`src/app.rs:914-930`) 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:237-239`): the owner already
+  (`answered_by_owner`, `src/autoreply.rs:243-245`): the owner already
   answered, so the AI stays silent.
 
 Otherwise it transcribes the new audios and describes the new images first
@@ -157,10 +161,10 @@ Otherwise it transcribes the new audios and describes the new images first
 
 ### After the reply
 
-`AiReply` is always followed by `finish_reply` (`src/app.rs:837-852`), which
+`AiReply` is always followed by `finish_reply` (`src/app.rs:883-900`), 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:1696-1698`) that itself passes
+(`arrived_after`, `src/app.rs:1770-1772`) that itself passes
 `should_schedule`, a **new** reply is scheduled (with a fresh delay, keeping
 the media received meanwhile); otherwise the kept images and audios are
 dropped. So messages that arrive while a reply is being generated are
@@ -173,13 +177,13 @@ 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:924-930`) runs `catch_up` once per process (`catch_up_done`;
+(`src/app.rs:970-980`) runs `catch_up` once per process (`catch_up_done`;
 later reconnections skip it).
 
-`catch_up` / `catch_up_chats` (`src/app.rs:940-981`) look at every enabled
+`catch_up` / `catch_up_chats` (`src/app.rs:986-1029`) 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:278-290`) accepts it only when:
+`ReplyGate::catch_up_due` (`src/autoreply.rs:284-296`) accepts it only when:
 
 - `catch_up_seconds` (default 7200) is positive — `0` disables the catch-up;
 - the chat is not a group;
@@ -194,23 +198,23 @@ the reply delay, together with the images and audios of its offline messages
 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:571-581`), and offline audios
-transcribed the same way (`transcribe_late_audio`, `src/app.rs:616-626`).
+(`describe_late_image`, `src/app.rs:617-631`), and offline audios
+transcribed the same way (`transcribe_late_audio`, `src/app.rs:662-676`).
 
 ## 5. Media: image descriptions and voice-note transcriptions
 
 Media bytes are never sent to the text model. Each image or audio sent by an
 allowed sender in an enabled chat is kept by `keep_image`
-(`src/app.rs:550-564`) or `keep_audio` (`src/app.rs:590-609`; audios are
+(`src/app.rs:596-612`) or `keep_audio` (`src/app.rs:636-657`; audios are
 ignored when `transcribe_key` is unset) and, just before the reply that
 covers it, processed **once**:
 
 - audios are transcribed first by the configured `audio/transcriptions`
   endpoint (`spawn_transcriptions` → `spawn_audio_transcriptions`,
-  `src/app.rs:688-715`, `src/app.rs:1796-1830`), in parallel. The
+  `src/app.rs:734-763`, `src/app.rs:1870-1908`), in parallel. The
   transcription is appended to the stored message text as
   `[messaggio vocale] <text>` (`handle_audios_transcribed`,
-  `src/app.rs:784-829`, via `History::update_message_text`), so it enters
+  `src/app.rs:830-878`, via `History::update_message_text`), so it enters
   every later context; failures stay untranscribed and the status shows
   `Trascrizione vocale fallita (N): <first error>`. When the reply was
   waiting for them, the postponed images are described next, or the reply
@@ -222,22 +226,22 @@ covers it, processed **once**:
   stored in the `image_descriptions` table and reused in every later
   context.
 
-`select_media_to_describe` (`src/app.rs:1655-1676`) keeps only the items
+`select_media_to_describe` (`src/app.rs:1729-1752`) keeps only the items
 whose message is in the reply context and that are not already processed or
 in progress (`describing` / `transcribing`, plus stored descriptions or
 transcribed texts, respectively `images_to_describe` and
-`audios_to_transcribe`, `src/app.rs:888-918`), and caps them to the most
+`audios_to_transcribe`, `src/app.rs:934-966`), and caps them to the most
 recent ones: **5 images** (`MAX_IMAGES_PER_REPLY`) or **3 audios**
 (`MAX_AUDIOS_PER_REPLY`); older media of the same batch are dropped for
 good, to cap the cost of albums and bursts of voice notes. Without
 `openai_vision_model` images are not described; without `transcribe_key`
-audios are not transcribed (`descriptions_apply`, `src/app.rs:1855-1857`).
+audios are not transcribed (`descriptions_apply`, `src/app.rs:1929-1932`).
 
 Both processing kinds run in parallel with one task per item; each download
 (`download_image_with_retry` / `download_audio_with_retry`,
-`src/app.rs:1737-1784`) makes up to 3 attempts, each abandoned after 60 s,
+`src/app.rs:1811-1866`) makes up to 3 attempts, each abandoned after 60 s,
 waiting 2 s × attempt between them, and the bytes stay in memory.
-`handle_images_described` (`src/app.rs:747-770`) stores each description
+`handle_images_described` (`src/app.rs:793-825`) 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
@@ -246,40 +250,46 @@ pending, the reply then starts (re-checking `reply_context`).
 
 ## 6. Building the context
 
-`start_reply` (`src/app.rs:1009-1028`) loads the context with
+`start_reply` (`src/app.rs:1056-1077`) loads the context with
 `History::messages_since(chat_jid, context_start(now), CONTEXT_MESSAGES)`:
 
-- `context_start` (`src/autoreply.rs:466-468`) is local midnight of
+- `context_start` (`src/autoreply.rs:480-482`) is local midnight of
   **yesterday** (falls back to `now` minus two days if local time cannot be
-  resolved, `src/autoreply.rs:475-480`);
+  resolved, `src/autoreply.rs:489-494`);
 - `CONTEXT_MESSAGES` = 200 (`src/autoreply.rs:15`) caps the window,
   keeping the most recent messages in ascending order
   ([History store](../concepts/history-store.md)).
 
 It records the last context message in `answered_up_to`, replaces every
 `@<own number or LID>` in the texts with `@[te]` (`mark_own_mentions`,
-`src/autoreply.rs:165-170`; `replace_mention`,
-`src/autoreply.rs:444-456`; a longer number is never matched) and calls
-`build_turns(system_prompt, messages, TurnContext)` (`src/autoreply.rs:495-503`)
-with the self-chat/group flags, the chat name and the stored image
-descriptions:
-
-- first a `System` turn (`system_turn_content`, `src/autoreply.rs:427-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
+`src/autoreply.rs:171-176`; `replace_mention`,
+`src/autoreply.rs:458-470`; a longer number is never matched), formats
+today's local date with `today_label(now)` (`src/autoreply.rs:496-504`:
+`DATE_FORMAT` = `%d/%m/%Y`, e.g. `25/10/2026`; an empty string when the local
+time cannot be resolved) and calls
+`build_turns(system_prompt, messages, TurnContext)` (`src/autoreply.rs:519-527`)
+with the self-chat/group flags, the chat name, today's date and the stored
+image descriptions:
+
+- first a `System` turn (`system_turn_content`, `src/autoreply.rs:436-450`):
+  the `SOUL.md` prompt as written; then, when the date is not empty, the line
+  `Oggi è <dd/mm/yyyy>.` (`DATE_LINE_PREFIX`, `src/autoreply.rs:49-53`), in
+  every kind of chat; 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;
+  separated by a blank line (`ROLE_NOTE_SEPARATOR`), so the model can resolve
+  relative dates ("domani", "venerdì") without the app interpreting them;
 - then the last `CONTEXT_MESSAGES` messages, skipping blank texts;
-- roles (`turn_role`, `src/autoreply.rs:318-321`): in normal chats and
+- roles (`turn_role`, `src/autoreply.rs:324-327`): 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:387-418`);
-- only `User` turns are rewritten (`user_content`, `src/autoreply.rs:370-379`),
+  (`sender_label`, `message_to_turn`, `src/autoreply.rs:395-428`);
+- only `User` turns are rewritten (`user_content`, `src/autoreply.rs:378-387`),
   with plain facts — how to react is left to `SOUL.md`:
   - voice note **without** a transcription →
     `[Il contatto ha inviato un messaggio vocale che non puoi ascoltare]`;
@@ -291,9 +301,13 @@ descriptions:
     appended as `\nDidascalia: <caption>`;
   - any other text is kept as is.
 
+The date is computed when the reply starts (after the reply delay and any
+media processing), in the local time zone of the machine running the bot; it
+carries no time of day.
+
 ## 7. Request, `no_reply` and retries
 
-`spawn_ai_request` (`src/app.rs:1600-1611`) calls `AiClient::complete`, which
+`spawn_ai_request` (`src/app.rs:1674-1692`) 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 —
@@ -312,28 +326,29 @@ after all retries. The request task is awaited by a second task, so
 ## 8. Sending
 
 A text reply is handled by `send_ai_reply_if_enabled`
-(`src/app.rs:1064-1077`), which re-checks two things:
+(`src/app.rs:1113-1130`), 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:1084-1090`): if a hand-written
+- `owner_answered_meanwhile` (`src/app.rs:1133-1142`): 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:1096-1120`) stores it as an own message with
+`handle_sent` (`src/app.rs:1145-1175`) 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 gate rules, pending tracking, catch-up, mention marking and turn building
-are covered by `src/autoreply.rs:505+`; enable rules, `web_chats`, media
-selection, failure statuses, `arrived_after` and `manual_reply_after` by
-`src/app.rs:1941+`; image descriptions, text updates and the global flag
-storage by `src/history.rs`; the `no_reply` tool, descriptions and the retry
-policy by `src/ai.rs`; the transcription client by `src/transcribe.rs`. See
+The gate rules, pending tracking, catch-up, mention marking, `today_label`
+and turn building (including the date line) are covered by
+`src/autoreply.rs:529+`; enable rules, `web_chats`, media selection, failure
+statuses, `arrived_after` and `manual_reply_after` by `src/app.rs:2041+`;
+image descriptions, text updates and the global flag storage by
+`src/history.rs`; the `no_reply` tool, descriptions and the retry policy by
+`src/ai.rs`; the transcription client by `src/transcribe.rs`. See
 [Unit tests](../testing/unit-tests.md).

+ 1 - 1
openwiki/workflows/index.md

@@ -1,4 +1,4 @@
 # Files
 
-- [AI auto-reply workflow](ai-autoreply.md) - End-to-end trace of the AI autoreply — enabling it per chat (F3 on the selected or open chat) 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, voice-note transcriptions and image descriptions processed once and reused, the startup catch-up, building the AI context from SOUL.md, the no_reply tool, 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 on the selected or open chat) 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, voice-note transcriptions and image descriptions processed once and reused, the startup catch-up, building the AI context from SOUL.md with today's date, the no_reply tool, retries and sending the reply.
 - [WhatsApp relink and pairing](relink-and-pairing.md) - End-to-end trace of pairing wh as a WhatsApp linked device and of re-pairing without restarting — F8 twice in the TUI or "Nuovo QR" in the web panel, App::start_relink, WaHandle::relink (logout, stop, session.db deletion or in-place reset, new unpaired client), the new QR on the TUI pairing screen and in the web pairing card, failure behaviour and tests.

+ 38 - 25
openwiki/workflows/relink-and-pairing.md

@@ -12,10 +12,10 @@ sources:
     resource: repo://src/web.rs
   - id: openwiki-source-7ea5eac45e5030178a6ea05b
     resource: repo://src/whatsapp.rs
-generated: { by: "openwiki/0.7.2", at: "2026-10-24T16:42:08.550Z" }
+generated: { by: "claude-code", at: "2026-10-25T12:54:53.684Z" }
 verified:
   - by: openwiki/0.7.2
-    at: 2026-10-23T21:26:24.156Z
+    at: 2026-10-25T12:54:53.684Z
 ---
 
 # WhatsApp relink and pairing
@@ -39,6 +39,7 @@ TUI: F8 -> confirm prompt -> F8          Web: "Nuovo QR" -> confirm() -> POST /a
                       \                                       /
                        +--------> App::start_relink <--------+
                                    (ignored while relinking)
+                                   wa_connected = false,
                                    screen = Connecting, qr cleared,
                                    status "Cancellazione della sessione in corso...",
                                    own_jids_known = false
@@ -56,7 +57,9 @@ TUI: F8 -> confirm prompt -> F8          Web: "Nuovo QR" -> confirm() -> POST /a
                                           |                                   \
                      TUI pairing screen (half-block QR)        GET /api/state: screen "pairing",
                                           |                    qr_svg -> web pairing card
-                     phone scans -> WaEvent::Connected -> Screen::Main, own JIDs refreshed
+                     phone scans -> WaEvent::Connected -> Screen::Main, wa_connected, own JIDs refreshed
+                                          |
+                     after 5 s (and after SelfPushNameUpdated): presence "unavailable"
 ```
 
 ## 1. Requesting a relink
@@ -64,28 +67,29 @@ TUI: F8 -> confirm prompt -> F8          Web: "Nuovo QR" -> confirm() -> POST /a
 **TUI.** F8 works on every screen (connecting, pairing and main), so a stuck
 or expired pairing can be restarted from the pairing screen itself, whose key
 help reads `F8: nuovo QR   F10: esci` ([Terminal UI](../architecture/terminal-ui.md)).
-`handle_key` checks F8 right after F10 (`src/app.rs:1130-1141`).
-`request_relink` (`src/app.rs:395-403`) cancels a pending chat deletion; the
+`handle_key` checks F8 right after F10 (`src/app.rs:1179-1190`).
+`request_relink` (`src/app.rs:435-443`) cancels a pending chat deletion; the
 first press sets `pending_relink` and shows
 `Cancellare la sessione WhatsApp e rifare l'abbinamento? F8 conferma, Esc annulla`;
 the second press calls `start_relink`. Any other key in between clears the
-flag through `cancel_pending_relink` (`src/app.rs:409-415`) with the status
+flag through `cancel_pending_relink` (`src/app.rs:449-455`) with the status
 `Nuovo abbinamento annullato`; an Esc used that way does nothing else, other
 keys are then handled normally.
 
-**Web panel.** "Nuovo QR" (`relinkBot`, `src/web_page.html:707-718`) asks
+**Web panel.** "Nuovo QR" (`relinkBot`, `src/web_page.html:791-802`) asks
 `Cancellare la sessione WhatsApp e rifare l'abbinamento? Il dispositivo verrà
 scollegato dal telefono.` and posts `/api/relink` with the mandatory
-`X-Wh-Request: 1` header (`src/web.rs:171-174`, `src/web.rs:238-259`).
+`X-Wh-Request: 1` header (`src/web.rs:173-176`, `src/web.rs:240-261`).
 `App::handle_web_request` maps `WebCommand::Relink` straight to
 `start_relink` — the browser dialog replaces the double F8
 ([Application state](../architecture/app-state.md#web-panel-requests)).
 
 ## 2. Starting it in `App`
 
-`start_relink` (`src/app.rs:360-375`) clears `pending_relink` and returns at
+`start_relink` (`src/app.rs:399-415`) clears `pending_relink` and returns at
 once if `relinking` is already set, so repeated requests during a relink are
-ignored. Otherwise it sets `relinking`, switches to `Screen::Connecting`,
+ignored. Otherwise it sets `relinking`, clears `wa_connected` (so no web panel
+avatar fetch starts while no session runs), switches to `Screen::Connecting`,
 drops the stored QR, shows `Cancellazione della sessione in corso...` and
 resets `own_jids_known`, because the next pairing may be a different account
 and the [autoreply gate](ai-autoreply.md) must learn its JIDs again. It then
@@ -99,63 +103,72 @@ every 2 s.
 
 ## 3. Replacing the session
 
-`WaHandle::relink` (`src/whatsapp.rs:261-270`, see
+`WaHandle::relink` (`src/whatsapp.rs:267-276`, see
 [WhatsApp client](../integrations/whatsapp-client.md#relink)):
 
 1. **Take** the current `Session` out of the shared slot. From now until
    step 4 succeeds every client operation fails with
    `WhatsApp session not running`: manual and AI sends end with
    `Invio fallito: ...`, media downloads fail, `own_jids` is empty.
-2. **Stop** it (`Session::stop`, `src/whatsapp.rs:223-231`): `logout`
+2. **Stop** it (`Session::stop`, `src/whatsapp.rs:229-237`): `logout`
    deregisters the device on the phone when connected (best effort) and
    disconnects; then it waits up to `STOP_TIMEOUT_SECONDS` (10 s) for the
    client run loop to report it stopped, and aborts the translator task,
    which holds a reference to the client.
 3. **Reset** the session database (`reset_session_files`,
-   `src/whatsapp.rs:374-386`): delete `session.db` and its `-wal` / `-shm`
+   `src/whatsapp.rs:400-412`): delete `session.db` and its `-wal` / `-shm`
    side files. If the deletion fails — on Windows `whatsapp-rust` keeps the
    SQLite file open until the process exits — `clear_session_tables`
-   (`src/whatsapp.rs:407-428`) opens it with `rusqlite` (5 s busy timeout)
+   (`src/whatsapp.rs:433-454`) opens it with `rusqlite` (5 s busy timeout)
    and, in one transaction, deletes every row of every table except SQLite
    internals and `__diesel_schema_migrations`, so the schema stays valid for
    the next client. If both fail the error reads
    `cannot delete (<io error>) nor clear session database <path>`.
 4. **Start** a new client on the empty database with `open_session`
-   (`src/whatsapp.rs:430-471`) and put it in the slot. It sends its events
+   (`src/whatsapp.rs:456-496`) and put it in the slot. It sends its events
    to the same `WaEvent` channel as the old one, so the forwarder and `App`
    need no rewiring.
 
 Only `session.db` is reset. `history.db` — chats, messages, contacts,
 autoreply flags and image descriptions — is untouched, so chats of the
-previously linked account stay visible until deleted locally.
+previously linked account stay visible until deleted locally. The in-memory
+web panel avatar cache is not cleared either; its entries expire after their
+one-day TTL.
 
 ## 4. Outcome and new QR
 
-`finish_relink` (`src/app.rs:382-388`) clears `relinking` and shows
+`finish_relink` (`src/app.rs:422-428`) clears `relinking` and shows
 `Sessione cancellata: scansiona il nuovo QR` or
 `Nuovo abbinamento fallito: <error>`. After a failure the handle has no
 session: the app stays on the connecting screen and the user can press F8
 twice again (or restart).
 
 The new client emits `WaEvent::Qr` payloads; `handle_wa_event` switches to
-`Screen::Pairing` and stores the payload (`src/app.rs:440-445`). It is shown
+`Screen::Pairing` and stores the payload (`src/app.rs:480-485`). It is shown
 in two places:
 
 - the TUI pairing screen draws it with half-block characters
   (`qr_to_text`), or a "terminal too small" message
   ([Terminal UI](../architecture/terminal-ui.md#qr-code));
 - `App::web_state` renders it to SVG with `web::qr_svg`
-  (`src/web.rs:369-384`) and returns it as `qr_svg` with
+  (`src/web.rs:371-386`) and returns it as `qr_svg` with
   `screen: "pairing"`; the page's pairing card shows it as an image and the
   WhatsApp linking steps, polling every 2 s so rotated codes appear quickly
-  (`src/web_page.html:604-629`).
+  (`src/web_page.html:688-713`).
 
 When the library runs out of QR codes the status shows
 `pairing QR codes exhausted; request a new QR to pair again` — another
 relink starts a fresh sequence. Scanning the code produces
-`WaEvent::Connected`: the app switches to `Screen::Main`, clears the QR,
-shows `Connesso` and fetches the new account's JIDs; the web page hides the
-pairing card and returns to 5 s polling.
+`WaEvent::Connected`: the app sets `wa_connected`, switches to
+`Screen::Main`, clears the QR, shows `Connesso` and fetches the new account's
+JIDs; the web page hides the pairing card, returns to 5 s polling and its
+next request may start the avatar fetch. Five seconds after the connection
+the WhatsApp client sends an "unavailable" presence so the phone keeps its
+notifications; on a first pairing the push name is not known yet, so it is
+sent again after the `SelfPushNameUpdated` event
+([WhatsApp client](../integrations/whatsapp-client.md#presence-and-phone-notifications)).
+A `LoggedOut` event (device unlinked from the phone) clears `wa_connected`
+and returns to the connecting screen.
 
 ## Failure and edge cases
 
@@ -174,10 +187,10 @@ pairing card and returns to 5 s polling.
 
 - `web.rs`: `POST /api/relink` resolves to the relink route, requires the
   `X-Wh-Request` header, and `GET /api/relink` is not found; `qr_svg`
-  produces an SVG document (`src/web.rs:423-444`).
+  produces an SVG document (`src/web.rs:425-446`).
 - `whatsapp.rs`: the ignored live test
   `relink_deletes_the_session_and_offers_a_new_qr`
-  (`src/whatsapp.rs:1238-1258`) starts a real client, waits for a QR,
+  (`src/whatsapp.rs:1293-1313`) starts a real client, waits for a QR,
   relinks, waits for the next QR and checks that the device keys in the two
   payloads differ; run it with `cargo test -- --ignored` (network needed).
 - The `App` side (F8 confirmation, `start_relink`, `finish_relink`) and the