Browse Source

Update AI auto-reply workflow documentation to include voice-note transcriptions and adjust related descriptions. Modify relink and pairing workflow for improved clarity and accuracy. Add INSTRUCTIONS.md file for repository guidance.

softword 1 hour ago
parent
commit
12737b17a8
34 changed files with 1540 additions and 932 deletions
  1. 150 68
      openwiki/.claims/architecture/app-state.json
  2. 1 1
      openwiki/.claims/architecture/runtime-and-event-loop.json
  3. 1 1
      openwiki/.claims/architecture/terminal-ui.json
  4. 106 10
      openwiki/.claims/concepts/history-store.json
  5. 1 1
      openwiki/.claims/integrations/ai-client.json
  6. 1 1
      openwiki/.claims/integrations/web-control-panel.json
  7. 50 36
      openwiki/.claims/integrations/whatsapp-client.json
  8. 40 40
      openwiki/.claims/operations/configuration-and-data.json
  9. 11 11
      openwiki/.claims/quickstart.json
  10. 32 28
      openwiki/.claims/testing/unit-tests.json
  11. 54 50
      openwiki/.claims/workflows/ai-autoreply.json
  12. 1 1
      openwiki/.claims/workflows/relink-and-pairing.json
  13. 3 3
      openwiki/.last-update.json
  14. 60 60
      openwiki/.page-manifest.json
  15. 1 0
      openwiki/INSTRUCTIONS.md
  16. 252 128
      openwiki/architecture/app-state.md
  17. 1 1
      openwiki/architecture/index.md
  18. 27 26
      openwiki/architecture/runtime-and-event-loop.md
  19. 28 29
      openwiki/architecture/terminal-ui.md
  20. 265 85
      openwiki/concepts/history-store.md
  21. 1 1
      openwiki/concepts/index.md
  22. 1 1
      openwiki/index.md
  23. 102 49
      openwiki/integrations/ai-client.md
  24. 2 2
      openwiki/integrations/index.md
  25. 22 22
      openwiki/integrations/web-control-panel.md
  26. 58 53
      openwiki/integrations/whatsapp-client.md
  27. 58 43
      openwiki/operations/configuration-and-data.md
  28. 1 1
      openwiki/operations/index.md
  29. 15 13
      openwiki/quickstart.md
  30. 1 1
      openwiki/testing/index.md
  31. 22 21
      openwiki/testing/unit-tests.md
  32. 149 122
      openwiki/workflows/ai-autoreply.md
  33. 1 1
      openwiki/workflows/index.md
  34. 22 22
      openwiki/workflows/relink-and-pairing.md

+ 150 - 68
openwiki/.claims/architecture/app-state.json

@@ -1,14 +1,14 @@
 {
   "schemaVersion": 1,
-  "pageVersion": "sha256:5a08c085203eed7c6af86a5ee968fd9f61a42c7dec64787f196a9cfc4a404452",
+  "pageVersion": "sha256:37292ba995a3a70e902fd75c163ea0039a8724324712200d9fa529205b98e321",
   "claims": [
     {
       "id": "claim_daef343b267c45a987738a3daf7e404d",
-      "statement": "AppEvent has variants Key, Redraw, Wa, AiReply, ImagesDescribed, ReplyDue, Sent, Web, ConfigReloaded and Relinked; AiReply carries a Reply or an error, 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 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.",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L106-L141",
-          "version": "repo-lines-v1:sha256:de9462259da6519ea1c124deddbc8bb27a8c18b17c07eed5377af48ee5589801:eyJzZWxlY3RlZExpbmVDb3VudCI6MzYsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImI4OWMyOWE3NjJmOGZhMmEzZmY0NmY5MGZiZDY3N2QyYTI3MjViMjM3NTYxNjIwODdkOTRmMWI2MDg2Yzk5YzEiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjZmMGExNjA2YWZlMjU3OTU2NWMyMWNkOGU1ZjUyNzljMmY0ZWMwZWMyMjZmZjM5YjljNmY2MjllZTNkYjk0NTEiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjJkNTQzZDNkZjU4ZmJmODdkZjViYjZlOGNmZDQwYzBkNjY5MjY3MWZmMTAyOWRmZDM4OWY0MWIyNDY2OTZiY2EifQ"
+          "resource": "repo://src/app.rs#L117-L169",
+          "version": "repo-lines-v1:sha256:4e60f6fe2040a9712b61baea9f6be8ae53ca7b98d9ec4ad1f2fac5320b51dd4b:eyJzZWxlY3RlZExpbmVDb3VudCI6NTMsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImI4OWMyOWE3NjJmOGZhMmEzZmY0NmY5MGZiZDY3N2QyYTI3MjViMjM3NTYxNjIwODdkOTRmMWI2MDg2Yzk5YzEiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjZmMGExNjA2YWZlMjU3OTU2NWMyMWNkOGU1ZjUyNzljMmY0ZWMwZWMyMjZmZjM5YjljNmY2MjllZTNkYjk0NTEiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjJlYzJiMDUyMWI2NDA2YzM3MGE3MzdmMTZhYTU0NDBkZDcyZDE1YWMxY2E2OGY5MmI1ZDc5NzUxM2I0NDk0NGQifQ"
         }
       ]
     },
@@ -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#L151-L171",
+          "resource": "repo://src/app.rs#L171-L191",
           "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 pending deletion or relink, 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, and a ReplyGate created at the current time.",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L246-L286",
-          "version": "repo-lines-v1:sha256:94b043b31861b7f2e517e87907e09881fb6d2f163bbb08ec7784e45a44e7d767:eyJzZWxlY3RlZExpbmVDb3VudCI6NDEsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjdmNTYzZjU4ZTBlOWQyYjgxMjgwMTZhOWEyZGZhMTU2YjgzNTM0NWYzOTkzYTVhYjdkNjVhNWU4ODFmMDg0Y2UiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjBhOGJlZTA0NjIzODBmZjdmN2Y0ZDZhYzE2MGViODRmYjk3YjgwZGRjMjM2ZGFmZWY4NTQ4OTYxYjcwZDQ4ZmQiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImU3M2U3ZTFjNDIxZDk5NzQ5ZWVmNzNjNjJiNmE4ODAyODhjNGNhZDM4ZTYwZWQyMTNjNmM1ZDI1ZjAyMDU3OGMifQ"
+          "resource": "repo://src/app.rs#L269-L317",
+          "version": "repo-lines-v1:sha256:9c70d97117149450bc5590493b9c129d6759a93c1d30ddfc08f8f3cf47891eec:eyJzZWxlY3RlZExpbmVDb3VudCI6NDksImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImRjOTk1ZDBmZjIyZDRiZjUyYjZjNDM1YjMxY2Q0ZGFhM2U1OGE4ZjVkMzAxMzgwMjk1MDAyZWY2MDMzOWIwZjYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjQ5NWU2MTMyNDM5Mjg3ZWRkNTY5ZGNmZDI3Y2JkODAyYTBjOWE4ZjYyZWUyNzM0NGQ0NDc5Y2U5YzJkM2IzN2UiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImU3M2U3ZTFjNDIxZDk5NzQ5ZWVmNzNjNjJiNmE4ODAyODhjNGNhZDM4ZTYwZWQyMTNjNmM1ZDI1ZjAyMDU3OGMifQ"
         }
       ]
     },
     {
       "id": "claim_bf08f07b96ed49c2b688852a35a263f5",
-      "statement": "handle_wa_event maps Qr to Pairing, Connected to Main with status Connesso, LoggedOut to Connecting, OfflineSyncCompleted to the startup catch-up, and routes Contact and Message { message, image } events.",
+      "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.",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L406-L429",
-          "version": "repo-lines-v1:sha256:5d1dce3b181c3675faeb3b14606340ed867acb60a349b69ef26369d3856c49c6:eyJzZWxlY3RlZExpbmVDb3VudCI6MjQsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjZkOGNmMDJkODU2OWY1YTQ3ZjRiNmY0Zjc5MzBmNzkzMTczM2VmNGE4NDMwODNmYmZjMzAzNmNiM2JiMjc5MGQiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImM5NDkyZWM5YTM2ZThkODU0NjE1MmI4MjJkNTdmZDIwMDk4OTE1MDMwYTIwZDliZGExZDVmYWQ0NGY4ZjFiNWYiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImI2ZDczY2I1NTUwODdmZGQwZTc4MzhjZjM4MWZmZmRmMzljMzk2M2VmZWJhMGE0ZjQyNzg4OWE0MGUwODYwZWQifQ"
+          "resource": "repo://src/app.rs#L436-L463",
+          "version": "repo-lines-v1:sha256:6cb99e0ed4d9cc3d67855b607fc8f10d28c57e26bd5863778dfad3aa44c3ce88:eyJzZWxlY3RlZExpbmVDb3VudCI6MjgsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjgwNjYyNjdlNzM4MTkwMDlkOWYwMTZiM2IwMmY0YWEwMTM4MzI1MDUyZjg2ODA4M2VlZmZlMGI4MTQzNGI1YzgiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjdkNTdjYjhjOGU1NTg3MTQ3MDZiYTY4NDc1YTBiNTMzOGQwYzYyM2VjOTBhOWEyMDk1OTg1ZDAxMmIyZGZmZjkiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImI2ZDczY2I1NTUwODdmZGQwZTc4MzhjZjM4MWZmZmRmMzljMzk2M2VmZWJhMGE0ZjQyNzg4OWE0MGUwODYwZWQifQ"
         }
       ]
     },
@@ -47,8 +47,8 @@
       "statement": "on_message returns early when History::save_message reports the message was not new.",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L450-L459",
-          "version": "repo-lines-v1:sha256:39169d7955b19430b175fe5273033b79acede8ec3c2de6b4a8998c3201160caa:eyJzZWxlY3RlZExpbmVDb3VudCI6MTAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6Ijk2ZjNjMTFjYTY5OWY4OTA5MTUwMjAyYzhkZTVlMjdlZjAwNTIyOGRmZmNjMzAzOTlmNDg4OTdmYzdhOTVlMTEiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjdmYzAyNDRhNGJkOTI4YWRmMDE3YjljOWQ0ODIxYjU4NjgwZGRhMmI2YTBhZDEyYWU2MWVhNDBlOWZkMjM3OGMiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjJhNDYyZmZiOTg5MjlkZTUyYmFlMTM0ZWVmMWRkZjYzNTRiODVlNGQ4MzE2MzlkMzZhMDIyMzA5NDcwNDYyZDYifQ"
+          "resource": "repo://src/app.rs#L485-L499",
+          "version": "repo-lines-v1:sha256:a70d770a3f3dc3ed4a57db8f2b971da1b9f50afa36fc45185c4f5c34bcf1eba0:eyJzZWxlY3RlZExpbmVDb3VudCI6MTUsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjBlYWIwYmIzYTMyYTBlZGUzMGY0ZTk1MmQ1MjcxYzRhMTgwMTZjMGU4Y2M4N2VhYmE1ODA1OTYyZDNjOGE4NTYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjgyODRmZjk5YWRlYTg2MmRlOTFhNTQwZTNjOTcwNzc4NWU0ODY1ODg2YjZlOTFmMzFiNzdmNzdlOGQzNzI4OGMiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjFjMGQxZmNhMmFmODQ5N2RiMDdiZDk4NWM2YjJkNmQwOGM2MjA2ZDNiN2E4ODY3MjcyNGUwM2I4YzI3YTU5OWUifQ"
         }
       ]
     },
@@ -57,11 +57,11 @@
       "statement": "F10 quits and F8 requests or confirms a relink on any screen; any other key cancels a pending relink and an Esc doing so is consumed; non-main screens ignore other keys; on the main screen F2 switches the list mode, F3 toggles the focused chat's autoreply and F4 the global autoreply, Tab switches focus, and Ctrl command chords are swallowed.",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L23-L32",
+          "resource": "repo://src/app.rs#L24-L33",
           "version": "repo-lines-v1:sha256:683c16567f4074fbe88d85ef8581c16739d5ca0531826b0fb49c5df95ac18f20:eyJzZWxlY3RlZExpbmVDb3VudCI6MTAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImJkMTkzOTVkMDA1MWQ2YTA4MzBkMmQ5OWZhNTk4ZDU4N2EyOWZkNTA0OTkwNGQ1OWNiNTI2YmQ3Yzc4ODBiMGUiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6ImFmNjIzNzA4OWI5MWQxMDdiNjY0NTc5NzNhOGJhMmE3MWQ4NzA0NGVhOTFlOTE4Y2RlMjA1Zjg2YWNkNDZjOTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjFlZmRkMDA4YTE2MzY2YTNmMTdiNjY1M2IyNzcyMThkYzQ2NTRiMGFjYTE5MGI1YmFiZDcwNmIyM2IxMDQwNzciLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImI2ZmI3YjljNTEzODFiZmNlNmIwYzU4ZTY0ZmZhMjhjODMyNjU5NTkyOWQ1MDNlYTUwYjBlMDRiNTk0N2QwNTMifQ"
         },
         {
-          "resource": "repo://src/app.rs#L884-L929",
+          "resource": "repo://src/app.rs#L1122-L1167",
           "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#L1064-L1088",
+          "resource": "repo://src/app.rs#L1305-L1329",
           "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#L1549-L1557",
+          "resource": "repo://src/app.rs#L1894-L1902",
           "version": "repo-lines-v1:sha256:ba5bea3063970ee3b66b2d54fc0529a98bf3972bf7e9a5cab37c6d0bf4659ff7:eyJzZWxlY3RlZExpbmVDb3VudCI6OSwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiZmFmNzM0NDVmZTU0ZTJjMzIyMDBiMGRiNDhlM2MyOGZlZDgyZDk3NWY0Mzg4MjM0NjA5NjhlZmM4ZmU1ZjJmOSIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDEyY2EzNDVjY2Y3NWJmOWMwODA2YmNlNjk1YmU4ZGU4MDhiNzk5ODQyNTFhN2E1NGQyMDJjZjYxMDFkZDQ1MSIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiOWM5Yjc1NjM5OGI3MTI0NGNmYThkOTk4ZGRlOTQwZWRmODk1MmNkMDEwMTYyMzM1YmEwMzRjYjkxZGZmMjRiZSIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiNDQwM2Q5ZmY0OGI3ZmI2ZDcyM2RlNWM5YjMxODkzMTNlMjQ2NzBhMWE5NjJhYWUxZTYxODRhMjllZmM3MWM1ZCJ9"
         },
         {
-          "resource": "repo://src/app.rs#L1124-L1137",
+          "resource": "repo://src/app.rs#L1365-L1378",
           "version": "repo-lines-v1:sha256:cbcb0d80e707fedd21b3de7081436677468c63c025abb23dab5e5a6ab0ad90cc:eyJzZWxlY3RlZExpbmVDb3VudCI6MTQsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjA0ZTRlYmRmYjA5YmI0NTM3YzBhZDM0MWUxNGY5NTkzYjEwYWE5ZWJiOTM0ZTNiOGJhMGViY2ZiYjlhYjBkZmYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjI1Zjg5YTg3NjBkYWQ3NjUxYmZiZGQ4ZGFkYTM4OGRmZDUwZmU2NDczOGUwMzM4YjIzZTM5NzM4Y2Y1NTRmOWUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImUzZWNmNGIzNzRlN2M0ODFlZjUzODQ1Y2Y1YjlmNDI5MDk3NmQxYTlmNDlkOWY5NWJjNjIxNWEyY2VhMDYyOTAifQ"
         }
       ]
@@ -95,11 +95,11 @@
       "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#L1205-L1232",
+          "resource": "repo://src/app.rs#L1446-L1473",
           "version": "repo-lines-v1:sha256:e52bd5fba06c8fa9c17319299e938b8922b2b431f8015138d79e1dca4d4c26d6:eyJzZWxlY3RlZExpbmVDb3VudCI6MjgsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQwZDJhY2QwNjZiMWU0MDM4ZGMyYzllOTI0YTAwNzlkZDNmOGNlNjllZjY4OTZlMTYzYzYyZThhMDlmYTQzYjAiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjE2MmVlNTdlNDFhOGMyMGFlMDE2NGRmMjYxNDM5ZGNjNDk5ZDM1MjA4MzQxNTE1MTZkMDQ3ZThkZWFlOGE0NjYiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImNjYzVlMzg4ZThmOTY0ZGY1OTFjZDM1Y2FlZjk5ODM0MmRjNTllNGU5OTMwMmMyYmI0MjMyZmMxMzdhM2Q1ZjkifQ"
         },
         {
-          "resource": "repo://src/app.rs#L20-L21",
+          "resource": "repo://src/app.rs#L21-L22",
           "version": "repo-lines-v1:sha256:052992c9f8e9b996b43554cee0a2351140f142d6d9781a2b3217356c40bf6cb1:eyJzZWxlY3RlZExpbmVDb3VudCI6MiwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiODdkOGI3ZDU2NTM2Yjg3MDE0OGY1Y2U5YzQ2YWQxN2FmMzI4ZDQ5NzRjMmNlMTVhODNhOWQ0ODllZTBjNDY5MiIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiMjQwNDg4Y2VlODM4MmUwZTUzMWI5ZjEzZmQwYWE5NDFjNzJkMDM5MGU3ZTg5YzQ0MzA4MTFjYmM3NmFhNjYxMCIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiOWY5NTFjZjJlOTBkY2Y1MDJiMGFhYWNmODA0ODQxY2UyZjk3NjUyMmZlNWI2ZDI1OGY3YjliMDY2ZTY5MDNiNyIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiNDcyMjFlYzQ5YThiY2FiYTFkNzUwNTE0MWZhYzY0MDhkOGZmMmUwMjUyMDgwZTZlMGYwNzQ1MDMzMjY3ZjQwZSJ9"
         }
       ]
@@ -109,7 +109,7 @@
       "statement": "spawn_send sends the text via WaHandle::send_text and emits AppEvent::Sent.",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L1518-L1527",
+          "resource": "repo://src/app.rs#L1863-L1872",
           "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#L826-L852",
+          "resource": "repo://src/app.rs#L1064-L1090",
           "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#L1534-L1542",
+          "resource": "repo://src/app.rs#L1879-L1887",
           "version": "repo-lines-v1:sha256:02a6b90c8547969f5229ba908e64ec2f9fd8d0547f3a7b34404429320b337fe4:eyJzZWxlY3RlZExpbmVDb3VudCI6OSwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiNzcyNTRhNjcwYWJhNGU4ZWFiNDlmM2U1OWYzNjc4NTk0OWM2ZDk1OTFlN2ZkMWY1NjA4NGU3NzBlOWM3NDJlNCIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDEyY2EzNDVjY2Y3NWJmOWMwODA2YmNlNjk1YmU4ZGU4MDhiNzk5ODQyNTFhN2E1NGQyMDJjZjYxMDFkZDQ1MSIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiMGQ3OWM4ZDU3OGFjNjM3NDFmOTgwMTlmMzlmOWRmMDA4M2RkN2YzM2U0ZDNlOWU3ODU1YzZmNmQ2MWQzNjg0MCIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiNTA2ZDU0NTM1OGJmMGQyNTFiNDhiNWZhZTE2YzIyZjkzYzgzNDM5ZTQwNzA0YzRkYzg1MzgyN2ZmNWFiNmYyNiJ9"
         }
       ]
@@ -139,7 +139,7 @@
       "statement": "is_command treats Ctrl as a command only when Alt is not held, because Windows reports AltGr as Ctrl+Alt.",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L1587-L1594",
+          "resource": "repo://src/app.rs#L1932-L1939",
           "version": "repo-lines-v1:sha256:a7e4215d1055bcf132d529dd3c057ef20525fbb22d5a1301c6980d41107b5124:eyJzZWxlY3RlZExpbmVDb3VudCI6OCwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiYzllOWIxZTI5MGI4MzMyZDI5YTMwZjc1N2JjN2E3N2ZlYjZmMjAxYjNjMmM5M2ZiNTQ4MDU1YTFhODVmN2MyZSIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDEyY2EzNDVjY2Y3NWJmOWMwODA2YmNlNjk1YmU4ZGU4MDhiNzk5ODQyNTFhN2E1NGQyMDJjZjYxMDFkZDQ1MSIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiZDg4NTkyMTBlYmNiMTJjZjY4YzYxNjIyMmZjNTYwYzFmNjhlMmZhMzFmNjMwNDkzZGY4MDk1ODg4N2Q1N2FmOSIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiN2JjOThkNmIwZGM0OTJjNTAxNmRmMTA4MDVhM2E1NGViOWMxODRhNWE2MTM3ZTdmNGJlY2ZhMGMyMjA1NTBkMCJ9"
         }
       ]
@@ -149,7 +149,7 @@
       "statement": "start_reply records the last context message as answered, marks own mentions, reads the chat name and stored image descriptions, builds the turns and spawns the AI request.",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L771-L790",
+          "resource": "repo://src/app.rs#L1009-L1028",
           "version": "repo-lines-v1:sha256:0a937f62450210fb1b4a45c2eb392c8533cab8fc5e7f2ca88caec3ba64669dbb:eyJzZWxlY3RlZExpbmVDb3VudCI6MjAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImZkMjA2ZWQ4NGMzNGNhMWE0ZmNmZmQ1NmFlOTBiNDgyZDIyNGM2OWQ0ZmRlMDY4MmI2MTllODNlMDRlNWJhYjYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjEyZTA5MzVlNDEzNWQ5MWEyZGFlMDUxOGE3ZTY5NjQ0MmIxYmM5ZGYxOThiNDA1ZmVmMGQzMjUwY2VmOWU1ZWQiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImQwMjI2ZGQ4YmZkY2JkMDhlNjUxMjUzOGY4MmU5YWNiOTEyNzg2NTgwNDEwMmQ2YjRlMzQ4NmRmMjU4YjMxNmYifQ"
         }
       ]
@@ -159,32 +159,28 @@
       "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#L1345-L1350",
-          "version": "repo-lines-v1:sha256:9d9b7ef08292560e1f25590a1f38732ed3fd2b45ffe522b5928335427699b373:eyJzZWxlY3RlZExpbmVDb3VudCI6NiwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiNWM4Mjk4ODI1Mzg5YTU1NjFkNzUyZjZjZThhMjgyNzlkZTI4MDMyOTVlNTRlY2Y3NGQyODc2NWUwM2E3MDBkNSIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDEyY2EzNDVjY2Y3NWJmOWMwODA2YmNlNjk1YmU4ZGU4MDhiNzk5ODQyNTFhN2E1NGQyMDJjZjYxMDFkZDQ1MSIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiZjE5NGJjNjc2ZDg4NmZhM2MyZDM3ZTNlMGFhM2M4OTEwODc1MWZhYzYzZmQwNzViNDY3MzM3NDZlZGIyM2Q2YiIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiMDVkMmViYWNlOGUxMTQ2MDNlNjJkNzViNWY2ZGM4MWMzMmIzODMyYjhiMGIwZTNkNWFkNjQ5MzIwNDhhNmQzOCJ9"
-        },
-        {
-          "resource": "repo://src/app.rs#L468-L493",
-          "version": "repo-lines-v1:sha256:0f06f0bd1697dfc6eea76bf1331023d0de98cf0face31b358f6defedc38a31d0:eyJzZWxlY3RlZExpbmVDb3VudCI6MjYsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjU5NWFjMWY4MTY1YjkzNmU3MzY3MTRlNTgzM2FmNTc3YWZjMjdkMTUzZGY4NWEwNDRmMzkxNDJiOWI3OTZhODciLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImRiMmY5MGU2YmE0M2NkMDdhNGMzZWRmMWRiZTJiZWI0MmY4NDBjYzAzYjhjNDI3NWQ1ZGEzNDUxYzhkNDlhNDUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjBmZDk1MWYzMzMxMmIwZGIxOTkwNzNjZDIzNDA4MWM3MzMyNDIxZjYwZjRjNmQ4MDA3YmU2YjM1YzMwODYxNGUifQ"
+          "resource": "repo://src/app.rs#L501-L542",
+          "version": "repo-lines-v1:sha256:182b47e758726e40d650a8411663c9043000bdb61933958b51b7e604c5899fe7:eyJzZWxlY3RlZExpbmVDb3VudCI6NDIsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImI3YzcyYzFlOTMzNjgyYzYyY2MyOGVmNzQ1NDMxNDM5OTZiYWVlNTg4NmFkM2I0YzEyNmNkNDI0OGU5MWY4MTAiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImNmMTVlOGU1NjQ1ZDM5YWE4YzRjNmIwZjMwYjAxYjEwYmNmYjYzMWFlOWI3ZDBjNGNkMjFjMzAwZjQ3ODVhNWMiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjBmZDk1MWYzMzMxMmIwZGIxOTkwNzNjZDIzNDA4MWM3MzMyNDIxZjYwZjRjNmQ4MDA3YmU2YjM1YzMwODYxNGUifQ"
         }
       ]
     },
     {
       "id": "claim_817155a979284282b5a78c3baad994c8",
-      "statement": "On ReplyDue the kept images of the chat are taken and, if the chat is still pending, prepare_reply either ends the pending state when no reply is wanted, spawns the description of images still lacking one (the reply follows ImagesDescribed), or starts the reply at once.",
+      "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#L539-L568",
-          "version": "repo-lines-v1:sha256:7dc3d726de8b72086eda0835321c1115c4fd2cd81cc43f8ed74a3ad6704b3b61:eyJzZWxlY3RlZExpbmVDb3VudCI6MzAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjIxMGU4Y2Q4NGRkNGVlMzUzOTU2ODAxODRjZTNlYTEwZDlhMTI3Mjg1Yzk2MDVkY2RmNDJiZjkwOWJkMDZlNjYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImI5NDI0YzQ0ZDRkYWFkZTBkMTA4ZmM3Nzk5MDVlMGI2Mjk4NDJiMjk3ZjRkMWZhYTc5OTIxNTg5NDJlZGY1MzkiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjU3ODQ1MTBjNTZmNWE5YTNiMmY0ZTdkNmZjMDFjNGM4NTEyOWJjYzUzM2U1YTZjZTA2MTYyY2FiMmIwNGY4YzIifQ"
+          "resource": "repo://src/app.rs#L628-L679",
+          "version": "repo-lines-v1:sha256:c98f9109631c043cce400a98fdab67562e0b19285fc5165cca7a94c260fee718:eyJzZWxlY3RlZExpbmVDb3VudCI6NTIsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjhhMGZkOGQ4YmVjYTA3ZDNkNGZkZDViOGFiMWI4NmQ1Y2MwNGVlYzdkNzY3ZTc1Y2Y3ODgxMDk2OGI2N2Q3NDUiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjE2MmVlNTdlNDFhOGMyMGFlMDE2NGRmMjYxNDM5ZGNjNDk5ZDM1MjA4MzQxNTE1MTZkMDQ3ZThkZWFlOGE0NjYiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjM5YzEwY2I0YTNhZmI3NWE4N2VmNmE4MmZmYzVhMjBiZDZiN2U0ZmQ0NDRhMmNjMWRlMGZmZDg3MjljODQzZGUifQ"
         }
       ]
     },
     {
       "id": "claim_c8b0ab491aad4ebf91a3b7e090cdbe2b",
-      "statement": "The startup catch-up runs once per process on OfflineSyncCompleted, marks pending and prepares a reply without delay for each enabled chat (every chat when the global autoreply is on) whose last message passes ReplyGate::catch_up_due within catch_up_seconds, and sets the status 'Risposte di recupero: N' when N > 0.",
+      "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#L688-L743",
-          "version": "repo-lines-v1:sha256:42baaf60c3fa91366d408e42913e5157b90816c6d76274c43152965dd45df15f:eyJzZWxlY3RlZExpbmVDb3VudCI6NTYsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjJiYWY4NjRiNzJmMjM1YjJhNzAxNzllOWRiZTlkYTg1ZmY0NWM1OTg4YjY1MTUzNmQyYjNmMDM0MzRkMTA3YjgiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjJkOWY0YzAxNDlmZTYwZWNjMDY0NDU2Y2VhNTgwNWIyMmI4YjgzZTljYzY0YzVmZTc3YzhjZDNjMzQ5ODg1OWMiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6Ijc4OTU5NGE5NzIzN2Y2NjVkMmU0Y2RhMTIyZmY1NTIyNzJjMjVmZTFhMTI0NmZjMGNkNjllMDQ4MjE2ZTNlYWMifQ"
+          "resource": "repo://src/app.rs#L920-L981",
+          "version": "repo-lines-v1:sha256:42232cc062a5b6425e02103fe331cbe869e08340278bde2cd954474e46206045:eyJzZWxlY3RlZExpbmVDb3VudCI6NjIsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImM5ZmJjMjYxZDk1MDVkNTBjZTU1MWY4ZDI4OTQ2MTZjMWM2MGY3NTlhZTAwYTE1OWY4Yzg3OGVmMWJjM2Y2ZWEiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjUwOTcxZjZhZWVlMGNhYTY2MWUzOGJkMDMzMDYzODk4NjdkOTc1ZjM3MGVhNWE4YWE3N2NmY2E2MzFiYTU0YzkiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6Ijc4OTU5NGE5NzIzN2Y2NjVkMmU0Y2RhMTIyZmY1NTIyNzJjMjVmZTFhMTI0NmZjMGNkNjllMDQ4MjE2ZTNlYWMifQ"
         }
       ]
     },
@@ -193,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#L638-L644",
+          "resource": "repo://src/app.rs#L854-L860",
           "version": "repo-lines-v1:sha256:9190cc2d950eb45e132eae01c1ae01b5fb4af82470bb42fae8b4e0227a315710:eyJzZWxlY3RlZExpbmVDb3VudCI6NywiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiYmMzZGZhMjZjNzdiNTNkZDcyZWZmZGI1MmEyNWFlNGY5YzMwNTdiZTk2NWI0MzRlMTMyOWIwMTM2MGZmNGViNyIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDg5M2Y0MGUzNzgxZWI4OTZjODJkMDQ2YTA4MDY3YTExZTM1Y2RmMmYxZGZhZjI2ZmVhOTBiOGRiNjY3NjQxMyIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiMTYyZWU1N2U0MWE4YzIwYWUwMTY0ZGYyNjE0MzlkY2M0OTlkMzUyMDgzNDE1MTUxNmQwNDdlOGRlYWU4YTQ2NiIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiOTEyYzE5ODI4NjExZDJhZjdmZWFmNjQyN2ZmNzE0MjFiN2JhMTU4MzM5OWY0YWU5ODBiMzJkNThlNDUzYTJiYSJ9"
         }
       ]
@@ -203,26 +199,30 @@
       "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#L1289-L1291",
+          "resource": "repo://src/app.rs#L1530-L1532",
           "version": "repo-lines-v1:sha256:fa265165a7dbaf620c22011a0c3c22d3351ef4c563f2f54f8fcb163c57bd1734:eyJzZWxlY3RlZExpbmVDb3VudCI6MywiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiOTBhZGVkYjQzODU5N2ZkNWM0ZTVlZTAxMmE5Y2RiYzY1Yjk4Y2VkYTcxMDc2YWU5YjE4MmFjMDNmMmQ5YWRlNyIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDEyY2EzNDVjY2Y3NWJmOWMwODA2YmNlNjk1YmU4ZGU4MDhiNzk5ODQyNTFhN2E1NGQyMDJjZjYxMDFkZDQ1MSIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiOWI5ODAxZjkzZGZiYTU3OWNmZTAzOWRiMGFlYTMwNzc1YWRhYTU2ZDM2NGUxZjE3NWJmNjYxYzhhZjg4MDRkYSIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiMmFiZDA1MjI3NzQyOTJhOTE3MDdjZDg0ZjRmMWExYTExNmQ3MWQzNDA3Yjg1Y2NkNDY4OWU1ZDM1MDBiYjAyNiJ9"
         },
         {
-          "resource": "repo://src/app.rs#L937-L973",
+          "resource": "repo://src/app.rs#L1175-L1211",
           "version": "repo-lines-v1:sha256:ebbdd1522a95030cb283c058e7c767edf7b3297cdfe336683fa94151dd1114b4:eyJzZWxlY3RlZExpbmVDb3VudCI6MzcsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjExNTZkYzNmM2MyODVjOTI5NmU1NWNlY2FiOGQzMzczNzg1YTgyOTVkYTZkZjU4Nzk1ZDUyNTZlNDc5MjRiMzMiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjU3NmVkYmFmNjIyMDAzYjdkMGYzOWE0YjE1ZGNkZjJmYTQ0NGNhY2Q5ZWRlMWRmNjc2OGI0ZWZiMzQ3OTVjNGIiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjdkZWE3NDZmZTI4YTEwOGI2ZDUyNmVkOWJlNzE1NjU3MmI2YjIwYTdmODgxYTI0NGU0OTViN2YzMzg4MWFiMDUifQ"
         },
         {
-          "resource": "repo://src/app.rs#L1048-L1058",
+          "resource": "repo://src/app.rs#L1289-L1299",
           "version": "repo-lines-v1:sha256:3f82b1885fd37c0a7f8cddf8a1e57280241400b28724250c318d147c0b73a56a:eyJzZWxlY3RlZExpbmVDb3VudCI6MTEsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImM4NDJlYTc1OTQ1NzNkNTFkY2MyY2YzMTUyNDRkODE2Nzc4NWQwYjA1ODUzZDc3NzAzMWQ4ODU5NmVmNzRlZTIiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImY5OTI3ZWJiYWIzOWI4Zjc4ZGM3NzcyYTY3Y2YxNzhjZmY4Y2I5OTBiYWJlZjVjMTc1ZDQwNDRlZTllOWZmMWQiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImQzMzk2MDA5NzYxYzQ4YWY3MDRkZDRmZDQzNWM1NTNhOTg2ZGQ5ZWIzMmZiNTYyZmY0MGFkODI0NGIwMWU4ZDQifQ"
         }
       ]
     },
     {
       "id": "claim_3b803c70a66943d38cb5b1741e5a1e08",
-      "statement": "Deleting a chat requires two Delete presses on the same chat in chat mode; confirmation removes the chat from the local history only, drops its pending reply, kept images and answered snapshot, and closes it if open; any other key cancels the pending deletion.",
+      "statement": "Deleting a chat requires two Delete presses on the same chat in chat mode; confirmation removes the chat from the local history only (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#L979-L1031",
-          "version": "repo-lines-v1:sha256:170c2b5d404120c0eeed964795203ed9134af4677755a22c29cc61b36babe747:eyJzZWxlY3RlZExpbmVDb3VudCI6NTMsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjU1NWJlYTQ4MGM0OWY0N2ZiZDg2MGY3ZThiMDIwMzNmYmZlNGU4ZDJjYmZmMWU4Y2Q3Mjk3NjY2YTc3NDI2ZjEiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImI0MzQxZWJmNGViOTE5ZGQwMmUwYWE3YTVmNjM0MjdhNDI1OGQ4YzZkOTAwNDc4MWY3Zjc5YmI2ZTViNWUwZjkiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjgyNTEwNTY0NjA2ZWRmNDUyODU1YTA4OTFlYmRlN2ZhOGZjYjIyMTBlODQyZjU4NDNjNzlhMmRmZDcyZjU5OTAifQ"
+          "resource": "repo://src/app.rs#L1225-L1272",
+          "version": "repo-lines-v1:sha256:ad391df25f8e04ff219ae608b732e73d9e13df87da14ad068cae689778491925:eyJzZWxlY3RlZExpbmVDb3VudCI6NDgsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjZhMTdmZjJmNjhhNGFkMzg5M2JmNTgwZjljZmVjNmUyNTI4ZjJjODhjZjJhMjNlNTg2MjcxYjgyMTkyOTBjMzciLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjVmMjAxZDg2ZGM3NzY0YWFmOTA5MGNhNDRkODcxNDM3Y2JiODk4ZGE5ZWJlMGU0ZDhlNjVlNDZiZDJkZGVkYjQiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjgyNTEwNTY0NjA2ZWRmNDUyODU1YTA4OTFlYmRlN2ZhOGZjYjIyMTBlODQyZjU4NDNjNzlhMmRmZDcyZjU5OTAifQ"
+        },
+        {
+          "resource": "repo://src/history.rs#L383-L408",
+          "version": "repo-lines-v1:sha256:c4f4b4352468ac2099f49717f06a98ea40bab4d5c62a4158e04234c744610ee6:eyJzZWxlY3RlZExpbmVDb3VudCI6MjYsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImNkMTFhNTg0ZDdhNWU5N2U1ZmY4NmU3MzY5ZDBkMzdhNGYxZjlmZjBlYzRlMzk5OWEyZWRkYTc2MDMyNTQwMGYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjNlMjRjZDNlMGNmY2Y4OGRmMGJmNGU1YjRjYzY0MzdiODBhMjAyYjEwNzk5Y2JjNTMyNzBjY2ZlMGRjNDk5MjYiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImYyZWNiYWUyM2ZlZTE0ZTViOWIzNWQ0NDcwNmFmOGQ1OWY1NWQyOWIwNzYzNjU4ZjI4YjIzNjZkNWUzNWM5Y2UifQ"
         }
       ]
     },
@@ -231,7 +231,7 @@
       "statement": "toggle_global_autoreply refuses to enable without an AI client, persists the global flag in the history and updates the status; disabling is always allowed.",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L1186-L1203",
+          "resource": "repo://src/app.rs#L1427-L1444",
           "version": "repo-lines-v1:sha256:05cde78eab7927a8b5c33ce971f85c0e210ce60936eada88f8c9481585c1519e:eyJzZWxlY3RlZExpbmVDb3VudCI6MTgsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjlhMjY2MjVlMDNiMmE0MDg1NDg0YjQ2NjUzOTllOWRmOWFhZDQzYmVkNGQxNWFkODEwODMzMGIyMDhjNmM5ZTYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6Ijk5OWJkZWZlZWFjYTYzNDBlMDI5MGRjODZlODg5Yzg5ZDVlMTg5YWE1YmE0NGI1ODViM2I3MDczNGY0YTYyZDgiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImU2ZWQ1MmZlYjhjZTliMjNiYmNiYTkwMGFkYjk0MzE3MjA4ODNhZTk0ODY4NmQyNjhlNmRkMjFlNjljYTU3YjkifQ"
         }
       ]
@@ -241,7 +241,7 @@
       "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'.",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L1139-L1169",
+          "resource": "repo://src/app.rs#L1380-L1410",
           "version": "repo-lines-v1:sha256:64fdd9a3310b49def09cbd68a0103e07c41050b652d90eece3d0e2323f063a71:eyJzZWxlY3RlZExpbmVDb3VudCI6MzEsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjA3YTEzOTMxODdkZWNmZmM0NzMzYzYwY2FiYjA2MTkwYjdkYjAxMjJjYWEyYWM3YjRjNGUwNzBiNzIxNTk1NWQiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjM2MGUyOWExOGZkNDE5NDVjMjM3NTgxMzM4ZGI3NWFjOTIzYzYxYjVmYmFiOTNlOTM3ODdlNWJmMGMyMjk4OTciLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjVhOGY4ZTE1N2Y4ODViMmUzM2NmZjkyMzAzNTIyY2VlYzYzZTlhZDcwZmI5NDgyMjg2MGI4NmJiZmJlNjc2ZmEifQ"
         }
       ]
@@ -251,7 +251,7 @@
       "statement": "web_chats exposes the TUI chat list in the same order, restricted to chats that support autoreply.",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L1571-L1585",
+          "resource": "repo://src/app.rs#L1916-L1930",
           "version": "repo-lines-v1:sha256:fef040ce667500067e70dc91e838a73c35752821bb041d4463b4361b4445b2c0:eyJzZWxlY3RlZExpbmVDb3VudCI6MTUsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImYxMTFhMDk4Y2E0M2I2Zjg1OTYwYTUxMjFiOGVmY2Y5MDVlZDZmNjdmZjE5MjY0NjcxZDhkYTFiYzhjZTEwNWQiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjM1ZjZmNDY2ZTU5Y2ZjMzA3MDNjNDVmNzJhZjY1ZGU1NGFjMzYzZDM1NGNjMWEzMzMwMzUwYWNkNDUyNDM0YzQiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6Ijg5NGUwY2FkODFjNTBiOWY4MWE5ODI2ZjA1YzUyZjY0MGE0ZGM3NzdiYWQyNGM2MGEwMjQ5NzhlZWJmMWRjMGMifQ"
         }
       ]
@@ -261,26 +261,22 @@
       "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#L1462-L1465",
+          "resource": "repo://src/app.rs#L1705-L1708",
           "version": "repo-lines-v1:sha256:22bd4378ebfd0c259aac4089a03d61912cbb17abca836659a5d9c8fc9cde7b15:eyJzZWxlY3RlZExpbmVDb3VudCI6NCwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiYjA0MjdkNzcxY2Q5ZTlmY2YxZTMyODc5YWNlMDkzZWY5ODk3NjRiZjE4ZjE3OWY2YzFkZGFjM2M3Nzc1MDMyMiIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDEyY2EzNDVjY2Y3NWJmOWMwODA2YmNlNjk1YmU4ZGU4MDhiNzk5ODQyNTFhN2E1NGQyMDJjZjYxMDFkZDQ1MSIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiYTQzMjhiMGQzNWY1YmVlOTEzODE3NWRlZTE2ZDRmNmZhM2FmMzc1NDM0ZmIwOTMwYWU4Nzk5MDg2OTk2MTgxYyIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiOTViMzliZDc5NmMyYjVjYThiYjhjMWEzNjUzODk5NzY5MGE0OTE5ZDY5NzVlNGMxMGZiMWE1ZGE3MGU1Mjc3OSJ9"
         },
         {
-          "resource": "repo://src/app.rs#L591-L614",
+          "resource": "repo://src/app.rs#L747-L770",
           "version": "repo-lines-v1:sha256:5736af4a9574d77087b2de7f56f7b77e2d96d8861d19b9c42159d91fa560f0f1:eyJzZWxlY3RlZExpbmVDb3VudCI6MjQsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImJmNTc4ODJlNGRmYjJmODViYWQyOGRhYWU0Njc1ZjIwODYxYzc2N2E1ZjIxNzg3OTIyYzYyZDgxOGQ5YjJiYjUiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjdjZWI1ZWFiOTZmZDM0MzEzNTI3ZjczMDk2ZjYwMWYzZDZiNWUxYmY1MzE3YTRmMzg0YWM0MTY4OWE0YWI1NTIiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjBhN2YyNDA4YTExOTJiY2VmZTI0ODJhNTg2MGM1MTVhMmQ4NGIxMzQzZTE1ZDdmMGVmNGRhMTA0YzY1ZTJiM2EifQ"
         }
       ]
     },
     {
       "id": "claim_831a5e88df6448b9a0545ab015ba1580",
-      "statement": "finish_reply clears the pending state and, if a newer message that passes should_schedule arrived after the answered one, schedules a new reply; otherwise it drops the kept images of the chat.",
+      "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#L1453-L1455",
-          "version": "repo-lines-v1:sha256:f837e4630ef0ee6fe89e07bf810f5851838ae48e227e0659b648d4ea480392fa:eyJzZWxlY3RlZExpbmVDb3VudCI6MywiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiNTk5ZjgxMThmMmE5ODIzYzNmZDQwNDcyYjlhMmUwNDE1ZTRhZGQzMWQ5NTkwNjE0NzY3MzlhYjk4YWIyNTUxMCIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDEyY2EzNDVjY2Y3NWJmOWMwODA2YmNlNjk1YmU4ZGU4MDhiNzk5ODQyNTFhN2E1NGQyMDJjZjYxMDFkZDQ1MSIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiNGQ4NDkzOWY4ODU5NjI0YmY1NGNiOTdjYWJjMDE2ZjRhYmY3OTU0YWUwNGU0MTVmODUzMjQ3ZTRmYWY2Zjc3ZCIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiNDlhNTk5ZGY5ZDUwOWVhMzBiNDMzZjkzYjY4ZjE4ZGQ1ODY2ZDVmMGJjMGMzMGEyMTU1OWFkN2U4MWM1ZmMwMCJ9"
-        },
-        {
-          "resource": "repo://src/app.rs#L622-L636",
-          "version": "repo-lines-v1:sha256:084f8b11c8537716d5b5aa7dd03d18d8c925c2c8808ed3bd55605fac82c22a47:eyJzZWxlY3RlZExpbmVDb3VudCI6MTUsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjNmN2EzMzI2Mzk4MGZiOTQ4OGI5NDcyZTI4ZGU0NjE4ZWVjMzc3MTE2ZmFmZDViODZlNmUzYTZhZGVjYjM2YTEiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjBiZTFkYWFmY2ViYWI1NjdjMWQwYjE3NmVjZmY3N2I5NGNmOTllYmRmYzI0MGZiMjFhYTRjNDIxYzBkYTRiNzMiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImNmMGY4ZWU3NGMyYmNhMjZlNTlmY2NmYjUzZmQyYWI2OGJjNzIwZTY2MGZiZjM3M2IwZjFhNTY4ZmIyNjBjOTUifQ"
+          "resource": "repo://src/app.rs#L831-L852",
+          "version": "repo-lines-v1:sha256:50173ca19bcacd64331b57d37896a266693fbcf0d59be90d1ec263c47339b0ad:eyJzZWxlY3RlZExpbmVDb3VudCI6MjIsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ1NDc2YjZjNjU4MzI4NGViNzUyMTA1Mzg3NGFjZmU3NDY1MGQ0NTJhOTRhMjYxYzg4YTFhYzBjYTYzNWYwY2MiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6Ijk5OWJkZWZlZWFjYTYzNDBlMDI5MGRjODZlODg5Yzg5ZDVlMTg5YWE1YmE0NGI1ODViM2I3MDczNGY0YTYyZDgiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImNmMGY4ZWU3NGMyYmNhMjZlNTlmY2NmYjUzZmQyYWI2OGJjNzIwZTY2MGZiZjM3M2IwZjFhNTY4ZmIyNjBjOTUifQ"
         }
       ]
     },
@@ -289,7 +285,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#L1359-L1370",
+          "resource": "repo://src/app.rs#L1600-L1611",
           "version": "repo-lines-v1:sha256:5c8be8846fd13dfd0c92193083af62f733a451585be051143592b7b033237591:eyJzZWxlY3RlZExpbmVDb3VudCI6MTIsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjY2OWRlZjlmNmNmMDJlMjJjNGFhMmU4NTllMWY1MWZjYjdhOWUyYzc1NzRmZmFhY2IxNDNmOWIzZmUxMWY1ZmMiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6Ijc0ZmM5YzZmMDQxZWE4YTkyNjlhMGIzYmYzMTJjNjYwYjk4OTEyNWI5MDhmN2I1NjE2Y2IwZmZmMjM0NTI2YjUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjE5NTA2Nzc1NGYwNDQ1NDFmZmVlZDA3N2Y3OGMyZjg1ZTY3NjNiM2YzOTZkYjBlYTM5OTJmZDgxMmZjYWRmNDAifQ"
         }
       ]
@@ -299,7 +295,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#L812-L818",
+          "resource": "repo://src/app.rs#L1050-L1056",
           "version": "repo-lines-v1:sha256:f27e2fa9423ba6f3c9fb6381dd2ec60346c3c15ed204b861c320fac53ba2a473:eyJzZWxlY3RlZExpbmVDb3VudCI6NywiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiMWY1MGM3ZGZhNzg3MTk0NDViMjRiNDI2NjAyNjQxZDk0MmI1NjFmN2U1MmQ3Y2M2MGNkN2M5YzNiYjI2NWQzNCIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDg5M2Y0MGUzNzgxZWI4OTZjODJkMDQ2YTA4MDY3YTExZTM1Y2RmMmYxZGZhZjI2ZmVhOTBiOGRiNjY3NjQxMyIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiNzk0ZmJmY2NiNjQ2MWJhYjM5ZWE3YTVmZTJiZDJjMzU3YTQyY2I1NTUxYjE3NzJjNTc2YThjOTVjODkyMTY2ZiIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiZGVkOTlkNjVkMzhkZjFiODYxZTExYTc1MzdlZDk4YWI1NjA5NzEyYTVhZjgzNjdlNWY1Y2FkNjM4MGYyNWZiMSJ9"
         }
       ]
@@ -309,11 +305,11 @@
       "statement": "In list focus PageUp/PageDown move the selection by LIST_PAGE_ROWS (10) rows and Home/End jump to the first/last row, clamped to the filtered list.",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L33-L34",
+          "resource": "repo://src/app.rs#L34-L35",
           "version": "repo-lines-v1:sha256:7825efb1211acd5d025f243b3bb470aeb24c370f07856a47d71779b8de576ab3:eyJzZWxlY3RlZExpbmVDb3VudCI6MiwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiMTVlZWU2ODI2YzIzMDYxMTIxNjY4N2UzNjYyM2E1YjA0NThhMGRjNGFjMGIxOTk3ZDZjYmVlNDNhZGJkMDgyYiIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNGEwYTBlYmUxMGIzNDk4NjVhMzlmY2Y3ZjJlMzY5MDM2MTA2NjdiNWQwYmU5MGU2NWZhYzUwNGJiNmZkNTM5MyIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiZmZiMmY1ZDFiZmZhMTlmNWYxODFhNjBkYTg2NWU5YmY0ODk4MDBkYjUzNDgwMjZjNjE4MGJmNjBkZDk4MjhmZiIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiZjM1MGFjYmZhYzdlNDEzZmFkNWYxMzk0NTQwYWE3MWM3YWY3ZWZmMDA4MDliYmMxYTlhNzQxZGRhNDFiOGYwZCJ9"
         },
         {
-          "resource": "repo://src/app.rs#L937-L964",
+          "resource": "repo://src/app.rs#L1175-L1202",
           "version": "repo-lines-v1:sha256:acf06960921a82cc9b2dfbc4cd8be73f5ce9d8945326f358e29f727a80dd79f0:eyJzZWxlY3RlZExpbmVDb3VudCI6MjgsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjExNTZkYzNmM2MyODVjOTI5NmU1NWNlY2FiOGQzMzczNzg1YTgyOTVkYTZkZjU4Nzk1ZDUyNTZlNDc5MjRiMzMiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjU3NmVkYmFmNjIyMDAzYjdkMGYzOWE0YjE1ZGNkZjJmYTQ0NGNhY2Q5ZWRlMWRmNjc2OGI0ZWZiMzQ3OTVjNGIiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjFlYjhjMzVjMzllMzMxMTJhMGUzZTVlMDg1NjlmOWVkYWJkNDM5ZmNkM2FkZmVkNjQ5MzAwNjcyZWRiYjYwNjEifQ"
         }
       ]
@@ -323,11 +319,11 @@
       "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#L1101-L1116",
+          "resource": "repo://src/app.rs#L1342-L1357",
           "version": "repo-lines-v1:sha256:f3a02114e65f3b57e1e9609cdb5227848e7d4a8d0d2d3abc6b38c40f5286461f:eyJzZWxlY3RlZExpbmVDb3VudCI6MTYsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjBjMWZhZjI0NjQzNTZlZjY0NzAyNjI4YzM0MGYyOGNhZDljMjY3OGYxMTViNTAxNDZkMGMyNTBmNzhmZDk2NWMiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjkzNzNhMTgxNzI2YzQ3ZjdjNzlmOTI3ZDI2NzhhYzQ3NjYzY2VhMzI1OGYxOGEwNTM0ZTg2NDVlYmFlOTlkMGUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6Ijc1YjdhNGU5MWJmNTc3ZDQwNjIyM2IzMjg2ZTFhYmI0NGRkNzUwYmViNmU1MzIwYTMyMGZlZDJkOTBjOWQ3ZDQifQ"
         },
         {
-          "resource": "repo://src/app.rs#L1221-L1232",
+          "resource": "repo://src/app.rs#L1462-L1473",
           "version": "repo-lines-v1:sha256:b46e0c6074aa70a6a18b87a08237023dd69de8b989ccfb3ce3c5796148164d3b:eyJzZWxlY3RlZExpbmVDb3VudCI6MTIsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjRjZGFkMjlkZWMwNzg5N2ZlNTVmMTZlNGRiNTEzOWEwMzEyZmU3M2M5Mzc5OTk0NTU5ZDg2ZmQyNjVlZWU2NjciLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjE2MmVlNTdlNDFhOGMyMGFlMDE2NGRmMjYxNDM5ZGNjNDk5ZDM1MjA4MzQxNTE1MTZkMDQ3ZThkZWFlOGE0NjYiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImNjYzVlMzg4ZThmOTY0ZGY1OTFjZDM1Y2FlZjk5ODM0MmRjNTllNGU5OTMwMmMyYmI0MjMyZmMxMzdhM2Q1ZjkifQ"
         }
       ]
@@ -337,7 +333,7 @@
       "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.",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L326-L381",
+          "resource": "repo://src/app.rs#L360-L415",
           "version": "repo-lines-v1:sha256:1874e8b5ab58c3a5e271332f88ac81403b0acf03851aaef445bbd477d21f76c4:eyJzZWxlY3RlZExpbmVDb3VudCI6NTYsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjIxZGUyN2ZmNmY2ZmRjOWQxMWZhMDVjOGM5ZTVjNmI1NTc2NjU3NDhhMWRmM2I3YzFkOTVhZTQwNDY3MmJkNDEiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImM3ZWE1ZDExNGEyZjkyZThlZjQwMzQxN2JhMDkxZWRjODhiNTYwODM5OWY2YTFiNmM0ZWVmNTAzZmI1ZTdjZjIiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjBlZjIyYTA2NDVkNDAwY2EyNWVmYzI2Mjg2ZTdjMDZmOWUzZmZhZmQxY2UwMzVjMmZjOTkyYWZhNGI2NGIxY2EifQ"
         }
       ]
@@ -347,7 +343,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#L343-L354",
+          "resource": "repo://src/app.rs#L377-L388",
           "version": "repo-lines-v1:sha256:741e9b7ade558dddd72ab34aa017552f645c8cc476274a1266dc2b4f27a5860d:eyJzZWxlY3RlZExpbmVDb3VudCI6MTIsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjYzZjY3YjMxMGI5MjEzMzcwMjdhMWY2YzdiYjA0YjkzNDM5MWZjOWM1ZDAzYTJlNjY1NWI2ZmVjNDE4ZjljMjkiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImEyY2MwZDQ0ZGRlMzkxMDE4YTQ1OWMwNmIwOTBlZWVjOGRmNTA0MTg4Mjk3MzIwMmQwYTc2NWZjOTM1NjRjYTYiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImU1ZTg0YjQyYzc5NDMxZTNkOWI4Mjk4MGQwN2Y5NWQxMTBiYjQ1ZGUxZTI0NjlkYjcxMDJlNzJmODRiM2FjOWIifQ"
         }
       ]
@@ -357,7 +353,7 @@
       "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#L383-L400",
+          "resource": "repo://src/app.rs#L417-L434",
           "version": "repo-lines-v1:sha256:11076b2b498b6d19b7a0629886f2cdb37229105049de848490f4e1d9faead710:eyJzZWxlY3RlZExpbmVDb3VudCI6MTgsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjVhYTEzNjY2OGNiMzc5NzgyYTUwMTY4ZDNlOWM4Njg4MmM5YTBhZTAxOGQwNTZiNTEzZWU2ODA5OWYxNjc3YzYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjVmMjAxZDg2ZGM3NzY0YWFmOTA5MGNhNDRkODcxNDM3Y2JiODk4ZGE5ZWJlMGU0ZDhlNjVlNDZiZDJkZGVkYjQiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjAyNjE2ZDk0ZDJmZDg4NWViMDZhMGFmZjVkYjQ1YWUyOTdiMWQ5NzViYzU0MjRlZGIyYWUwZmE2NzZkMzE0ZTYifQ"
         }
       ]
@@ -367,18 +363,104 @@
       "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.",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L1171-L1184",
+          "resource": "repo://src/app.rs#L1412-L1425",
           "version": "repo-lines-v1:sha256:643e53e7eee86239879c37d679254899bc317cdbf3cd55b8314e04ba1657043c:eyJzZWxlY3RlZExpbmVDb3VudCI6MTQsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImIzN2M0MzFhZmQ5NGMyNzU0NzJkNjliODNlMGM0MmRmMjU0NmQ2MDQ3NmM4ODg3Yzk3MDRmZWM2Y2U0MDkyMDMiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImRkZDFkZTJkMmJiNWE5ODAxZDE4MTAyYTQwYzlmOTc4NjRkNjNkYmEzMzkzMzdiZDVlYTU3Y2FiOGU4YTYwYzgiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImJlODM0YjUzNzA2ODgzYmQzMmMxMmViNmQ0OGEyMmFjZjlkNTIyYzIzOWJlNTE5NDZmNzA5YjIxZDNkNjIwMzIifQ"
         },
         {
-          "resource": "repo://src/app.rs#L1563-L1569",
+          "resource": "repo://src/app.rs#L1908-L1914",
           "version": "repo-lines-v1:sha256:aeccbfa28dab16bc5a14f9e771c54b3c780209ad3837bba0672ad94aaf49bd5a:eyJzZWxlY3RlZExpbmVDb3VudCI6NywiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiZjYzMWVjNWFhMWFjMTI1MDk0ODNlODM1MTA5ZDBkNjMwMDY0YTBhYmU5MjU5NGUzOWUxZjUwOGIxZDUxNzVjNCIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDEyY2EzNDVjY2Y3NWJmOWMwODA2YmNlNjk1YmU4ZGU4MDhiNzk5ODQyNTFhN2E1NGQyMDJjZjYxMDFkZDQ1MSIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiYWNjYzYzZTY1NWZjNmMxODhkMDhhMzMyMTUyYzY1MTZiNzVlZDdkNTIzYmJmMGFhMTg2MzBkMTRkMjA0NDczOSIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiNDQwM2Q5ZmY0OGI3ZmI2ZDcyM2RlNWM5YjMxODkzMTNlMjQ2NzBhMWE5NjJhYWUxZTYxODRhMjllZmM3MWM1ZCJ9"
         }
       ]
+    },
+    {
+      "id": "claim_d629089dcfae40a9aaa6313846538b73",
+      "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",
+          "version": "repo-lines-v1:sha256:624a4ce8841071737337c985666fbac63835cc6490d2f2eb42a0f686ebfc5729:eyJzZWxlY3RlZExpbmVDb3VudCI6OCwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiOGUyMDU5YTZhNWEzYzU1YTNkYzg3NzhlMzUyMWI4NjM3NDExOWJiOGRlZDViNmY5NTg0OTNjY2VmOWJiY2U4NiIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiMzI0NjljOGEwODk5OGMwYmE3ZDUxN2FiYjcyYTQyMTI1YjRlZjc0YzFkNWMwNmNmZDU0ZDdmNWRjMWEwMDQ1NiIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiZjM3Y2IzMGQ4MjVjMTlhYTU3ZWFmY2E5MTg4ZmRjNTU1MTRmY2VhZjE1NmMzNTY2NTIwMDIxMjc0MzkyNjcxMiIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiYTg4MjZkZWMyMTEyZDgzMjYwMjQzMzU4MzRiMTI3YTBmYWRkYzIxMGYwMzUwMWEyZDI5NzVhMzE5YTdjY2EwYiJ9"
+        },
+        {
+          "resource": "repo://src/app.rs#L583-L626",
+          "version": "repo-lines-v1:sha256:1631b6874f5880f4290a306f619b45026e63622c4f480dc289aeddb04599171b:eyJzZWxlY3RlZExpbmVDb3VudCI6NDQsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImY4ZTgwYjQzNzA2MWU4ZDc4M2IzZDBhNmYxZThiMmI4YmNhZTYyYTIxNmQxZTA3ZDZlYzQ2ODZlNjMxZjE5ZTgiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjE2MmVlNTdlNDFhOGMyMGFlMDE2NGRmMjYxNDM5ZGNjNDk5ZDM1MjA4MzQxNTE1MTZkMDQ3ZThkZWFlOGE0NjYiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6Ijk0YjU3MDVlNmJlMTRmMGZkMjBhZTgxYTYzZDNlZWQzZjJhMTFmMWJhNGNlMWUyYzEzMjBhODVlOTdlYzJmYTUifQ"
+        }
+      ]
+    },
+    {
+      "id": "claim_67955abe9c374735abef0c3afe13110d",
+      "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",
+          "version": "repo-lines-v1:sha256:cbfda29e4579b9e3f15b7065e6b8f90aa77722d3009289a42486f3f85d5f5e74:eyJzZWxlY3RlZExpbmVDb3VudCI6MzAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjA1ZGMzYmMyMzYxYWY2MWZiOGM1N2YyZGZiYTExMWVkN2QyNTZhNzBhMTBmYWRhNDFkMzg2ODgwZDQ3YTc5OGYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImRmYWI3MTBkZGY3ODU5ZTU3NTc2NGE5MTNhMDAyYzc0NTg1ZmZhNzAwMTUxMGNiODgxMWRhODNkMmNmNzRiMzQiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjU0MDAyNGFmZmIxODk1NGEyZjYxODAzYjk5ODBiOGY1NmVkNTlmZjJjN2FiODAyZDU5NDI2NGZiMDFkNmMyOGUifQ"
+        },
+        {
+          "resource": "repo://src/app.rs#L717-L724",
+          "version": "repo-lines-v1:sha256:fa39de81b9c45f8ac3d33bc79d12c507e895c348f454fe7b20bb5e77d0bd40de:eyJzZWxlY3RlZExpbmVDb3VudCI6OCwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiOGVlMDgxMTQxMWY5NDM0NzM0MzhlNDI3YTdiN2ZkZWVmYjMzN2ViNDAzYzE5NDU2ZTMyOTZlNzgzMTFlNmNiZCIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDg5M2Y0MGUzNzgxZWI4OTZjODJkMDQ2YTA4MDY3YTExZTM1Y2RmMmYxZGZhZjI2ZmVhOTBiOGRiNjY3NjQxMyIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiZjJkODI4YTAzN2FjMWM4MTUzNWIxZDU1MDA4MzIzMjNiZmJiODY2YTNhM2Y4N2U3YTY4YzEzYTJiYWU1NmZmZiIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiNTc4NDUxMGM1NmY1YTlhM2IyZjRlN2Q2ZmMwMWM0Yzg1MTI5YmNjNTMzZTVhNmNlMDYxNjJjYWIyYjA0ZjhjMiJ9"
+        },
+        {
+          "resource": "repo://src/app.rs#L74-L77",
+          "version": "repo-lines-v1:sha256:c9d722762ee24413b4f692e17e9d10b9430f82db1b1c0cfecc1c9b9e33e0876a:eyJzZWxlY3RlZExpbmVDb3VudCI6NCwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiYjRkMDBmOGEwMmYwM2JkMGI0MDQ1MmI5NDM4ZDgyZTgyNDZkOTZiMTc5ZjllOGM4ZDY1ZDJiODg5YmQzM2U4YiIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiYWUwMTNhNDJhODRhNGUxM2UyNzBhOTM2ODdkMzEzMmE5N2UxZThmY2FkYWU3MDkxYjQxZTFiYTZjZDhjNjBhZiIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiN2UxZjg4N2FlOTZlZTg0ODQ3ZjZiNTExZWY4MDU5ODY2ZGExZjY3NDJhYzk4N2Y1YzE4NWRhYTcxODFhNmE4YiIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiNGI3M2NiMGJjNjJiMWRjODdhZjkyNjE5YWQwMGFkOTg5NGIzYTkwZmQ0MjY5YzkzZmQzODM3MzE5ZDgzNWEyZiJ9"
+        },
+        {
+          "resource": "repo://src/app.rs#L900-L918",
+          "version": "repo-lines-v1:sha256:aca8ef54d074bdd9a35c6228ea67532c04a9bf55079ae6ed368a56dfc7f87db1:eyJzZWxlY3RlZExpbmVDb3VudCI6MTksImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjI3MGQwY2JkODBmOGZhNDZkN2EzOGQxNDMyMzZjODMzYmViZGE4Mzg2ZGNmZTFkZTBjYmNjNWM2M2IzNGE1MzQiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjRiNDI5OGJkNmI1YjQyZmVjNDI0NDIxMjBlNTkzZjAxZDFiYTU0YmFhODg2NTUwMzE0MWE0YmQxYmViNjQxZjgiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjIyMGUyNjkzMzFhZTU2NTJiYTMxNWViYTdmYzRlNWMxNWFhNDlhMzdiMjE2NGY3NjRlYmNiYTA1ODE5ZGMyNDAifQ"
+        }
+      ]
+    },
+    {
+      "id": "claim_ece41a4d5617456597c8ff54f46ce321",
+      "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",
+          "version": "repo-lines-v1:sha256:450b2c863867076ae29a98f6b2df94b3dc43936844327c15ec52c2a5f4b2ec09:eyJzZWxlY3RlZExpbmVDb3VudCI6OTEsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjUwMzZlNzExZDhiYjhjNjMwNzQ2NTEwNGIwZDYwZTI0Y2M0N2IwZDY4ZDRlOTMyNWM3MGM2N2JkNDRkZTU3ZDQiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjE5ZTc2MzU1Y2YxMTk5NDgwYzNmOTk4MzZiMTQ4NmM4NDFkODkxZmFhZDkwNGM2MDhlMDkzMDIyZDM3NTdhZWUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjViYzQ0ZWIzMGI2OWE4MGNjYTQzMGFhMjIxNTg0MGU4ZTNkNTlhOWU4OTk2NDk5ZTlhZmM3NmQ3ZjY1OWUxMmEifQ"
+        },
+        {
+          "resource": "repo://src/app.rs#L681-L715",
+          "version": "repo-lines-v1:sha256:13c9ebb313a3bcdb8ca212f0ecedf91359ab8b7346f0a159add0c503cdc53a51:eyJzZWxlY3RlZExpbmVDb3VudCI6MzUsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImM1YjQ5M2Y3OGMzOGVhMGFhNGY2YzlmODE4ZWI3ZTc3ZTIxMDJiNzNjZDhhMWU3ZDc1MzAzOWExZWQ1ZTI4YjAiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjA4ODU4MjU5NDc5YmI1ZmYzY2Y0YjE2MDhjNTA0MzYzNzM5NThhMWQ3NWY2NDZlMDZiNGUyOWQ5MzVkNGMzN2MiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjBjOTYzNmU4NWU4NzBhN2VmZjYyMWM2ODNhN2RkODZhYzA3YTdiNmNhYTNhOTNlYzgwN2FjOWQzYTg4MzFmOGUifQ"
+        }
+      ]
+    },
+    {
+      "id": "claim_50f7571100854f1fbca745a983ddd658",
+      "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",
+          "version": "repo-lines-v1:sha256:b58e50269905a224c205da0c87a432f759e7992b40a8e0e8c3dae0872b66dbcc:eyJzZWxlY3RlZExpbmVDb3VudCI6OSwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiMDlhZGUzMDdjOThiYmRmZTE1NTJjZDJhY2E4ZTY4NTA5YTE5NGZhOTQ5NjkyNTdhMGZlYjFmYjM2NTFlM2E3MiIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDEyY2EzNDVjY2Y3NWJmOWMwODA2YmNlNjk1YmU4ZGU4MDhiNzk5ODQyNTFhN2E1NGQyMDJjZjYxMDFkZDQ1MSIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiMjcxOTg2YjExOGI3ZmNmMzEzNTE1MmVlODA0NDMwMmE5Zjc3YmE5ZTc1NmM3NWIxOGI4N2IzMjZjNjQ4OTI3YiIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiOTViMzliZDc5NmMyYjVjYThiYjhjMWEzNjUzODk5NzY5MGE0OTE5ZDY5NzVlNGMxMGZiMWE1ZGE3MGU1Mjc3OSJ9"
+        },
+        {
+          "resource": "repo://src/app.rs#L38-L41",
+          "version": "repo-lines-v1:sha256:fc00beef688a9051866e1972ad3485e7820d2935c053dbfd34d3c74b2411da65:eyJzZWxlY3RlZExpbmVDb3VudCI6NCwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiZGZmYTFjMjIzYTFjYjVlZDdkOWZmYzljYjllNjIzYzlhZjY1MjQ3MmJlZjUzYzMyZTBhZTcxYjc1ZjM3YjRjMSIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiZjQ5M2E2NjFmZWY4MzY3ODU3YWJkODk4ZWY5YjAxZWYyMmY2ZDg0MjlhOWEyNGI3MjRjY2Y1ZTZjNDEzNTc2MyIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiZTIzNTZlNmVmOTk1ZDNmNTcyOGVkZjBiNDg5MTk1ODg0NjliMGQ3YjI3Njc5M2Y2YjkyNTg4YjhkZWMyNzliNSIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiMjgxYzllMDA4NDg0YjQyMjE2ZmEyYTU5NjFkZjIwMzdhZjIyY2ZmODMwNWEyYWM5YzQzOTU1MWVjN2U5NDgwOCJ9"
+        },
+        {
+          "resource": "repo://src/app.rs#L70-L73",
+          "version": "repo-lines-v1:sha256:e8ad9bf94a9667abf25ac1ea023b45a46e983fa19f2693ad61572aa62b8dce37:eyJzZWxlY3RlZExpbmVDb3VudCI6NCwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiN2Q3YWEwNTI1MjIzNTNlZGExNGNjMGM5ZTg0ZTE3NWNlMDBlMjNmMWY1M2M0MDZlZGYwYmE4OTA4NTg2MjMyYiIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiOGRlMDQ3NmFjZmJlNjk5MzE1ZDU4YTBlNGMwZDIwZGZhM2FkMTlmMDg3MDYzM2Q4NmQyNzZkZjI1MGY0MTJhNyIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiZjQxYzA4ZTkxMzgyNzAzMTIzZDJkYWQ5Nzk0NWU1OGNlMDQ1NmU5NGVmMGYzZjFlNTFkMDFhYzlmN2M5OTQ1MyIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiZDQzOGUwNWQwNzYwZjY5ZjFjMTMwYTA4ZWVhYTc4ZWI5Y2MzNDFmYzMyMjI5MmYzZGNjYmI0ZTBiZmE2NWUzYiJ9"
+        },
+        {
+          "resource": "repo://src/app.rs#L772-L829",
+          "version": "repo-lines-v1:sha256:d5ad7a2c920ec3c326e04645e1ce43b9fb4ce906a211a835b056903dbcdf35d5:eyJzZWxlY3RlZExpbmVDb3VudCI6NTgsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjMwZjE0YmRhOTU2YjU3M2JhNzk1NTJhNTEwNmIyMzMzNjNlMWFlNGY3ZDMyZmE3YWU4ODk2MDRlMDMyOGI2MGUiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6Ijk5OWJkZWZlZWFjYTYzNDBlMDI5MGRjODZlODg5Yzg5ZDVlMTg5YWE1YmE0NGI1ODViM2I3MDczNGY0YTYyZDgiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjBhN2YyNDA4YTExOTJiY2VmZTI0ODJhNTg2MGM1MTVhMmQ4NGIxMzQzZTE1ZDdmMGVmNGRhMTA0YzY1ZTJiM2EifQ"
+        }
+      ]
+    },
+    {
+      "id": "claim_3f4229faf876457fb22804fed2a7ba4d",
+      "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",
+          "version": "repo-lines-v1:sha256:ec755f9e1aadd0ddfa4bcb7f40ceb61ebadb3024ee9727315ad63fcae7620f83:eyJzZWxlY3RlZExpbmVDb3VudCI6NzMsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImMyMWI5OTAxNjZkNDU5NjI1ZDk3MTdjZThkNTMzOTMzNTFhNzI5YTA5ZDY5ODBmN2UxYTJhOWJiNDkyYjVhNjMiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjE0OWI3NmEwN2ZiN2VjODVjZmJhNmNjNmQ1OTUzOTFlNDU5MjAzY2NmZDMwNTdmNmY3MzczYzNlNzZjYWE1N2UiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjBjOTYzNmU4NWU4NzBhN2VmZjYyMWM2ODNhN2RkODZhYzA3YTdiNmNhYTNhOTNlYzgwN2FjOWQzYTg4MzFmOGUifQ"
+        },
+        {
+          "resource": "repo://src/app.rs#L772-L829",
+          "version": "repo-lines-v1:sha256:d5ad7a2c920ec3c326e04645e1ce43b9fb4ce906a211a835b056903dbcdf35d5:eyJzZWxlY3RlZExpbmVDb3VudCI6NTgsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjMwZjE0YmRhOTU2YjU3M2JhNzk1NTJhNTEwNmIyMzMzNjNlMWFlNGY3ZDMyZmE3YWU4ODk2MDRlMDMyOGI2MGUiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6Ijk5OWJkZWZlZWFjYTYzNDBlMDI5MGRjODZlODg5Yzg5ZDVlMTg5YWE1YmE0NGI1ODViM2I3MDczNGY0YTYyZDgiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjBhN2YyNDA4YTExOTJiY2VmZTI0ODJhNTg2MGM1MTVhMmQ4NGIxMzQzZTE1ZDdmMGVmNGRhMTA0YzY1ZTJiM2EifQ"
+        }
+      ]
     }
   ],
   "verification": {
     "by": "openwiki/0.7.2",
-    "at": "2026-10-23T21:26:24.156Z"
+    "at": "2026-10-24T16:42:08.550Z"
   }
 }

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

@@ -1,6 +1,6 @@
 {
   "schemaVersion": 1,
-  "pageVersion": "sha256:e2f63ba48d127d1aaa5a3dc9ea68ed4b0137c88abb763235e4ed0f305b984766",
+  "pageVersion": "sha256:1c12bcb75120e45d7940b31833bbe96fe727146b35af7e2e965eff9da4c51980",
   "claims": [
     {
       "id": "claim_a384a56f0ebf4add8c0ab7722b040888",

+ 1 - 1
openwiki/.claims/architecture/terminal-ui.json

@@ -1,6 +1,6 @@
 {
   "schemaVersion": 1,
-  "pageVersion": "sha256:63e03e3e582fd585379805751a335bfe7b208a8a64451a79c8c177698950d3a0",
+  "pageVersion": "sha256:b917b7bf26bb55ba79b12fcf2c7cfe5936d6463a6bd9c6bfe7eed55735c000d3",
   "claims": [
     {
       "id": "claim_7b6015e24275488a85ba88dd065f2ff8",

+ 106 - 10
openwiki/.claims/concepts/history-store.json

@@ -1,6 +1,6 @@
 {
   "schemaVersion": 1,
-  "pageVersion": "sha256:fb61743878eebfa7a936223b03e0c9a3d4247468218b42035889a35e1283bc0a",
+  "pageVersion": "sha256:9d1f1a31451538c0c3bbc043ab35ce90cbffd32e26263470d8b43f7dc2486279",
   "claims": [
     {
       "id": "claim_3afb8d7c0dbf446090433a98b74ad2e5",
@@ -17,7 +17,7 @@
       "statement": "The schema defines messages, chats, contacts, settings and image_descriptions tables plus an index on messages(chat_jid, timestamp) using CREATE IF NOT EXISTS, with no migrations.",
       "evidence": [
         {
-          "resource": "repo://src/history.rs#L397-L436",
+          "resource": "repo://src/history.rs#L411-L450",
           "version": "repo-lines-v1:sha256:3c733d522843b98c851557c73e574046d7229aecf27eb5d391e341f2d18e015c:eyJzZWxlY3RlZExpbmVDb3VudCI6NDAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6Ijg2MTQ5Yjg1MjZhMTc4ZjhmMzMzNTViZjU1MmMxNjJlYzdiZDRhNTQyNjk2MzhmNzlhOTkwODIzNzg1YjM5NWYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjE5ZTc2MzU1Y2YxMTk5NDgwYzNmOTk4MzZiMTQ4NmM4NDFkODkxZmFhZDkwNGM2MDhlMDkzMDIyZDM3NTdhZWUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImEzNWM0OWE4ZjYxOTQzMjJkOTYwYTAwZjQwNzA2ZDJkODAzMTE2NDRkNmZmZDhmZWJlNmZjZWIyN2EyZDhkZjYifQ"
         }
       ]
@@ -31,7 +31,7 @@
           "version": "repo-lines-v1:sha256:598d40867f709e3d9ec09cccd1e752c4ba2ae078a93b814058400d12451c022f:eyJzZWxlY3RlZExpbmVDb3VudCI6MTIsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjJjY2YyOTI3M2ZhMDRlMTFmMTY4ODliZTBiYWY2ZDg1MjdjN2Y1ZGNmY2VjMGVhMmZmNmU3NmViNDA1MGVhZTkiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImYzMWE1ZDk2MDM0YzIyMWYyMWVkZTQyYTlkOWM3ODgwMjQxZTI2YzJjMzQxOWQ4Y2M4MmNiM2JiMjdjNWJhY2MiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImQxZmRlNjg0N2FkMzhlOWUyYjA1ZjdmYTRiNDUzOTY2YzE2NjRkNmJmYTExMDJmY2YyYzJiOWNjNDM5MGQ1OTUifQ"
         },
         {
-          "resource": "repo://src/history.rs#L442-L463",
+          "resource": "repo://src/history.rs#L456-L477",
           "version": "repo-lines-v1:sha256:c8657638062a2f1c8faa48bb2091b77a95206c5c18fb7f83ad66ee472be3b0cc:eyJzZWxlY3RlZExpbmVDb3VudCI6MjIsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjgwYzA2MDFlNTlmNWFmYmUyODNlZjFmYzQxNjFlYjJmOGE1MGMwNGEzZjEzODY1N2NkMTIzOWMxYmQ4MWJjZGIiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImFhODg5OGI0M2QxZDJjNzcwOTgxMzgwNTc4Yjg1N2E3OTY3MzAxMmY4ZGFlYmYyYmIzMTkzNTk5MDg5OWNkZTgiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjViNmM5OWZlNDhhNTllM2Q4NzBhODg1YjNlYzIzZDIyNGNlYmE0Njg5MTZiMzhiOWQ5OTEwZjAwOWQzZDRjNTEifQ"
         }
       ]
@@ -41,7 +41,7 @@
       "statement": "The chat upsert keeps the maximum last_timestamp and only sets the name when the chat has no non-empty name.",
       "evidence": [
         {
-          "resource": "repo://src/history.rs#L470-L483",
+          "resource": "repo://src/history.rs#L484-L497",
           "version": "repo-lines-v1:sha256:6acfe5aab3d7f5a481c785e16736ae25b7d72275d885003104132702619fa2c1:eyJzZWxlY3RlZExpbmVDb3VudCI6MTQsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImFlMjhiYjUwNmYwZTUwYzIxZDU0Y2ZhNWJhNTBhZDA2MzBkNDYxODJlNzUwN2U4ZDcxNzMwM2YzM2UzODBlNTkiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImM5MzAwMTI1YjUzZjIyNWI5MTFmNjc3YmFjZWM4MWM2MmIwMzAyZjg2NWJkN2NhNTAyYmNmNTZhN2UzYjdkOGYiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjgxZWMzYmUyNjA1OTc3Njk1OWJlMTExMjVhN2JkNDUzNmMxZTk2MTRiZDlkMjhkZmU5NThmOTZlM2Y4YTI1NGMifQ"
         }
       ]
@@ -51,7 +51,7 @@
       "statement": "Only the trimmed non-blank push name of incoming messages can name a chat.",
       "evidence": [
         {
-          "resource": "repo://src/history.rs#L489-L499",
+          "resource": "repo://src/history.rs#L503-L513",
           "version": "repo-lines-v1:sha256:0b03e9dc798065c6f1704fcb1a70ba4927bcb0ef47900c115b45a6cca449bfaf:eyJzZWxlY3RlZExpbmVDb3VudCI6MTEsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImMwOTZhZGVlYmNmNjA0ZWMzMTNlMTJkOTI4ODk0NGE4OTZkYmJhZWRlZDJkMzdmZmFjYzFmOTRjMzc4MjVjYWUiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjhhN2NlZDc2NTcxNDVkZjIyMjc1ZDI4Zjc0NGI5Mjg1YWVmYzU2NmE5NjZjNjhjYzVhYjQ4YjgwZGQwZDgxYjAiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjlhZDRhMTk0MTFmMjVhNWNjOWFmOTAzNWNhMmQ2MmY5ZjdmMzViOWIzYTJmZWJlNDczY2JiZDRmOTNlZTEwYWEifQ"
         }
       ]
@@ -75,7 +75,7 @@
           "version": "repo-lines-v1:sha256:21c241cfd2393adfe0d73994422db78368373279b5a142e4604f76c30b8092c6:eyJzZWxlY3RlZExpbmVDb3VudCI6MTcsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImViODI2ZTYxYWIwODc5YTZkZGEzYTVhZWYyNTc4NTEyZGMzNzYyMjA0ZjcyNDc5NzI3MWVhMDI1YzFlMWI2NjEiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImFlNjMxYmYzODIyNWIyNzIzNWJjOTE2NTg0YjEzNWMwZjZkNWY3NjQzYzYzZDVkMTU3NzYyODM1NjdjMTRiNTIiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjdhYWZlOThkYmNlMTM2NmMxYmU4OTdkZjk0MmE3NzFmZDI0YzQzODE1Nzc3ZDUzZTAzNjgzYmEyYTk4Zjk3N2UifQ"
         },
         {
-          "resource": "repo://src/history.rs#L530-L541",
+          "resource": "repo://src/history.rs#L544-L555",
           "version": "repo-lines-v1:sha256:65dc24e0ca6d8e29340e1116eab12f58e8fd73c4ed13f85023a071ca22aacfac:eyJzZWxlY3RlZExpbmVDb3VudCI6MTIsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImZhMDdjZTgwZGE1NThjZWQxZDcxMjdiMjIzN2U0OWVmOTQ5OGNmYTA1OGNjNDZmMzY0NzdhNDcxM2ExNDljZGYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6Ijg2NTQ5MzhhNmY0OTljMDQ1ZjcyY2ZkNzNhNDNkMmExZWQ4MjhiZTk3YjI2ZTc1OWY1ZTQ3YjQ2MzllNzcwY2YiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjdiYzk4ZDZiMGRjNDkyYzUwMTZkZjEwODA1YTNhNTRlYjljMTg0YTVhNjEzN2U3ZjRiZWNmYTBjMjIwNTUwZDAifQ"
         }
       ]
@@ -133,7 +133,7 @@
           "version": "repo-lines-v1:sha256:7446cd3d43ecedda818c27075602eb8b9f1b3ef669ea39edf3a6c5520cc4bec3:eyJzZWxlY3RlZExpbmVDb3VudCI6MzEsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImE2OTlhMTQ5OTg2YzgxZjQ4OGYwMjQxMmU4Zjk0YjE1NjdhNWQ2OTEzZTNiYTg0NjJlMmFmYzY5NDJkYWYxMTMiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjRjZTNjZDVlMGE4OTI3NjIwMWQ2ZWU2OWE2ZGM4ZTEyMzgxNDVmMzEyYmE2ODQxZjUzMGYzNDQ0MmU0Njc2NjIiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6Ijk4NTAwNmMxYjY0YjM5MzI5ZWZhMmUxZmU2YTEwMWRiYzg0MTIzYjkxNjkyYjM1MjhjOTg4YzhhOGVmNGRjY2QifQ"
         },
         {
-          "resource": "repo://src/history.rs#L732-L743",
+          "resource": "repo://src/history.rs#L746-L757",
           "version": "repo-lines-v1:sha256:a3499b20fe534b6969c5dd2ce949d44f4ccf9e144ef0dc6beff4386a09b24b59:eyJzZWxlY3RlZExpbmVDb3VudCI6MTIsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjdmNTE1NGVmMDNiMmZjYTVjNzk0NTNlZDVjNGIxYjI2MzllMDVhNDM0Y2U1Y2YzM2RiYjM0NTIwZDA0OGJiMmYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjAxYmE0NzE5YzgwYjZmZTkxMWIwOTFhN2MwNTEyNGI2NGVlZWNlOTY0ZTA5YzA1OGVmOGY5ODA1ZGFjYTU0NmIiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImEwMGRiMDQwZjI5MDliZjk3OGZmMGE5NTZhNTVlODNjNjUzNDE4ZGU2YmI2OGY4MDQxZGE5NzZmOGQzZDIyNzAiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImE4ZTcyMmMzMzU5NWJkZDhiNWE3YTI0YTdlM2ZlYmQxMzFjNzA5NTYyMGQ0NTllMWFjZDg4MWYzOWIzNWEzZGQifQ"
         }
       ]
@@ -143,7 +143,7 @@
       "statement": "delete_chat atomically removes a chat's messages, their image descriptions and its chats row (including its autoreply flag) while keeping the contact and other chats.",
       "evidence": [
         {
-          "resource": "repo://src/history.rs#L369-L394",
+          "resource": "repo://src/history.rs#L383-L408",
           "version": "repo-lines-v1:sha256:c4f4b4352468ac2099f49717f06a98ea40bab4d5c62a4158e04234c744610ee6:eyJzZWxlY3RlZExpbmVDb3VudCI6MjYsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImNkMTFhNTg0ZDdhNWU5N2U1ZmY4NmU3MzY5ZDBkMzdhNGYxZjlmZjBlYzRlMzk5OWEyZWRkYTc2MDMyNTQwMGYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjNlMjRjZDNlMGNmY2Y4OGRmMGJmNGU1YjRjYzY0MzdiODBhMjAyYjEwNzk5Y2JjNTMyNzBjY2ZlMGRjNDk5MjYiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImYyZWNiYWUyM2ZlZTE0ZTViOWIzNWQ0NDcwNmFmOGQ1OWY1NWQyOWIwNzYzNjU4ZjI4YjIzNjZkNWUzNWM5Y2UifQ"
         }
       ]
@@ -153,7 +153,7 @@
       "statement": "save_image_description upserts one description per message id (a new one replaces the old) and image_descriptions returns the descriptions of one chat keyed by message id.",
       "evidence": [
         {
-          "resource": "repo://src/history.rs#L334-L367",
+          "resource": "repo://src/history.rs#L348-L381",
           "version": "repo-lines-v1:sha256:63f7062cc95232bf44796ae0a8c94de3eb0412d9afad7f881a13da1e20f1c794:eyJzZWxlY3RlZExpbmVDb3VudCI6MzQsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjMwMjkwMTRkZWU5ZjY3MjRkMDM1MGYyZGRkYWU4YWE2MjYwODI3ZjdkNTc0NTc1OTBlMzQzOTFiYTRlOTMyNWMiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjJhYWFmOTZhMzJlYWM3NDA3ZjM4NGI3MzBkM2ExYzU0NDhjMjhiMTEzNmU3NDc1NWE4YWY2ZWNiMGQwNDE5MDciLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6Ijk4NTAwNmMxYjY0YjM5MzI5ZWZhMmUxZmU2YTEwMWRiYzg0MTIzYjkxNjkyYjM1MjhjOTg4YzhhOGVmNGRjY2QifQ"
         }
       ]
@@ -167,10 +167,106 @@
           "version": "repo-lines-v1:sha256:630d929422a0e6baf9ec9733842246510670f4cbd13a1a58e80d87fb8815c590:eyJzZWxlY3RlZExpbmVDb3VudCI6MTQsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImRmYWFhMjFhYzg3YThjYTczYzk4NDc5YjUzNTQ1MDc5M2U5MmU3MmE3ODg3ZmU5YjBmNmYwNWFiNzFmNDBlMGEiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImY5OTM0OWRjNmNiNTFlOWIwMzQxMTdiZjQxMzkzZDE5YzQzMGM3NTNmZDcxZWZmZmZjZmQzYmIzOGFhNjU0ZmYiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjlhNjc2ZmQ2MzliOTMyOTliYTFhMzBlY2QyM2NiMDQ1Yzg3OTBlMDc1OTAyNDRkNjY0ZjU0NTQ2MWIxMTc0ZDAifQ"
         }
       ]
+    },
+    {
+      "id": "claim_b381aec92a7146d19eabb8b2c887026d",
+      "statement": "update_message_text is the only method that rewrites the body of an already stored message: the insert is INSERT OR IGNORE and no other statement updates messages.text. It sets only the text column, is not scoped by chat or sender, and returns true when a row matched the id and false when the id is not stored (nothing written).",
+      "evidence": [
+        {
+          "resource": "repo://src/history.rs#L334-L346",
+          "version": "repo-lines-v1:sha256:7158060f4184225f939b1e15e679939dd96cb0e27d2a091e5a93f04a109ddcca:eyJzZWxlY3RlZExpbmVDb3VudCI6MTMsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImIxOTE0MGNlZWY0NWM2ODc5MTcwOTIxNGM1ZmJmZmQ2YTc4Mjg1OTM3MjJhMzRhODkwYWEwODdkZTA1ODhkNzMiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjJhYWFmOTZhMzJlYWM3NDA3ZjM4NGI3MzBkM2ExYzU0NDhjMjhiMTEzNmU3NDc1NWE4YWY2ZWNiMGQwNDE5MDciLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjlhNjc2ZmQ2MzliOTMyOTliYTFhMzBlY2QyM2NiMDQ1Yzg3OTBlMDc1OTAyNDRkNjY0ZjU0NTQ2MWIxMTc0ZDAifQ"
+        },
+        {
+          "resource": "repo://src/history.rs#L452-L497",
+          "version": "repo-lines-v1:sha256:2e34a474c156436f62541cf1f20071ce0ca2a9727793c2f9d871ff9ec1373f91:eyJzZWxlY3RlZExpbmVDb3VudCI6NDYsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjA5ODE1MjViOTE3ZTllNWMwOGI0ZDcyODU1M2FiN2IxYjhmZDFlNjhjMDBjNGEyMGVlODQxMDRiZmM3NTVhZWUiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImU5NmY4Y2IwODNmYTgwMGE0ZGNkMjk2OTcyMmQ3YjkyNjg2YWRkOWViZTM2NTU0OTJmYTY3MmIxYTdjZWQyNDIiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjgxZWMzYmUyNjA1OTc3Njk1OWJlMTExMjVhN2JkNDUzNmMxZTk2MTRiZDlkMjhkZmU5NThmOTZlM2Y4YTI1NGMifQ"
+        }
+      ]
+    },
+    {
+      "id": "claim_c5480b9d9c0d40f99c98c46e354c2f61",
+      "statement": "The transcription step of the AI autoreply calls update_message_text to overwrite a voice note's body with `[messaggio vocale] <transcription>` (VOICE_NOTE_TEXT, a deliberate duplicate of whatsapp.rs's private voice placeholder), guarded by message_exists and discarding the returned flag; the previous body was exactly the placeholder, so the placeholder is re-created as a prefix rather than appended to arbitrary text.",
+      "evidence": [
+        {
+          "resource": "repo://src/app.rs#L36-L41",
+          "version": "repo-lines-v1:sha256:9fa73b08edeb6e3bc3d3d40b114c9c25dac41393f654aef4f34b25bf000b64f1:eyJzZWxlY3RlZExpbmVDb3VudCI6NiwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiYWM4OTZiNjcxOWM0NWUwNjBiYzYxYjc2OTVlNWM3NjY5MDAxMGJhYWY5MjUxNTdjZGRmNzkyMzljYmJmYjBmMiIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiZjQ5M2E2NjFmZWY4MzY3ODU3YWJkODk4ZWY5YjAxZWYyMmY2ZDg0MjlhOWEyNGI3MjRjY2Y1ZTZjNDEzNTc2MyIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiZDM0MjI5OTEyYzRlYzAxNjlhMzMyOThiMTE1ZGI1ZWVmY2E4YjJjYmIwOWM1ZGVkOGZiZTZjNjVjOTdkOGRmYyIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiMjgxYzllMDA4NDg0YjQyMjE2ZmEyYTU5NjFkZjIwMzdhZjIyY2ZmODMwNWEyYWM5YzQzOTU1MWVjN2U5NDgwOCJ9"
+        },
+        {
+          "resource": "repo://src/app.rs#L784-L801",
+          "version": "repo-lines-v1:sha256:57ac76a55fce9ecc1f533f616fb4cdfc01c0da29873e427e3184c6d9dc184a58:eyJzZWxlY3RlZExpbmVDb3VudCI6MTgsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImJkYmFiZjFkNjI2OTdjZWE2NTk5ZWQyNjg5MDQzY2RhYzAyOWUxMWQ3NWIwY2U4MTRlYWVkNzhjZTk2Zjg5ZGMiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6ImU2YjA0NDkxNDY5NzIyZTgzNjNhOTRlNDA0NDUzOGUwMTNhMmEyMTdlMzZiMWU1NjhiYTYwYWNiN2I0YzhmMmUiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjNiOWQxNjk5ZDM4YTRkZDQ3ZjgxMGFmMDViYTIzZTM2N2NiNTE5NzljODAxM2Q3YTZmYWQyYjAzY2Q0MzA0MTEiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjdkNGY3ZDhmNzA1MzNkMmM0NTJkYzMwZTM1NzY1OTM2ZWE2NzkwMDIyODlmNTljOWQzMDU4ODc0ZjdjYjM1MDYifQ"
+        },
+        {
+          "resource": "repo://src/whatsapp.rs#L45-L46",
+          "version": "repo-lines-v1:sha256:3474d614982a770557cfcfc53711175e69a7c9841bfebcf210f0643f00ff9581:eyJzZWxlY3RlZExpbmVDb3VudCI6MiwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiMTU4MDBhYzJkZDhhYmM0NmUwNjVhMGY3Y2ZkOGMzNWYxZDRjMGUwYTBlNzNiZDc1MmY4NDRlZDVhMWY2MGU3MCIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiZjhlYjE3YjM5MDA1ZjBmMTU4MDZhOTg0NzBjOWE3NmUwOTZhMDFhMWI2NDJjY2UyZGRhMjA4M2E3YWQxYTExNyIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiZjExNmYzOTNiOTcyNzBhZmRjYzA1NTY1YWU1M2U3ZjU3YWNhMjQyM2I3ZGE3MzYxYjcxZjkzYWQyZWRlMzJlNiIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiYTMzZTY3ODlmYzBiYmYxNjczODJjNzljNTE3ZGFhZGEyYTk2ZDY1NWRmMDRiZmI3YTg5N2IyZTI0NDA0NjA0MiJ9"
+        }
+      ]
+    },
+    {
+      "id": "claim_0098ddf4255a482ba979394ac3011f5d",
+      "statement": "Transcription text is stored only inside messages.text: the store has no transcription column or table, so deleting a chat (which deletes its messages by chat_jid) also discards all of its transcripts and the placeholder prefix is the only marker that the audio was transcribed.",
+      "evidence": [
+        {
+          "resource": "repo://src/app.rs#L792-L797",
+          "version": "repo-lines-v1:sha256:9898eb8c9ec5fb7d3f4d48e6202a56aa5d8b21ac027749c821a8fc49e1a2b229:eyJzZWxlY3RlZExpbmVDb3VudCI6NiwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiYTYwNzU3ZTQwZGI3ZTJjZDBiYzFkMzM3OTkwMmVhMjNjMGI2YzBhNGEwMDI4ZTg4ODU2YTBmOGIxYjI1YzI0NCIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiMGM2NDc5Y2MyMzA0NDBmZDQxZTgyMjQyMTg2Nzk2OTg1NTlkMjg5M2JkNjdiNDM4MGFmYWI4MTFiMzhhNTVhYSIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiNjBkYTQzMWQ5MmZlYjM0NGEzYmEwZDhkYWQ4YzMyMjIzY2I4OGUxZTQ3MmIxZDYyZmU1MTkzN2E2ZmZhMmNhOSIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiNzU3NGNlM2NlN2MyMzQ3NWQxNGY5ZjkwZWQ4Zjg5ODczMmM3ZTAyOGVkNDJmNjg1ZTAyYmU4ZThhM2UwMTUxYyJ9"
+        },
+        {
+          "resource": "repo://src/history.rs#L383-L408",
+          "version": "repo-lines-v1:sha256:c4f4b4352468ac2099f49717f06a98ea40bab4d5c62a4158e04234c744610ee6:eyJzZWxlY3RlZExpbmVDb3VudCI6MjYsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImNkMTFhNTg0ZDdhNWU5N2U1ZmY4NmU3MzY5ZDBkMzdhNGYxZjlmZjBlYzRlMzk5OWEyZWRkYTc2MDMyNTQwMGYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjNlMjRjZDNlMGNmY2Y4OGRmMGJmNGU1YjRjYzY0MzdiODBhMjAyYjEwNzk5Y2JjNTMyNzBjY2ZlMGRjNDk5MjYiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImYyZWNiYWUyM2ZlZTE0ZTViOWIzNWQ0NDcwNmFmOGQ1OWY1NWQyOWIwNzYzNjU4ZjI4YjIzNjZkNWUzNWM5Y2UifQ"
+        },
+        {
+          "resource": "repo://src/history.rs#L415-L450",
+          "version": "repo-lines-v1:sha256:4ca7d34e842380ce2a5c1e60682b5710227d931679cb8406ec990fb6e5dadec8:eyJzZWxlY3RlZExpbmVDb3VudCI6MzYsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjVmNWI1MzQ2N2IzY2RmMTYxNjU0Y2U2YWEwNmNiYzkzNjlhOTNhM2ZhZDg2OTBkMWUxZWY3OWI5NWY0Mzk1NDgiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjdlMTEzNDZmMmE0ZDdkYmU3ZjE4ODhkZGJjMzViZjQyYWIxN2M0ZmFlZTkyOWUwZDY2Mjc0NWEyMWI4Mjc2NmUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImEzNWM0OWE4ZjYxOTQzMjJkOTYwYTAwZjQwNzA2ZDJkODAzMTE2NDRkNmZmZDhmZWJlNmZjZWIyN2EyZDhkZjYifQ"
+        }
+      ]
+    },
+    {
+      "id": "claim_0ab1354c4aa943baa68a49d9c264f443",
+      "statement": "is_voice_note is true only when the trimmed text equals `[messaggio vocale]` exactly, so a stored transcription no longer matches it; audios_to_transcribe relies on this to treat a message as already transcribed (each audio is transcribed once even though nothing else records it) and src/autoreply.rs passes the stored text to the model instead of its cannot-listen sentinel once a transcription exists.",
+      "evidence": [
+        {
+          "resource": "repo://src/app.rs#L900-L918",
+          "version": "repo-lines-v1:sha256:aca8ef54d074bdd9a35c6228ea67532c04a9bf55079ae6ed368a56dfc7f87db1:eyJzZWxlY3RlZExpbmVDb3VudCI6MTksImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjI3MGQwY2JkODBmOGZhNDZkN2EzOGQxNDMyMzZjODMzYmViZGE4Mzg2ZGNmZTFkZTBjYmNjNWM2M2IzNGE1MzQiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjRiNDI5OGJkNmI1YjQyZmVjNDI0NDIxMjBlNTkzZjAxZDFiYTU0YmFhODg2NTUwMzE0MWE0YmQxYmViNjQxZjgiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjIyMGUyNjkzMzFhZTU2NTJiYTMxNWViYTdmYzRlNWMxNWFhNDlhMzdiMjE2NGY3NjRlYmNiYTA1ODE5ZGMyNDAifQ"
+        },
+        {
+          "resource": "repo://src/autoreply.rs#L364-L379",
+          "version": "repo-lines-v1:sha256:16e2a124c955fde9bf455d688fa7aa42035a5fed15752ff59f0e5b47682c1dda:eyJzZWxlY3RlZExpbmVDb3VudCI6MTYsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImQyMWNlZTczZDBjMDU5ZmU1YjQ1MDVlMmJmMmY5OGE2OGMwY2Y4ZWJmNWI5Y2Q1MTAzNGY0ZTZhZTJkNWQyMGEiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjE5ZTc2MzU1Y2YxMTk5NDgwYzNmOTk4MzZiMTQ4NmM4NDFkODkxZmFhZDkwNGM2MDhlMDkzMDIyZDM3NTdhZWUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjQwNWQ4YWU3NjdiNjQ4YzZmM2Y4ODE1ZDZjODQ1MjFiOTQ5MWVmZTEyNmRiYTVmZjU0MWIyMmJjNWY3YjllNWQifQ"
+        },
+        {
+          "resource": "repo://src/whatsapp.rs#L1174-L1181",
+          "version": "repo-lines-v1:sha256:56200a5278b4431e6444218582169aabd8d4b1d57de764de0856dc98838c94d1:eyJzZWxlY3RlZExpbmVDb3VudCI6OCwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiZDI1MjE2Y2MwNzJmMWZjOGZhNTU5MGQyMjRjMjE5OTI1NTExNDkxMThjYTg1ZTlmODVkNGEwYjA1MjQ0N2IxMCIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDEyY2EzNDVjY2Y3NWJmOWMwODA2YmNlNjk1YmU4ZGU4MDhiNzk5ODQyNTFhN2E1NGQyMDJjZjYxMDFkZDQ1MSIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiN2ViZDBkMDlmODEyZmE0MjI4M2ExZGM5N2Y0MzdmYzQyNmIzZTg5NGI0NWUwMWFjNThjNTM5NTc5YmNhMDQ1ZSIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiMDdiNDRmYjk0MDJlZDMxOTc3NTA4MTZmZDNmYTg0MWZlM2VkMDNkMzllYWJlOGY3ZjZmOGI5MzgwMTI0NTU2YiJ9"
+        }
+      ]
+    },
+    {
+      "id": "claim_cacb86b26c5b4183a11194ccca0b5bb1",
+      "statement": "The schema declares no foreign keys and no ON DELETE cascades, so referential integrity among messages, image_descriptions and chats is maintained entirely by the store's methods, with delete_chat deleting the child rows explicitly.",
+      "evidence": [
+        {
+          "resource": "repo://src/history.rs#L389-L408",
+          "version": "repo-lines-v1:sha256:d484ab395f4a698a496310952b5cc06efa91723fc15452bd89fda7aebb9a4e36:eyJzZWxlY3RlZExpbmVDb3VudCI6MjAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImE5OGYyMjg4YzNjZTYwMTAzOTI1NmQ0YzdjMDI3ZGJiYTYxODFiNTM5ZmRhZjBlN2NhY2JjMGQ3Mzg3YzgwMjAiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImM1MzY2ODc3ZDQ3ZGY2Zjg3ZTNmNjk0NDY0YmUxNzdmMjljNTRjNDBhOTc1NDY4NDA1ZTJhMDk4MGRmMmIxY2MiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImYyZWNiYWUyM2ZlZTE0ZTViOWIzNWQ0NDcwNmFmOGQ1OWY1NWQyOWIwNzYzNjU4ZjI4YjIzNjZkNWUzNWM5Y2UifQ"
+        },
+        {
+          "resource": "repo://src/history.rs#L415-L450",
+          "version": "repo-lines-v1:sha256:4ca7d34e842380ce2a5c1e60682b5710227d931679cb8406ec990fb6e5dadec8:eyJzZWxlY3RlZExpbmVDb3VudCI6MzYsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjVmNWI1MzQ2N2IzY2RmMTYxNjU0Y2U2YWEwNmNiYzkzNjlhOTNhM2ZhZDg2OTBkMWUxZWY3OWI5NWY0Mzk1NDgiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjdlMTEzNDZmMmE0ZDdkYmU3ZjE4ODhkZGJjMzViZjQyYWIxN2M0ZmFlZTkyOWUwZDY2Mjc0NWEyMWI4Mjc2NmUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImEzNWM0OWE4ZjYxOTQzMjJkOTYwYTAwZjQwNzA2ZDJkODAzMTE2NDRkNmZmZDhmZWJlNmZjZWIyN2EyZDhkZjYifQ"
+        }
+      ]
+    },
+    {
+      "id": "claim_aa83b0cbbe354e229e15875dcc735284",
+      "statement": "Because set_autoreply upserts the chats row, enabling the autoreply on a chat that has no messages creates a chats row with last_timestamp 0: the chat appears in the chat list (sorted last, named by the JID user part until a contact or message names it) but the startup catch-up skips it since it has no last message.",
+      "evidence": [
+        {
+          "resource": "repo://src/app.rs#L964-L981",
+          "version": "repo-lines-v1:sha256:15f024e6c558596295755f720dfa14a980113d18c386f066c5353019c2917bce:eyJzZWxlY3RlZExpbmVDb3VudCI6MTgsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjllZjk3NWI0MzRkMmRhMjhhYWMwZmUwMGVkYjI5MzE4NzY0NzA5OWU0ZmUzOThkNWJlMTI3NTAwMDQzOWY2NmIiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjE2MmVlNTdlNDFhOGMyMGFlMDE2NGRmMjYxNDM5ZGNjNDk5ZDM1MjA4MzQxNTE1MTZkMDQ3ZThkZWFlOGE0NjYiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6Ijc4OTU5NGE5NzIzN2Y2NjVkMmU0Y2RhMTIyZmY1NTIyNzJjMjVmZTFhMTI0NmZjMGNkNjllMDQ4MjE2ZTNlYWMifQ"
+        },
+        {
+          "resource": "repo://src/history.rs#L255-L270",
+          "version": "repo-lines-v1:sha256:1f5fe70558e4ea0b095d596aae2cd7786c7e8d94397391cd9cd582e5a4829d6d:eyJzZWxlY3RlZExpbmVDb3VudCI6MTYsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjU0MTJkMjhkMWQ0ZWFjNjU5YTJhMGU3MGRjNmJhOWMxMjk4MGJmYzg4NDY1N2FkMWM4ODlkZTMzODE4ZDlmNzAiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImJhYzU1MzBkMTVhODE0YWY3YTA4ZTYxYjYzMjY1ODQ5MjBmMzQ4ODE3NWFjZmZmMDlhOWVhNjE0ZmM4MmZiZTQiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjYyNTBlZmE1NzY2OTg2OTA2MTJhMjQ4YTYzMDNlZDUzOGQ2MTVmYzVkMzEzNTk1OGEwODQzYzEyOGY0OWZiOGIifQ"
+        }
+      ]
     }
   ],
   "verification": {
     "by": "openwiki/0.7.2",
-    "at": "2026-10-21T21:49:12.291Z"
+    "at": "2026-10-24T16:42:08.550Z"
   }
 }

+ 1 - 1
openwiki/.claims/integrations/ai-client.json

@@ -1,6 +1,6 @@
 {
   "schemaVersion": 1,
-  "pageVersion": "sha256:fddcffbcc714b9d61c2a530a57c5b40dbc6dd4279ee7a363ee96b0d7a44fb7bb",
+  "pageVersion": "sha256:4927c894dab42fbe05e52e939703259666aa47947ef7945c623bfa2116a02e4d",
   "claims": [
     {
       "id": "claim_c5dd9ee6003245adaa1701eb40862582",

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

@@ -1,6 +1,6 @@
 {
   "schemaVersion": 1,
-  "pageVersion": "sha256:5fefadb20f2b8cf87afcc027db804249288fd112025b45dd9e740fc8bca85d7e",
+  "pageVersion": "sha256:deebb3a40f405864bed483e185086a069b8cd23cd1464a3e108c07917ca9b04c",
   "claims": [
     {
       "id": "claim_3b56098917ee4e02b2f8ede066a9921a",

+ 50 - 36
openwiki/.claims/integrations/whatsapp-client.json

@@ -1,6 +1,6 @@
 {
   "schemaVersion": 1,
-  "pageVersion": "sha256:18ed8fdf1d8ab4689b600aa550246fd3d6b6b26340ea7a345c775d128f40bc56",
+  "pageVersion": "sha256:4bfc94bcec7d0629fe535e916bfc05dd51ef04c1fe94a8b004f82f7f5cfb98d9",
   "claims": [
     {
       "id": "claim_5cd95bce2100497cbe7ac9e4c2cb1856",
@@ -17,11 +17,11 @@
       "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#L298-L312",
+          "resource": "repo://src/whatsapp.rs#L351-L365",
           "version": "repo-lines-v1:sha256:803ade08ccaecd3502b4e412e74510353b9c4495ad8c7ba92531c66c20e7145c:eyJzZWxlY3RlZExpbmVDb3VudCI6MTUsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjEzMWZkNzY1ZDk5ZGViYjBhNjQwNjJjNzhjMzdmYjVkYjZlODdmOWIwMmJmMDVhOTE4NzQ5NGFmYmM4ZjgwYmEiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjE5ZTc2MzU1Y2YxMTk5NDgwYzNmOTk4MzZiMTQ4NmM4NDFkODkxZmFhZDkwNGM2MDhlMDkzMDIyZDM3NTdhZWUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjIzZDg5MDg5YWZjZjlkYTg2NDg4ZDFjM2E1NDk2YzEwMDIxZGNlMWE0Mjc2YTYwNWUyNzY3ZWM0YmVjYzliYWUifQ"
         },
         {
-          "resource": "repo://src/whatsapp.rs#L369-L416",
+          "resource": "repo://src/whatsapp.rs#L422-L469",
           "version": "repo-lines-v1:sha256:dd10b6e01369abf7053e52f04c2399ba9fe4a096f9593c2658e91f60650f6fbc:eyJzZWxlY3RlZExpbmVDb3VudCI6NDgsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjRmZDNiZDg2NTQyNDAyYjk5Y2JjNDI1MGY1YmQ3YTE5MmNkM2VhODgyNGFjNjFiMDI4NTc1NzQ1MzZmNDAxYTgiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjM1ZjZmNDY2ZTU5Y2ZjMzA3MDNjNDVmNzJhZjY1ZGU1NGFjMzYzZDM1NGNjMWEzMzMwMzUwYWNkNDUyNDM0YzQiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjIzNjEyMDNhNTJjMmI5YTk4MmJjMjVkNWE3OTU1OWIwNGJlNjAxODcxYzZlZjk1ZWExZTMzNzU4MmM2YTRlZDcifQ"
         }
       ]
@@ -31,22 +31,22 @@
       "statement": "EventForwarder forwards raw events in order to an internal channel with interest limited to SUBSCRIBED_KINDS, which includes OfflineSyncCompleted.",
       "evidence": [
         {
-          "resource": "repo://src/whatsapp.rs#L419-L440",
+          "resource": "repo://src/whatsapp.rs#L472-L493",
           "version": "repo-lines-v1:sha256:90cc37eed600d805f12bf6cff91abb61379cdfa881ee401bcd902a8e11133f60:eyJzZWxlY3RlZExpbmVDb3VudCI6MjIsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImQxN2YyYmY0MzZiNzViNGYzNDUyNWY2MTk0NjU4OGE5ZmQ3MjRkNDA2ZDNmODk0NjI4MTE2ZGRlNzg0NDVmNTAiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImZiOGQyYjQ2OTczM2U4ZTNmOWVjNWRhMDk2Njk3NDlkMDJiYTczYjM1YTQzYmQ2YzRkNjNhYTE4M2FkY2U5ZGUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImU0NTQ4YjU3YzUzZDdhYmNmY2QwY2E0YTJjZGZiZjdhYjk4NWYzMDYzMGM1N2FiYTA2Y2U1MGZmYjc2ZjZiNmMifQ"
         },
         {
-          "resource": "repo://src/whatsapp.rs#L55-L74",
+          "resource": "repo://src/whatsapp.rs#L58-L77",
           "version": "repo-lines-v1:sha256:361053245006ac3f73c75b71c22349865d2290626d5d26e9439e3cedd7193959:eyJzZWxlY3RlZExpbmVDb3VudCI6MjAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjkxN2YxNjJiMTNkNjQ1MzcxOWQ0ZTgwZGQxNzc3NzM5Yzc4Y2M5YTIxNDM4MTBmNDU5ODBhOGQwNWUyOThjNzMiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6ImIwMmQyYzBkNmRjNWU3NGQ5MmZhMTRiOTFkYmVhMjY0MDkwYmViMzVmZGI5MTg4ZDliMzQxMTgxZGNlOWY0Y2UiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjNlMzgwNWRiNmFmZTQwNDJiYTcwZTM1YTQyNGFmYWEwZDIyOTUxYzUzZTEyZWU5YThiMTFhMGRlNjZlMDMyMjciLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjhjODM5ODYwY2FlZWIwMmYwOGMwZTE3NDIwYTNjOTFlYTRhYmY0NTg5OGUwNGI3OTIzZjRjYmFhZTRiNjg3MTIifQ"
         }
       ]
     },
     {
       "id": "claim_2e8c63c28ca0475da8d2604a3bb43b93",
-      "statement": "WaEvent has Qr, Connected, Disconnected, LoggedOut, OfflineSyncCompleted, Message (with an optional image reference), Contact and Error variants.",
+      "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#L104-L131",
-          "version": "repo-lines-v1:sha256:0c68b0b9c76d57c34f906ece59c675e4be58899b2a26f2631d9ee4722c3e8379:eyJzZWxlY3RlZExpbmVDb3VudCI6MjgsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImUyMDliMmJhYjE2NjZhZjkyYjQxZWViYjI0MWM1MGQ5ZjI5NmRiMDViOTMyNzI5MzExYzFjM2NkYzY3ZDUxNDkiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjAxYmE0NzE5YzgwYjZmZTkxMWIwOTFhN2MwNTEyNGI2NGVlZWNlOTY0ZTA5YzA1OGVmOGY5ODA1ZGFjYTU0NmIiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImIxNmE2ZjRjMmRkNDg2YjE4NDI2YWUzNGYyMjc5YWViYjM4OTY0OTM3MTE0YmZmZjc1NmY4NGIwY2RmZGM2NzgiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjcwODc1MzQwZmI2YTRmMjNkNmJmMzU5MGU5MzU3ZDBhODNjMjQ0NWM3OGQwYTg4ODc0ZjQxMGM0OGRlMzA1NGIifQ"
+          "resource": "repo://src/whatsapp.rs#L109-L135",
+          "version": "repo-lines-v1:sha256:c833ef517073c3c33b9b8885771f54d2c0026fb424dc83e1cb1c7850d0e6b75d:eyJzZWxlY3RlZExpbmVDb3VudCI6MjcsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjgwYzU5NWY0YTU0MTFjZTFkNGJjNzJiZTEzMjQ0ZTgwZTc0MDg1MTAxZGQwYTdjNzQ4YjcyYzViYWQ5MDkxYTkiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjIxOTJmNjU0MjE1OGExOGY0MDRlMjg1ZmI5NGM2N2VjYmFmMjg0NzUxOGM5ZjQ3Y2IzZDA3MjVmOWYwNTk0NTUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjY1MTM1NTA1ODhkNzA3ZGJiODBlZDU1N2ZjZTRlYTU1NjY0ZWVlMzM2NWE1YzQxOGJjM2FiZjQ4MzNiZDNmZDYifQ"
         }
       ]
     },
@@ -55,7 +55,7 @@
       "statement": "Translator::handle_event maps raw events to WaEvent, including OfflineSyncCompleted, and turns failures into descriptive Error texts.",
       "evidence": [
         {
-          "resource": "repo://src/whatsapp.rs#L585-L634",
+          "resource": "repo://src/whatsapp.rs#L638-L687",
           "version": "repo-lines-v1:sha256:be3833261dc69977863314832b6160d74b61cfd510ba80f67d687d7b53649355:eyJzZWxlY3RlZExpbmVDb3VudCI6NTAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImUyYWRmNTE3NDJlNmQ4MmEyNDE0NjE5ZmJlOWRhOTMzZGIxYzVjYTZlZGIwZjRhZmJhMGE5NmMyNGEzNjA1OGYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6Ijk5OWJkZWZlZWFjYTYzNDBlMDI5MGRjODZlODg5Yzg5ZDVlMTg5YWE1YmE0NGI1ODViM2I3MDczNGY0YTYyZDgiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImM2ZTYxOTdhNjY3YmI1ZjgyYzI4ZDdmYWM2OGIzYWE1YzU1YTk5NmQ2NGRkYmYyOTI3ZWQ3YjAyNzk5MDI0NWYifQ"
         }
       ]
@@ -65,18 +65,18 @@
       "statement": "canonical_jid prefers the phone-number JID, resolving LIDs via the alternate JID or the store LID mapping, and reports lookup failures.",
       "evidence": [
         {
-          "resource": "repo://src/whatsapp.rs#L541-L556",
+          "resource": "repo://src/whatsapp.rs#L594-L609",
           "version": "repo-lines-v1:sha256:c9a3a286dc3e2c684087cd5ca273467ac6be5cc180556477e58883a1587ad161:eyJzZWxlY3RlZExpbmVDb3VudCI6MTYsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImRjNzhjNDlmYTU0ZmZiOGEwMmNhNDczYzhjYTFmYjY3YjVjNDQyMjBiNDUyMDYzNWI0NmI1MjgyODRkODc1MjgiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjY3ZWQ4MGI0MjM1MDQ2ZTljZWQ4OTEyNjc0ZDRhMTMwMjM3MzJiNTNhYThhNDdhMGYxNzUyNGI2ODM1ZjAxM2YiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjcyNDliZThhYWJlNTBkZDkyNzY5NDkwNzViMDI3NTA1Y2Y0ZWQ3MWE3NmIxYmJiZjg0ZjFhZTFmMjdkZDU4Y2UifQ"
         }
       ]
     },
     {
       "id": "claim_bdbfcaf218dc45d88ab121c191882d66",
-      "statement": "handle_live_message skips status broadcasts and silent messages, canonicalizes 1:1 chat JIDs, notes push names and attaches an ImageRef only for image texts.",
+      "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#L650-L696",
-          "version": "repo-lines-v1:sha256:a4d4ee9a079e03eb111e30743b9678dab815a424e54ce5a0be4a0f1ddd68051c:eyJzZWxlY3RlZExpbmVDb3VudCI6NDcsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImFkMmYxYjJjNzYwNGI3NDk3NmI5YmMyNjRkMDBlYjJlODJlZWM0MTk5NzE4NzJjMWFjOTE2OTJhNDQ1ZjMxYmYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjdkZTRkYTlhNTUwNmQ1MmI3NWUwOTY1YjY0ZWI2Zjk5ZDhjMWE4MmZjMWU1ZjE1YTZhMjY0MDhlYjc2MmJlOTUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjY2NDFiNWZlM2M5YmRjNTg2MGQzNmM1MzMzZjMyYTY2OGU4OWUzNWJjZTBiNjU2ZTViYjY2MjVhNjFhNTE3ZWQifQ"
+          "resource": "repo://src/whatsapp.rs#L703-L755",
+          "version": "repo-lines-v1:sha256:b8e0680690aaa46f7e4bc079ff00bc4c7cd87b2d32b6fe4595fb846fad76e44f:eyJzZWxlY3RlZExpbmVDb3VudCI6NTMsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImFkMmYxYjJjNzYwNGI3NDk3NmI5YmMyNjRkMDBlYjJlODJlZWM0MTk5NzE4NzJjMWFjOTE2OTJhNDQ1ZjMxYmYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjdkZTRkYTlhNTUwNmQ1MmI3NWUwOTY1YjY0ZWI2Zjk5ZDhjMWE4MmZjMWU1ZjE1YTZhMjY0MDhlYjc2MmJlOTUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjY2NDFiNWZlM2M5YmRjNTg2MGQzNmM1MzMzZjMyYTY2OGU4OWUzNWJjZTBiNjU2ZTViYjY2MjVhNjFhNTE3ZWQifQ"
         }
       ]
     },
@@ -85,26 +85,26 @@
       "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#L442-L449",
+          "resource": "repo://src/whatsapp.rs#L495-L502",
           "version": "repo-lines-v1:sha256:63fcb3bcc3fee239b6af98cdaeb31163e5ebb60f59467e221c3df0d186d7af3c:eyJzZWxlY3RlZExpbmVDb3VudCI6OCwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiYjU2NmIzYWVmYmU1NWMyMjE5ZTM3MzEzYmFiOWMwZDZkNGNmY2Q3NzUzMjZiMDQ1NWUxY2Y1YmM1MzE3OGZiNyIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDEyY2EzNDVjY2Y3NWJmOWMwODA2YmNlNjk1YmU4ZGU4MDhiNzk5ODQyNTFhN2E1NGQyMDJjZjYxMDFkZDQ1MSIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiMTllNzYzNTVjZjExOTk0ODBjM2Y5OTgzNmIxNDg2Yzg0MWQ4OTFmYWFkOTA0YzYwOGUwOTMwMjJkMzc1N2FlZSIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiMjg2NmRmMDRmMjc4MGQyOWI2MDQ1YWM1OTM2NmUzZjI2OTk5ZDIxMTdjZmVhZTcxY2ExZGQ5NzcwNGE5YTY1YiJ9"
         },
         {
-          "resource": "repo://src/whatsapp.rs#L504-L522",
+          "resource": "repo://src/whatsapp.rs#L557-L575",
           "version": "repo-lines-v1:sha256:0cb33ca45f9b72198804d2a68dffd870e11f0018eff0f5b4d6e6639f0dabecb2:eyJzZWxlY3RlZExpbmVDb3VudCI6MTksImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjI3YzZkOTQ3ZWQ4OWVjMDgxZmY1ZDUxZDliMzYxOWRhOWRmMjZhODdlZGYxM2MwNDMwMzhiMDRjN2E5ZTliZWEiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjZmYjQ4MjFjNTZhMzYwOTAzMGFlYzM0MWQ4ODI5NDhmZjBlMmYwY2VlNDQ1MzQ0YzczNTJiYmMwYjlhYTA3Y2EiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjY1NmEzOWUwZGJjMTIyMGQ0OTg3Mjk2NDBiOTEyNWQ0YjIxMWMzNmVmZGRlODVkMWIzZmM4NjgyZTU2NTE0NWEifQ"
         }
       ]
     },
     {
       "id": "claim_835ad8e902fd4a18909210d2c9e1408b",
-      "statement": "History sync chunks are decoded in spawn_blocking and emit push names, inline contacts, conversation names and messages without image references.",
+      "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#L730-L822",
-          "version": "repo-lines-v1:sha256:6c5078dc648f03cc0bba1f6aced5618a9773cf5ce4e6b821cd9284ef4c4268ae:eyJzZWxlY3RlZExpbmVDb3VudCI6OTMsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjAzMDUzNzcyNjU4MGYxZDA2OWQxMjUwNTI3MmRhODg5OWM0M2RiYmZiZmM4NmZiY2IzOGJhMmNhZWE0OWU4NDIiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjgwMmNlYTE4N2JjY2NlOWZhNDkyM2M4NzAzZDliYjk3ZWZhZTZkNTk2YTc4OTQ2YTlhZWMzMzc4MDM3YjU2MWMiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6Ijk5NzM0ZmViMmI1ZGU2NjEwOTU2ZjI3N2RjMzAyMWJjMTZhNDVhYzBhMjc2MWM1Y2YwOGM3M2NiZjA0NTU0MWYifQ"
+          "resource": "repo://src/whatsapp.rs#L789-L836",
+          "version": "repo-lines-v1:sha256:d9d9b03372176b8ae315c56ec351773837c3d40ddbc09cb3033511ed440dd9f6:eyJzZWxlY3RlZExpbmVDb3VudCI6NDgsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjAzMDUzNzcyNjU4MGYxZDA2OWQxMjUwNTI3MmRhODg5OWM0M2RiYmZiZmM4NmZiY2IzOGJhMmNhZWE0OWU4NDIiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjgwMmNlYTE4N2JjY2NlOWZhNDkyM2M4NzAzZDliYjk3ZWZhZTZkNTk2YTc4OTQ2YTlhZWMzMzc4MDM3YjU2MWMiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjVmMTVmNmFmMDRiYTEzNmRmMTIyMDNiYWY0OWQ1MzZiNDE3OGUzYWYzMWRiMDcxMThhMDhhMzUxOWRjZWFhYzYifQ"
         },
         {
-          "resource": "repo://src/whatsapp.rs#L879-L920",
-          "version": "repo-lines-v1:sha256:8c2114ae474695a3523c19bf0ceac407e82c82d1882bd2d0511a38cab8eeaaa0:eyJzZWxlY3RlZExpbmVDb3VudCI6NDIsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImY1N2FhZjc1ZDg1ZGRhNjUwNDRiYzA5YmE2YjgxN2ExODFlMDNkZmIxNWY5MzIwYWIzNGZmYjkzOTc2MzljZGEiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjliYjA3YjBjZTBkNjhiMjljNDMxZWQyZjUyODcxN2Y0MDY3ZTA2NzZlNWViZGIzNzQ2N2Y5ZWZjNTM1YmI4YTMiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImE4ZDQ3NzIyZjlhZjNiZmQ2OGYwMzRmYTIzMTgwNGJjMTViOGUxNGIwMzY3Y2FmYmIzZDc2YjI5YjU3ZDQ2YTQifQ"
+          "resource": "repo://src/whatsapp.rs#L939-L984",
+          "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#L1011-L1069",
+          "resource": "repo://src/whatsapp.rs#L1071-L1129",
           "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#L200-L210",
+          "resource": "repo://src/whatsapp.rs#L233-L243",
           "version": "repo-lines-v1:sha256:8ed098e34276e2c4b9ff74e786b647375c2d873d70640b609b0ae4bca9505394:eyJzZWxlY3RlZExpbmVDb3VudCI6MTEsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjEwYjNlNTU4ZDczYzgzNWI1NWVkY2U4NWUxNTMzYjM1ODczM2U1OWU4ZGNmYTJmZTM3N2FmNDBjZTcyZjY4OTUiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjE5ZTc2MzU1Y2YxMTk5NDgwYzNmOTk4MzZiMTQ4NmM4NDFkODkxZmFhZDkwNGM2MDhlMDkzMDIyZDM3NTdhZWUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImE3M2Y4MjFiY2U5NjYxNDdhMDEyMmRjYjVhZGExZmVjZjgxMWIyZmNiNjIwZDUwZGFiY2U2MTg0OTUzNjVhNWUifQ"
         },
         {
-          "resource": "repo://src/whatsapp.rs#L239-L277",
+          "resource": "repo://src/whatsapp.rs#L272-L310",
           "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#L1000-L1002",
+          "resource": "repo://src/whatsapp.rs#L1060-L1062",
           "version": "repo-lines-v1:sha256:2dff1120b156dde5cd502a03211b000f533177e7214711f8a8a081955c3663ac:eyJzZWxlY3RlZExpbmVDb3VudCI6MywiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiOWY1MGVlZDdhYWNmZGE2Mjk4Mzg4Yzg2MmI1ZjIyNDNlNzIyYWMxNWEyY2FkNzAwNWUyOWQ1NzIxMmE1YThiOSIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDEyY2EzNDVjY2Y3NWJmOWMwODA2YmNlNjk1YmU4ZGU4MDhiNzk5ODQyNTFhN2E1NGQyMDJjZjYxMDFkZDQ1MSIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiNGNjZjc5MzkxNTY5MjhjYzk4Y2ViZDIwYjJlYTA2OWM0ZWNlZGJlZWE5Nzk2MjBiN2IzNWUzYTY5Mjg3YWE4MyIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiM2I4MWNkZTZlMzVlZWI1MjRlMzM5ZDFlMjM1MzE5Y2Y1NTI5YjFhNTBhMmE2MDUyZmUzNWI1ZDQ4NWRkOWUxMyJ9"
         },
         {
-          "resource": "repo://src/whatsapp.rs#L279-L295",
+          "resource": "repo://src/whatsapp.rs#L332-L348",
           "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#L1205-L1208",
+          "resource": "repo://src/whatsapp.rs#L1291-L1294",
           "version": "repo-lines-v1:sha256:af6676593e9e90c7ac6a4b2b64625d687ad77fa0265e7f46bb6b8dbef4446d86:eyJzZWxlY3RlZExpbmVDb3VudCI6NCwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiMzFiZTM3ZmI4YTA3YjI0YzY1ZmVjNGQ3MGI2NWNhNDAzNTU5ODQwOGE3NDhhYjg2YWY4YTY4YWJlMGJjMTNhYSIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDg5M2Y0MGUzNzgxZWI4OTZjODJkMDQ2YTA4MDY3YTExZTM1Y2RmMmYxZGZhZjI2ZmVhOTBiOGRiNjY3NjQxMyIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiYTAwZGIwNDBmMjkwOWJmOTc4ZmYwYTk1NmE1NWU4M2M2NTM0MThkZTZiYjY4ZjgwNDFkYTk3NmY4ZDNkMjI3MCIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiYjQ1MTJjNzhmN2UyZTJiODdhNDZhYThlZWUxYTg3YjIwZjZjMWE0Y2MwZmNkNDQ4ZDhmZjJmMWJlOGYxMGQ4YSJ9"
         },
         {
-          "resource": "repo://src/whatsapp.rs#L612-L621",
+          "resource": "repo://src/whatsapp.rs#L665-L674",
           "version": "repo-lines-v1:sha256:55006adc8131456b9811de9f46520c07874aab146e6d5deb0b359a138bc063e8:eyJzZWxlY3RlZExpbmVDb3VudCI6MTAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjljNjAwZjllYTA2YzVjZTAxYjEzODExOGM5YjdhZDVjNTUxYzBhMmNjZDkxZTJmNWQxMWI4OTNiMWU5YzdjN2IiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjU1MjJjNDJjZDE5YzZkMDNkODA4YzQ1MjcyMDU1MjgyNWI3ZWExYzViNDkxYjIxODUxMWNhYzc0MTVjZjlkOWIiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjlhNDQ2MGQwYzUyNjkwYzM3YTQyOTI4MTc3NjkzNmQ4ZDA3YTJhYjg3MDEyYTRkMDY0MWNiMTQ4ZjkzOThjOGUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjQ3YTNlYmViMDcyOTcwMjY0MTFmZWVhZWViOTNkODBhMDZjOGQzNmE0OWE0NzgyNjhiZDFjNjJkMWRkOTk2YTkifQ"
         },
         {
-          "resource": "repo://src/whatsapp.rs#L978-L985",
+          "resource": "repo://src/whatsapp.rs#L1038-L1045",
           "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#L160-L180",
+          "resource": "repo://src/whatsapp.rs#L193-L213",
           "version": "repo-lines-v1:sha256:0f8cd1675d28ef481cfcb8fdf5554a3529190b4edb526cd8f974d32643f56909:eyJzZWxlY3RlZExpbmVDb3VudCI6MjEsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjdhYTg4OWY5NzI4NTVlZGJiMWEyNzExOGE1NDY1ZTc2MjhlM2NjZWYzMDg2OTU5YThjNGZlOTViMDJmNDM1ZWIiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjVkYjk4OGU0ZDZiYzFkOGViNDU4NmY1NWY3MDBlNGVjY2EzYjhkYzliZDM2YzJjMjgyNDQ2MDhkN2M1NWEwMjAiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjViODNiNGUwM2Y4ZmFkZmM1OWI5NGZjZjhmNGUyYTI2ZjNlNTdmMjI0NzczODU0NjVmNjAzNjBhODQwNDdiNTMifQ"
         },
         {
-          "resource": "repo://src/whatsapp.rs#L200-L219",
+          "resource": "repo://src/whatsapp.rs#L233-L252",
           "version": "repo-lines-v1:sha256:5de6b5901fa8a014bd2fffe970ba820db963f438b9ec7bf051f62c957198ecd2:eyJzZWxlY3RlZExpbmVDb3VudCI6MjAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjEwYjNlNTU4ZDczYzgzNWI1NWVkY2U4NWUxNTMzYjM1ODczM2U1OWU4ZGNmYTJmZTM3N2FmNDBjZTcyZjY4OTUiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjE5ZTc2MzU1Y2YxMTk5NDgwYzNmOTk4MzZiMTQ4NmM4NDFkODkxZmFhZDkwNGM2MDhlMDkzMDIyZDM3NTdhZWUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjQ4Njc5ODk5ZGJiYzc0NzgxOGMzNWI3ZTNiMjY2ZWYwNTQyMWJmNTJkYzBmNmU4OWI4N2VhZjcyZjAzMzQ0NGEifQ"
         },
         {
-          "resource": "repo://src/whatsapp.rs#L85-L86",
+          "resource": "repo://src/whatsapp.rs#L88-L89",
           "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#L182-L198",
+          "resource": "repo://src/whatsapp.rs#L215-L231",
           "version": "repo-lines-v1:sha256:bc94058fad104fda8ef2d9889e6539ac65f19dd28226e1edec14d240905e4aa2:eyJzZWxlY3RlZExpbmVDb3VudCI6MTcsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjYwMzUyMWI1YmRmYWViNzk2MzY5ZTExMzEwODIwMjIwZTNhMTI1MGUwY2E5NzIyMGEzYTI1OWFkMjQxNTcwZGIiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6Ijg3NzllYzNlN2JjNmZkMzdlNDYxMTdhODY2ZDA2YWIzZTVlNGVlYmE0ODJjYmViYzkzZjhjZGRjZjYyOTM5ZTAiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImM4YzBjZDA2MzU4OWY4OWUzMTY0ZTQ3MjczYzg2ZDQzYjBjNzkwZTc2OThhMDFmNTU1NGQ1ZTUxODkzMmRiYjYifQ"
         },
         {
-          "resource": "repo://src/whatsapp.rs#L221-L237",
+          "resource": "repo://src/whatsapp.rs#L254-L270",
           "version": "repo-lines-v1:sha256:63e137b22805f72a6e35c74bf1f9296c30d5dd1c696101d7b7db2f3dedb3b664:eyJzZWxlY3RlZExpbmVDb3VudCI6MTcsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjY4YjU3MmE2ZGZjZGNjZDc1YmE0ZDBkZDZiYmEzNjUwY2UwNDFkZDkxYzRjNjE1YTBmMDJhNDllMjNlNTA2OWQiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImIyNTdmMTI1NGQ0NjJmZjgyYTFmYTFmZjYyNzFlNzIzNTM1NTFkMDZlNjMxMDQ2OWYxMGZmZWUxZTY2ZmVlMjMiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6Ijk5OTU0Y2ZjYmI3ZmVlMDA0ZTg0M2I4ZDdmZjAxNTA3ZTlmOGU5YTYzNWI1MjZlOWQ1ODRmNzY1MDA4YjRiZDYifQ"
         }
       ]
@@ -201,18 +201,32 @@
       "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#L314-L367",
+          "resource": "repo://src/whatsapp.rs#L367-L420",
           "version": "repo-lines-v1:sha256:dae8e9fd58393c76159603a1143da95f40e9d77d62eaa8e9531737a4a58b8edb:eyJzZWxlY3RlZExpbmVDb3VudCI6NTQsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjgyNTI5M2Y3MjNjNGMzMjZmNDJlMDFmMzc1Nzk1ZTJjMjE4NzQxNGEyNjI5NDEzOTEwODc3Mjc3MzdlMWMzMDEiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjYwNjNjNGQxYWY4Y2Q4OWJiMGJkZDc2NTdmZmNiZjY4MDZhNjEwZDc5ZDZjOGQ0YzMwN2MyNDFkZjM5MDA1MjIiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjI1MjJlMWI3MWMwMDczNGUwMWUwYTJhN2ZjYjU1ODdjYWEwN2RkYzRlZjRjMTViYTEyNGFhN2M1ZWRlZTVjYzUifQ"
         },
         {
-          "resource": "repo://src/whatsapp.rs#L88-L102",
+          "resource": "repo://src/whatsapp.rs#L91-L105",
           "version": "repo-lines-v1:sha256:ee68380c7344a72f8bb87cef4651b5340e1832b4eaed61c614d88b8964dc7950:eyJzZWxlY3RlZExpbmVDb3VudCI6MTUsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjE0YmIxNWU3MmU0YTFmMzBmNDU2OThjMDA4MDc2ODZjYWVjZTEzNTI1Yzk3YmU2ZTc5ZWZkYTlmMDQyMGNkZmMiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjZkMDcxMDczNGRhYWVkNTFlMTIwNmE1ZDU0NzMxMGQwOTg2Nzg2MTAyYzA3YzM0NjRiYzEyMzY5ODJkZjFjY2MiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImFlMDAzODBjNDQ0YjdjNmE5YTgyZTAxMDJjY2IwNzI0MWVlYjhhNTU1NGE2MTkxOTFhNzk2MDJhNGFhMDc4MGQiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjIxOTJmNjU0MjE1OGExOGY0MDRlMjg1ZmI5NGM2N2VjYmFmMjg0NzUxOGM5ZjQ3Y2IzZDA3MjVmOWYwNTk0NTUifQ"
         }
       ]
+    },
+    {
+      "id": "claim_fbc61a059a984bc88df8d3ee81a0a3d2",
+      "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",
+          "version": "repo-lines-v1:sha256:45a35da8df6fc27760a95dca63d9e04961dc44f29c71fcdb85fef2c62a9e848b:eyJzZWxlY3RlZExpbmVDb3VudCI6MTMsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjE3OGM5MzE3NmI2YjkyM2QwMDkzOGM3N2FkMGMzNmEzOTg2ZjJlOTJjYzY2Mjc3ODcyZjFlN2QxNGZiNDk5YjciLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjVhOTA2ZmY3MWMzYTY2N2Y1MTE0ODk0MWJiNDA3MzgwNjFkMTBhNTA2NzI2OGM4ZTBhNmRjYjJjMmJkZjU0NmQiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjAyYzFjZTZmZmI5ZGEyOTBlMjgyNWRlNjVmNTllYWExNjliZDJiMDBlNDBlZjA0NjAzYWRkODFjZjU5MzZhNjIifQ"
+        },
+        {
+          "resource": "repo://src/whatsapp.rs#L317-L330",
+          "version": "repo-lines-v1:sha256:948cec998055e53be3e45a29232f60f6f315896f8a9f3b0ac6617ef65852536c:eyJzZWxlY3RlZExpbmVDb3VudCI6MTQsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImE4MjAwYTdhNDMzMTFkNzkxN2NkYWQ4NjIyMjhjYTg3ZDIxYjRhZmMyMjJmMGZjYWM3MjJmODU3YWEwNzAwNWYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImFkNzI0MjA4NTQ4MTRiMWU2YzUwNzc4ZDU3YjMxNTIzMWYzYTBmMDRkMzFkOGVjMDNiMjc5ZWU2YTdmOGE1OWIiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjMwZGZlODgwMmNkNjM3Y2NmMjZjNWIwYjZhOGJhYmNhOWRhYzI5NTNlOTRmYjA4YzFjNzA1NGVkOWUyNmYwMmUifQ"
+        }
+      ]
     }
   ],
   "verification": {
     "by": "openwiki/0.7.2",
-    "at": "2026-10-23T21:26:24.156Z"
+    "at": "2026-10-24T16:39:50.206Z"
   }
 }

+ 40 - 40
openwiki/.claims/operations/configuration-and-data.json

@@ -1,18 +1,18 @@
 {
   "schemaVersion": 1,
-  "pageVersion": "sha256:4424aa91ddf23d979235806fa52b23f096e57c324e97fc229b803823db189ab9",
+  "pageVersion": "sha256:8c0a6de343db6a1114f9ba81a51b8cb1b5f189feadb28cbed319933c98ef98d2",
   "claims": [
     {
       "id": "claim_0519d797500c4fba86d6c64ac1e60df4",
       "statement": "Configuration is read only from files in the data directory (config.toml, SOUL.md and VISION.md) and no environment variables are used.",
       "evidence": [
         {
-          "resource": "repo://src/config.rs#L1-L25",
-          "version": "repo-lines-v1:sha256:3e4cf760dbcc82e6ee4f5b88a2c765c266de46001249e1c954503d63a7351276:eyJzZWxlY3RlZExpbmVDb3VudCI6MjUsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImI0MGE0NTBjZTI3ZmY3MjYwYzM4M2U4NDk4MjhjNzY1NTVlZTk1ZTllM2JmMTJmOWYxY2IxY2Q2OGZlZGFkMDgiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6ImE0Y2JhZGQ0ZTk3ZjA4YzA5MmE3ZmRkODNiNDM4NjUwNjFlNDBmZDlhMjExZjZlZDhkYjQzYTkxNTRlMTI4MTciLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjowLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImUzYjBjNDQyOThmYzFjMTQ5YWZiZjRjODk5NmZiOTI0MjdhZTQxZTQ2NDliOTM0Y2E0OTU5OTFiNzg1MmI4NTUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjcxMzE1MjZlMmQ3ZWFkMDgwNmJkNzJlYjRjMGJlMjE1NjI4ZWZiMGMzMjc3OTM0NjVkYjM1NmQ2NTQ4NDAzN2YifQ"
+          "resource": "repo://src/config.rs#L1-L29",
+          "version": "repo-lines-v1:sha256:96b953069f6ae5437bfecc28035a013002cc36e1bf510fca44227d9fb53e8e3d:eyJzZWxlY3RlZExpbmVDb3VudCI6MjksImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImI0MGE0NTBjZTI3ZmY3MjYwYzM4M2U4NDk4MjhjNzY1NTVlZTk1ZTllM2JmMTJmOWYxY2IxY2Q2OGZlZGFkMDgiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6ImE0Y2JhZGQ0ZTk3ZjA4YzA5MmE3ZmRkODNiNDM4NjUwNjFlNDBmZDlhMjExZjZlZDhkYjQzYTkxNTRlMTI4MTciLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjowLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImUzYjBjNDQyOThmYzFjMTQ5YWZiZjRjODk5NmZiOTI0MjdhZTQxZTQ2NDliOTM0Y2E0OTU5OTFiNzg1MmI4NTUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjcxMzE1MjZlMmQ3ZWFkMDgwNmJkNzJlYjRjMGJlMjE1NjI4ZWZiMGMzMjc3OTM0NjVkYjM1NmQ2NTQ4NDAzN2YifQ"
         },
         {
-          "resource": "repo://src/config.rs#L265-L281",
-          "version": "repo-lines-v1:sha256:4ade9876bd8e1653384dcebde47d413e1aab36995b53055ab5441d82e8b28290:eyJzZWxlY3RlZExpbmVDb3VudCI6MTcsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImQ4MDc5NGE2MDA2MzNhZTY5ZGFlMWYzOThjYTk3ZTc5Yzg3NDc5ZDFjNjE3MDg0ZTRkMTgyODZmMTlkODFjMTUiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjgzZDI4NDFkZmUxZTBkZDIxMmQwYzE1ZDAxZTVhYzkyMTBmNTgwMGI4YmRmZTk5M2FkZDAwYmQ5NGYwMTRkNmMiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImU2YzkxMzJjZTZhZmY4ZmJjMmFmMzRiY2JmYWE1ODVhN2RmMWUxODViYjc5OTFlY2I5YzgxYTUxODg3NWUyOGUifQ"
+          "resource": "repo://src/config.rs#L325-L335",
+          "version": "repo-lines-v1:sha256:a5c79f041835554e8c3f17508bdbb7041c60c8f9877218c2f82630a36623ef43:eyJzZWxlY3RlZExpbmVDb3VudCI6MTEsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjMyNTQ1NmMwMjcxYTU4NGY0NTQ3Mzc4YjQ2ZTVjYTNlZWNhOTkzNzJlM2YxY2Y3OWIyZmJhNzU5MWQ1OGUxNWMiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjgxMDhhNGNmYjk5YTAxN2FkZDFkMTVhYTI1MmNmOTQ3ZmJhODdhZjk5YzM4NzFmOWRkYmNkYzBmMzYyN2I0ODIiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImU2YzkxMzJjZTZhZmY4ZmJjMmFmMzRiY2JmYWE1ODVhN2RmMWUxODViYjc5OTFlY2I5YzgxYTUxODg3NWUyOGUifQ"
         }
       ]
     },
@@ -21,11 +21,11 @@
       "statement": "The data directory is the platform config directory joined with wh and is created at startup by ensure_data_dir.",
       "evidence": [
         {
-          "resource": "repo://src/config.rs#L164-L168",
+          "resource": "repo://src/config.rs#L206-L210",
           "version": "repo-lines-v1:sha256:dd4141940976146e96656f2de294f92e1275a16efba97aa2c7318676f34bdfa4:eyJzZWxlY3RlZExpbmVDb3VudCI6NSwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiMzVhMDYyMDkxMmIzMTUwZDZlYmQwNDQyOTUxZTdjNWEwODlmODMwMzYzNmJjODZiNmQxOWRmN2U3YTFmMWEzZCIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDEyY2EzNDVjY2Y3NWJmOWMwODA2YmNlNjk1YmU4ZGU4MDhiNzk5ODQyNTFhN2E1NGQyMDJjZjYxMDFkZDQ1MSIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiNGU1ZGI4YzkwMjczZTAyOTc3MTY3ZWMwNDM0N2M0MWQ5MmE1MTE4NDMxMTA0M2UyMTU2YmM4MGFiYjkwMWQ0NyIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiMDY4YzgxNzM5NzQwNTE2YzI4OWQ4ZWRlZWM0N2IwMjcxMGFmOWVmNWM4MjE4M2NhNWQwMTc4MWVhMjI1YjU1MCJ9"
         },
         {
-          "resource": "repo://src/config.rs#L312-L315",
+          "resource": "repo://src/config.rs#L372-L375",
           "version": "repo-lines-v1:sha256:70ea48c62bb6583f73eaa3d204fb92456b268569dccd85ba7c47f85f0175339f:eyJzZWxlY3RlZExpbmVDb3VudCI6NCwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiMDE1Zjk2ZjM2OWY4YjE1NjExZWVjMDljZTBjOThiYjNhZTNhM2U5Y2U2MDEzZGNmMGE3OWY4ZTM1MjVmMjMxNyIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDg5M2Y0MGUzNzgxZWI4OTZjODJkMDQ2YTA4MDY3YTExZTM1Y2RmMmYxZGZhZjI2ZmVhOTBiOGRiNjY3NjQxMyIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiM2UxNTlhZDA5YmYzMzllMTM2ZmY0NzVjMjc4NDNkMTYyY2JiMTI2ZGMxMmFiMDM3MjE3M2M2ODY4ZGM5NzUwMyIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiMTZjMzk0MWE5YjYxMDI1Yjg1ZWU5ZWEzNzYzOTUyYTViMTM5NmNkMmY2ZjczMTkyZGQ3NDYwNDc5OTlhYjY0MSJ9"
         }
       ]
@@ -35,26 +35,26 @@
       "statement": "session.db, history.db and the optional ai_requests.log live in the data directory next to config.toml.",
       "evidence": [
         {
-          "resource": "repo://src/config.rs#L325-L347",
+          "resource": "repo://src/config.rs#L385-L407",
           "version": "repo-lines-v1:sha256:f895d312efc12c901e18e3c47999e82cde54de595ed326f94dc9c80be206fd75:eyJzZWxlY3RlZExpbmVDb3VudCI6MjMsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImIxNjY4MjQ0NGNiOTU1NTQxNjJjOGIzNjYxMjg0YTJhNGMzMDIwZmQxODU3OTYxMDU2ZGE1MjMxM2Y2YjgzYjkiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjIxNTA5ZGYyZDNjMWJjODQxODQ2NDlhOTg3NzAwZTU4ZGVmNTJkZGQ1YzBkZTVlZWIwZjY0YTc1YmQ5MzBlYzEiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImE1MjE2MDRiMzVmYzQwYWYwMGNiYTRlZWEyZmQ1ZTJjM2FmMTg2M2IzOTExODVkN2JmNGJiMDllNzJkMGM4YjMifQ"
         },
         {
-          "resource": "repo://src/config.rs#L45-L54",
+          "resource": "repo://src/config.rs#L55-L64",
           "version": "repo-lines-v1:sha256:6624d58d13d3982b8829e7b759fc7c36a643278f2d243526a887428e34a0d469:eyJzZWxlY3RlZExpbmVDb3VudCI6MTAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImU1MDllZGUxYTUxZmE5MGM4ZmQ0YTNkODdmYjJhMGU5OTMwNzhkMjc0MzU1NGFjMGM4MmIzMTdmNzQ0OTY5ZmYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjFmNzZjMzcwNTRkNWY0YzFhNWZhZDE4YzFlYjI0NTNmODIxNTI0NDQ2M2ZkNjA4MzIwZDEzZDYxNmFlYmVhNmQiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjMwY2VjY2EzYjg3NDgwNDU3ZGVmZTlkOWVkY2I1N2VmMDdlN2ZlZTE2MTljZjdmYWEyNzM0Y2Y2M2Q5MzIxYjEiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjQ1N2RlMDJkYWE2M2RmYzAxZmQxNjkyNDc1ODY2ODA3OGM5NmJlMGM1ODkyZjQ4M2UzMDdmNjAyYTU0MDRkMzQifQ"
         }
       ]
     },
     {
       "id": "claim_4a0091b8b0c944f9916463712878acbe",
-      "statement": "Keys openai_api_key, openai_base_url, openai_model, openai_vision_model, ai_log, reply_delay_seconds, catch_up_seconds, ai_retries and web_port map to Config with defaults (base URL, model, ai_log false, reply delay 30 seconds, catch-up 7200 seconds, 2 retries, web panel disabled); the system prompt is not taken from config.toml.",
+      "statement": "Keys openai_api_key, openai_base_url, openai_model, openai_vision_model, ai_log, reply_delay_seconds, catch_up_seconds, ai_retries, web_port, transcribe_key, transcribe_url, transcribe_model and transcribe_language map to Config with defaults (base URL, model, ai_log false, reply delay 30 seconds, catch-up 7200 seconds, 2 retries, web panel disabled, transcription disabled without key, Groq URL, whisper-large-v3-turbo, auto language); the system prompt is not taken from config.toml.",
       "evidence": [
         {
-          "resource": "repo://src/config.rs#L283-L306",
-          "version": "repo-lines-v1:sha256:337be3d271ffd1034a8dead0b8ab67544c72bc1e22f24bb99e564ca5bb9b1ffb:eyJzZWxlY3RlZExpbmVDb3VudCI6MjQsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6Ijc2NWZhNGU5MTRkN2UzOWE2MGJhZTc1M2NhMjE2YTg3NGE3NmZmZWY4NTc4OTY5NTZkNjg0MDM4YTY1MWJjZDAiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImViOWEwOGJjMjlmNmViZWJlOWE1ZjAxOTAzZDk4NDVkYmNiNTg0MmE0NTVmMDdkNWY0MDAyZGFjMzJhNWM2MDUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6Ijg4ZWYzM2E3NjQ3YTM0NDVkNzE1NDU4YmE2Yzc0OGY2NmIyYWZiNzYxYzEyODc4NWU3MzRjZjIyOThjOTdkMzUifQ"
+          "resource": "repo://src/config.rs#L342-L366",
+          "version": "repo-lines-v1:sha256:6d7db37ed42406b64796f6fe895dff6eda6035abf11de0730be579786f6118ec:eyJzZWxlY3RlZExpbmVDb3VudCI6MjUsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImFjZWRhOTg0MDJkODQxZjI4MThkMTg2OTM4NWQ0MzA2OGVmNjkyODg0ZmRiOWMzMzFjOTc4NTY3Y2EwOGQ0NzkiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImI2NmNmNTgzOWE3ZWIyYzcxOTUwYWEzYzRkMTU2YzI1Y2U0NTBmODZlZWI0MDU4ZmZkN2FjNzU3YTYxMDU4ZTAiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6Ijg4ZWYzM2E3NjQ3YTM0NDVkNzE1NDU4YmE2Yzc0OGY2NmIyYWZiNzYxYzEyODc4NWU3MzRjZjIyOThjOTdkMzUifQ"
         },
         {
-          "resource": "repo://src/config.rs#L34-L43",
-          "version": "repo-lines-v1:sha256:db0845d61fe67c0b00b5070ab68f50571dffeed86fa0119d9004244edcab7733:eyJzZWxlY3RlZExpbmVDb3VudCI6MTAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjdkNWE4ZjI1MGMwMjJlOTMwNzUxZGFlMTg0YjBhZGMxNTUwYWRkNzVlNGJiOTUyMDBiNTE0NmY0NzQ5MjdlODMiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6ImQ1MDFkZTlkMjcyZmZkODVmYTk1OWMxN2JiMTdjYWZkMmJkYjM1YTgyMGQzNDA1NGRkOWQ4NThkNzUyMDMyODIiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6Ijc2YTNkY2UwNTMyZWI3NmI2M2EyNmViYzdlZmNkNWE3YTViYzFjMTFhOWFiZGRhM2Y3ZjJmNzhkODc1Y2Y3M2YiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjdlODA1ZjRiMzVlMzg3NmExMjg5OTE1MjgyYWRhNWNkMzNhNzdmNTMyYmYyOWMxOGM4OWRhMjJiMTRjZjUwYmQifQ"
+          "resource": "repo://src/config.rs#L38-L53",
+          "version": "repo-lines-v1:sha256:c61beab344a88ae0cbfc061ba30c9e7a1f5423801b317d8120f0025b643606ad:eyJzZWxlY3RlZExpbmVDb3VudCI6MTYsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjdkNWE4ZjI1MGMwMjJlOTMwNzUxZGFlMTg0YjBhZGMxNTUwYWRkNzVlNGJiOTUyMDBiNTE0NmY0NzQ5MjdlODMiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6ImM2MmRkNzUzZTU1MmY5NjNlNzA3NGY1ZGI4YWMxY2YxNGZkMzgwNjFlM2EzZDNlNWY2NjdlNTBiZWIzNTBhZDQiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6Ijc2YTNkY2UwNTMyZWI3NmI2M2EyNmViYzdlZmNkNWE3YTViYzFjMTFhOWFiZGRhM2Y3ZjJmNzhkODc1Y2Y3M2YiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjdlODA1ZjRiMzVlMzg3NmExMjg5OTE1MjgyYWRhNWNkMzNhNzdmNTMyYmYyOWMxOGM4OWRhMjJiMTRjZjUwYmQifQ"
         }
       ]
     },
@@ -63,7 +63,7 @@
       "statement": "A missing config file yields defaults, while unreadable or invalid files produce errors naming the path without echoing content; a BOM is tolerated.",
       "evidence": [
         {
-          "resource": "repo://src/config.rs#L185-L210",
+          "resource": "repo://src/config.rs#L227-L252",
           "version": "repo-lines-v1:sha256:98536507e8260febe788fbadf4507f155d98cae147436b79fb8b0f633fd9ecdf:eyJzZWxlY3RlZExpbmVDb3VudCI6MjYsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImE2NTEzYThiZDE0NzdlYTVhZTdhMWU5N2IzMjFhZWUxNmY1YjcwZmEyZDA3ZTMzMTAyYmNiY2M3MWE1N2JiMTEiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjRiNzAzOGM3MGY5ZDBmYzEzOTYwY2YyOTRkOTdhODRlOTUwMmNhZmI2Yzk1NjM2ODhiZjc4ZGQyNGY0NWE2YWUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjJkM2E4MDAwMjZjNmY2MDFiYmM4MDdlYWI0YmI1Nzk1ZTY4MjEzNGYxMWU3MTRmNzRkMWVlMzQzOTUxNjE4NDYifQ"
         }
       ]
@@ -73,18 +73,18 @@
       "statement": "An openai_base_url made only of slashes is rejected.",
       "evidence": [
         {
-          "resource": "repo://src/config.rs#L245-L252",
+          "resource": "repo://src/config.rs#L287-L294",
           "version": "repo-lines-v1:sha256:416d7587d25100ea2db4d28ac60e8ff2b5b07566b3bbad27544a561576d2d6d4:eyJzZWxlY3RlZExpbmVDb3VudCI6OCwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiZjg0NWUxMjk4NjExOTI3YTI5YjJhNjA0ZmZiMTBhOTcwMjc5ZjIwYjZhMjBiYTA4MGJiNDFmYWU1NGJkYmFmNiIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDEyY2EzNDVjY2Y3NWJmOWMwODA2YmNlNjk1YmU4ZGU4MDhiNzk5ODQyNTFhN2E1NGQyMDJjZjYxMDFkZDQ1MSIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiZmM2ZTJmOGQ3OGU4N2ViYmYxZTQwNWUyMmIxODU5MGViZjMzNzQ3OTY5MGVlNDVlZGIxNjMxZGVlYjJjZDQ0NiIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiZTM0ODM0OGE5MDUyNDI1NDNlZmRlZTgyZjg3NWRlMTk1NzU5Y2Q0MDkyODQ3ZDBlZWIyNzdmYWQ4M2Q3NzUxZiJ9"
         }
       ]
     },
     {
       "id": "claim_5adc6f9c49dd4401a99e814a572bc8fa",
-      "statement": "Config Debug masks the API key and FileConfig has no Debug implementation.",
+      "statement": "Config Debug masks both API keys (openai_api_key and transcribe_key) and FileConfig has no Debug implementation.",
       "evidence": [
         {
-          "resource": "repo://src/config.rs#L107-L158",
-          "version": "repo-lines-v1:sha256:9241fafa52c556df183667779add419c51d39295a9403897555c062e1ff441ea:eyJzZWxlY3RlZExpbmVDb3VudCI6NTIsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImQwOGE4NDgyODgzYmU5ZjVmMDIxNDY2ZTgyOGE2MjgwZTI5YTNiMjI0ZTQ4MTY4ZjZjMWQxMDY0MzI3YjlhMjYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjY0Zjc0NTFlMTZhYzBlYjU5ZWE3OGI4Nzk0OTdhMjVkNzM4NzUxYjYwZjE1ZmYyYjQ3ODBhM2RmMGYwYTIyNTIiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImFiNzFkMjFhZWI1ZGU3M2Q1YmI3YmU2NDg0YTMzODliNjY1YjdjMDc1YmM4MjlmMDIyYWE0ZDc1MjM2OTNhMTUifQ"
+          "resource": "repo://src/config.rs#L129-L200",
+          "version": "repo-lines-v1:sha256:7b5ec32a3a8c6ff16ae1fa5f4768631ae65531f8e7af7570d4d8398a341fec3d:eyJzZWxlY3RlZExpbmVDb3VudCI6NzIsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImQwOGE4NDgyODgzYmU5ZjVmMDIxNDY2ZTgyOGE2MjgwZTI5YTNiMjI0ZTQ4MTY4ZjZjMWQxMDY0MzI3YjlhMjYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjY0Zjc0NTFlMTZhYzBlYjU5ZWE3OGI4Nzk0OTdhMjVkNzM4NzUxYjYwZjE1ZmYyYjQ3ODBhM2RmMGYwYTIyNTIiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImFiNzFkMjFhZWI1ZGU3M2Q1YmI3YmU2NDg0YTMzODliNjY1YjdjMDc1YmM4MjlmMDIyYWE0ZDc1MjM2OTNhMTUifQ"
         }
       ]
     },
@@ -103,12 +103,12 @@
       "statement": "reply_delay_seconds, catch_up_seconds and ai_retries are Option<u32> in the file so negative values are rejected, and 0 means answering at once, disabling the catch-up or disabling retrying.",
       "evidence": [
         {
-          "resource": "repo://src/config.rs#L107-L130",
-          "version": "repo-lines-v1:sha256:88a2926a51f37f52593b1be029cb70e800f905c7073db0aa97177ad6d303faaf:eyJzZWxlY3RlZExpbmVDb3VudCI6MjQsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImQwOGE4NDgyODgzYmU5ZjVmMDIxNDY2ZTgyOGE2MjgwZTI5YTNiMjI0ZTQ4MTY4ZjZjMWQxMDY0MzI3YjlhMjYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjY0Zjc0NTFlMTZhYzBlYjU5ZWE3OGI4Nzk0OTdhMjVkNzM4NzUxYjYwZjE1ZmYyYjQ3ODBhM2RmMGYwYTIyNTIiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjNhN2ZlNzVmMzU5ZTMyZTFkYmE4YWFjMDY1ZmMzYWFhMjY4YzcxZDc0NjUyYjU2OWNkNTJhYjEzYmZlYmMwN2UifQ"
+          "resource": "repo://src/config.rs#L143-L148",
+          "version": "repo-lines-v1:sha256:b461aea6a358d4de7c2864e9e30a18dc2352db5f9e1f79b792181ce9b310f5cc:eyJzZWxlY3RlZExpbmVDb3VudCI6NiwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiYzJiZDQ5Y2FmZDYyNmM2ZjljYjljOWI1NDllZGUwZGQ3NDkyN2IyZjYwZWYwMmY4OWNlMDIyMGZmNzA3YjIwZCIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiYjBiOTRjODBmNjUxYzk0ZTBmZTBmZWE4ZDc2NGUyMWFiMjc3NWFhZTkwZTVkYzczN2NjYWVmMTA1NDhjYTIzMCIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiNjliZTNjZGRhOWEwYTNlYjkzNGI5OGIzN2E0MGJmYmNkNTVhYWQyNzUyNmRjOTM2NDYwNDczMzNjMmFiZjc4ZiIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiYjRmOGYzYzg1N2I3MjNlMDgyNWQ1MDA5MzQzOGI5ZmYyMjY2MzAxY2I1NzA3MTk4ZTc2NGZlODI0OTZhYjMwYSJ9"
         },
         {
-          "resource": "repo://src/config.rs#L7-L17",
-          "version": "repo-lines-v1:sha256:92e165d4ca3482a66abeebff4316ae732db9bd09e242fa332b4d3ab62e4d7488:eyJzZWxlY3RlZExpbmVDb3VudCI6MTEsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjdlMzA5ZTQ5ODMzYTEwNTQ2YTMxZDhlMDFjZGNkMWRjMGJhMWQ3YzA5Yzg5MGZiMDhlODhjMDFiY2MzZDI1YWIiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjAxNjUyNTRiZGUzY2I5ZjNkMjU1MDNkMGRlYzczZmZlOGY4MTcyMmUzNjc5Nzc4Y2NiMTRkNDA1M2MyMTJhYTQiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6Ijg3ODQxYjgxNDUwYmZlOTMzYzA0ZWYyMzMyN2ZmODg5N2I0Njc1Njg4ODU4YmRkNzg5NWM5ZTQxNzIwMGE2ODAiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6Ijg2MGZlMmRiYmY0ZjEzYjNkNDBjZDhmNjliMGQzY2Y5ZGM3ZjI4Y2U5N2FjZmZjNzFmODgzNWIxYmIyMGZhNmEifQ"
+          "resource": "repo://src/config.rs#L9-L17",
+          "version": "repo-lines-v1:sha256:845319fa0039763fc9db3cd6750ce50c646161fc39e33e071883122758fd0737:eyJzZWxlY3RlZExpbmVDb3VudCI6OSwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiMDMzYzQzOTM5MmM3MjQ2OTdjZjkwZjQxM2Y3MGYxNzIzNDIzYWYzMGMzYTFlZjFmMzJiYWQ1MjFjYmVlMGUzZCIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiMGU0ZDYwYWYxNWM0OTQyM2ZiMTA4YzE3YzY1NTFiNDNlOWZlMGJjYTczN2ZjZGFmNjYxYTNmYWU4NDQ1NTIyMSIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiNmJmODI5MjJlZDA3NzE0MDI0M2U4NjQ1NTkzZjY1MDRlNWJlYjYwMjgyMzQ5NGJjNDM4OTBiZWVhMjUwZDcyZiIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiMzdjZDhlNWJhY2VlYzEzYTM2ZmI4NjNhMTcxZjg2MDhjZjI4ZWM0M2EzZGU0NmIzZGZiMzdhYmZiZmUxOTE3NSJ9"
         }
       ]
     },
@@ -131,26 +131,26 @@
       "statement": "web_port is parsed as Option<u16>, so negative or above-65535 values fail TOML parsing, and 0 is rejected with an error naming the key; unset disables the web panel.",
       "evidence": [
         {
-          "resource": "repo://src/config.rs#L127-L129",
+          "resource": "repo://src/config.rs#L149-L151",
           "version": "repo-lines-v1:sha256:b4f8f3c857b723e0825d50093438b9ff2266301cb5707198e764fe82496ab30a:eyJzZWxlY3RlZExpbmVDb3VudCI6MywiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiNzEzN2YxMDljNWYyMDU1OGE3MzUwZDRlYjdkMmU2ZTllNDk1NzIwY2Y0YWQzZTk4NjZlY2E2NDAxNWU3MGJkYSIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiN2U0MTE5NDE4OWJhMWY1NzIyMjFiMjA3N2JmMDU4ZmExYTNkZTA3ODdkMzIzZWMyYWI0MGU3MmEyMTRiYmM3OCIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiMjgyZmY5MDM4ZGNhMmQwMGZjZjczNzgzOTQwNWI2NGI3NmVlODMyNzA4NTVlMDMyZDU5YTU4YzYyMTc4NDkwNSIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiOTY4MTE0ZGZhNDU1MWJiNmMwMDBmNGUyNTVjNjhiMDYyMmZjYmUyY2YyZTJiYzJmZThhODNmYTg2ZmY2OTk2MCJ9"
         },
         {
-          "resource": "repo://src/config.rs#L254-L263",
+          "resource": "repo://src/config.rs#L308-L317",
           "version": "repo-lines-v1:sha256:6d490312b71acd64e2e0d7d854caad2acb8ba41147fcb3bcf92195ba26fc850e:eyJzZWxlY3RlZExpbmVDb3VudCI6MTAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjM3NGJkYjgxZDEzY2MxOTQwZWE0Y2EzNTQ4NzZiODhiYmEyOWYxMmIzNTIzZTE1M2VlZDZiOGFlNWM4YjllYTciLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImEzMjJlODc4N2I1NGUwYzg5MmMxMzExNmFmOTM5ZjY5YTZkM2VjNzE2ZTMzMWY3N2JkN2E3OTFmYzAwMjFhOWYiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImUzNDgzNDhhOTA1MjQyNTQzZWZkZWU4MmY4NzVkZTE5NTc1OWNkNDA5Mjg0N2QwZWViMjc3ZmFkODNkNzc1MWYifQ"
         },
         {
-          "resource": "repo://src/config.rs#L512-L536",
+          "resource": "repo://src/config.rs#L621-L645",
           "version": "repo-lines-v1:sha256:37db7f6e1b478b12a699a96825e0e7d8506be31e81194c64ea6f58027479b43f:eyJzZWxlY3RlZExpbmVDb3VudCI6MjUsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImE3NmFiMWQ5ZjdkNjNlNzQ4YjE1ZWQ4NzQzYjFjOGE2MWQxZmE4ODNmYzQ5NjkwMDM1NzUxMDk3N2UzMmJkMjQiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6ImFjZjZmZDFkMjdkMzRmN2Q2N2RhMWQ5MDI2M2I0MzllZjk3MjNmOTRmYTY5YTJmZDI5NzZlYmE2ZDQ3MzAzNDAiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImEwMGRiMDQwZjI5MDliZjk3OGZmMGE5NTZhNTVlODNjNjUzNDE4ZGU2YmI2OGY4MDQxZGE5NzZmOGQzZDIyNzAiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6Ijg5M2QyZGYwZGQzNzE4YmQ2M2NiMGFkYjE3NjllNWMxOWJmYzA4YzY4ZDM5NjEwYjU5NGM3NDM0MTE4YTQwMGQifQ"
         }
       ]
     },
     {
       "id": "claim_919f934c898e4e68a57c6e267fd4238c",
-      "statement": "config.toml.global.template lists every supported key with its default value, leaves web_port commented out and points to SOUL.md for the system prompt.",
+      "statement": "config.toml.global.template lists every supported key with its default value, leaves web_port and the transcribe_* keys commented out and points to SOUL.md for the system prompt.",
       "evidence": [
         {
           "resource": "repo://config.toml.global.template",
-          "version": "repo-file-v1:sha256:0ecec1d521cb1e678f5b7c0baf141bd90137ab7d2b436941b35aaf71c9ca5004"
+          "version": "repo-file-v1:sha256:8f96a36cdfa2c1c67c8bb694e8de674a0412d80a5fee2e821e0d89885827f2d5"
         }
       ]
     },
@@ -159,26 +159,26 @@
       "statement": "SOUL.md and VISION.md are read as written with only a leading UTF-8 BOM removed; a missing or blank file yields no prompt, while an unreadable or non-UTF-8 file is an error naming the path.",
       "evidence": [
         {
-          "resource": "repo://src/config.rs#L212-L223",
+          "resource": "repo://src/config.rs#L254-L265",
           "version": "repo-lines-v1:sha256:935a8d5c1ea2b970e49dfefd61104d95d4f7f9ab66af3adddb7b055acc5c5da0:eyJzZWxlY3RlZExpbmVDb3VudCI6MTIsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjliZGJjNjVlNGE0YTc1ZjNmMzk1NmNmOTMxYzg4MTdmYWNlM2M4MzRjMjc3YjdjMDIwNTRlZGEzZmViZDZmYTYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjJjNjc1NTVkMWRiNmUxYTQwOTI2MzU5ZmVhYTMyODBlNDcyNTdmMGQ4NzQxN2Y4ZmI1MzBiMWM4ZjhmNmZkYzQiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjJkM2E4MDAwMjZjNmY2MDFiYmM4MDdlYWI0YmI1Nzk1ZTY4MjEzNGYxMWU3MTRmNzRkMWVlMzQzOTUxNjE4NDYifQ"
         },
         {
-          "resource": "repo://src/config.rs#L265-L281",
+          "resource": "repo://src/config.rs#L319-L335",
           "version": "repo-lines-v1:sha256:4ade9876bd8e1653384dcebde47d413e1aab36995b53055ab5441d82e8b28290:eyJzZWxlY3RlZExpbmVDb3VudCI6MTcsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImQ4MDc5NGE2MDA2MzNhZTY5ZGFlMWYzOThjYTk3ZTc5Yzg3NDc5ZDFjNjE3MDg0ZTRkMTgyODZmMTlkODFjMTUiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjgzZDI4NDFkZmUxZTBkZDIxMmQwYzE1ZDAxZTVhYzkyMTBmNTgwMGI4YmRmZTk5M2FkZDAwYmQ5NGYwMTRkNmMiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImU2YzkxMzJjZTZhZmY4ZmJjMmFmMzRiY2JmYWE1ODVhN2RmMWUxODViYjc5OTFlY2I5YzgxYTUxODg3NWUyOGUifQ"
         }
       ]
     },
     {
       "id": "claim_5b5fea341d19402280a2707c4ca82af7",
-      "statement": "The legacy config.toml keys system_prompt, cooldown_seconds and group_cooldown_seconds are ignored rather than rejected.",
+      "statement": "The legacy config.toml keys system_prompt, cooldown_seconds, group_cooldown_seconds and transcribe_command are ignored rather than rejected.",
       "evidence": [
         {
-          "resource": "repo://src/config.rs#L107-L130",
-          "version": "repo-lines-v1:sha256:88a2926a51f37f52593b1be029cb70e800f905c7073db0aa97177ad6d303faaf:eyJzZWxlY3RlZExpbmVDb3VudCI6MjQsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImQwOGE4NDgyODgzYmU5ZjVmMDIxNDY2ZTgyOGE2MjgwZTI5YTNiMjI0ZTQ4MTY4ZjZjMWQxMDY0MzI3YjlhMjYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjY0Zjc0NTFlMTZhYzBlYjU5ZWE3OGI4Nzk0OTdhMjVkNzM4NzUxYjYwZjE1ZmYyYjQ3ODBhM2RmMGYwYTIyNTIiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjNhN2ZlNzVmMzU5ZTMyZTFkYmE4YWFjMDY1ZmMzYWFhMjY4YzcxZDc0NjUyYjU2OWNkNTJhYjEzYmZlYmMwN2UifQ"
+          "resource": "repo://src/config.rs#L132-L160",
+          "version": "repo-lines-v1:sha256:fd3e11ef8cf2bbcc46307ae10bb2207a236966ead999d1d2f838ccd65911235b:eyJzZWxlY3RlZExpbmVDb3VudCI6MjksImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjViMzkzZWNjMDhiZGJhZjY4YzMzYjk5NjJiZjdjNjBkMjE5N2Q0YzQ3MjQzNzljMDYyMzIxOGU4YTE4MzNkYWQiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjU1ZGYyZGE5ZjNlNjBlZDY5OThlYWM2ZWMyM2E0NjA5Mjc2NjFhMmUwOGI1MDM1Zjc5N2JmMTQyZmE1MDc4ZGQiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjEwNTEwODUwYjhhMDczN2Y2YThiODA3M2Y1NGVkNDZmMDQ0YzdkN2IxYzdmZjg4ZWRiZDNkNmM3MzdkMzU0YzMifQ"
         },
         {
-          "resource": "repo://src/config.rs#L283-L306",
-          "version": "repo-lines-v1:sha256:337be3d271ffd1034a8dead0b8ab67544c72bc1e22f24bb99e564ca5bb9b1ffb:eyJzZWxlY3RlZExpbmVDb3VudCI6MjQsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6Ijc2NWZhNGU5MTRkN2UzOWE2MGJhZTc1M2NhMjE2YTg3NGE3NmZmZWY4NTc4OTY5NTZkNjg0MDM4YTY1MWJjZDAiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImViOWEwOGJjMjlmNmViZWJlOWE1ZjAxOTAzZDk4NDVkYmNiNTg0MmE0NTVmMDdkNWY0MDAyZGFjMzJhNWM2MDUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6Ijg4ZWYzM2E3NjQ3YTM0NDVkNzE1NDU4YmE2Yzc0OGY2NmIyYWZiNzYxYzEyODc4NWU3MzRjZjIyOThjOTdkMzUifQ"
+          "resource": "repo://src/config.rs#L342-L366",
+          "version": "repo-lines-v1:sha256:6d7db37ed42406b64796f6fe895dff6eda6035abf11de0730be579786f6118ec:eyJzZWxlY3RlZExpbmVDb3VudCI6MjUsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImFjZWRhOTg0MDJkODQxZjI4MThkMTg2OTM4NWQ0MzA2OGVmNjkyODg0ZmRiOWMzMzFjOTc4NTY3Y2EwOGQ0NzkiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImI2NmNmNTgzOWE3ZWIyYzcxOTUwYWEzYzRkMTU2YzI1Y2U0NTBmODZlZWI0MDU4ZmZkN2FjNzU3YTYxMDU4ZTAiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6Ijg4ZWYzM2E3NjQ3YTM0NDVkNzE1NDU4YmE2Yzc0OGY2NmIyYWZiNzYxYzEyODc4NWU3MzRjZjIyOThjOTdkMzUifQ"
         }
       ]
     },
@@ -197,7 +197,7 @@
       "statement": "config.toml, SOUL.md and VISION.md are polled every 2 seconds by modification time; any change, including a file being created or deleted, reloads the configuration and sends it, or the load error, to the application as AppEvent::ConfigReloaded.",
       "evidence": [
         {
-          "resource": "repo://src/config.rs#L317-L323",
+          "resource": "repo://src/config.rs#L377-L383",
           "version": "repo-lines-v1:sha256:ffe0647e13679fb34451804a50c541dc7dbd58c8758cacebf62063952f220b8e:eyJzZWxlY3RlZExpbmVDb3VudCI6NywiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiYjY4NmY2YmZhODVjMWJjMjM5OWFjMTAzYmM4ZmIwMmQ0NWZmYTU4ZTA0ZDI4OWIwNzkwNTg3ZWU4NDMxZWI4NyIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDg5M2Y0MGUzNzgxZWI4OTZjODJkMDQ2YTA4MDY3YTExZTM1Y2RmMmYxZGZhZjI2ZmVhOTBiOGRiNjY3NjQxMyIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiMjE1MDlkZjJkM2MxYmM4NDE4NDY0OWE5ODc3MDBlNThkZWY1MmRkZDVjMGRlNWVlYjBmNjRhNzViZDkzMGVjMSIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiMTZjMzk0MWE5YjYxMDI1Yjg1ZWU5ZWEzNzYzOTUyYTViMTM5NmNkMmY2ZjczMTkyZGQ3NDYwNDc5OTlhYjY0MSJ9"
         },
         {
@@ -205,7 +205,7 @@
           "version": "repo-lines-v1:sha256:1501e5e6ec52e197ae84506acd57d2b315d6a1dd434c842b88dc4a4b6d2f1bd3:eyJzZWxlY3RlZExpbmVDb3VudCI6MzgsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImQ3NGMwOGQzMzg5NGUyMWQyYzBmNTMyMDkwYmQwNjFjNDNjMzg1ZDU1MzkwMmI0MDkwZDkxZjJjZDY3M2VjMWQiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjE1Y2VkZDkyMzY4ZGI1MDMwYjUxMjQ3ODcwODlmY2FmZTI5YzhmOGQ4MjExZDA0MTE4ZmM4MTViYWM1MDBjOWEiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjdiYzk4ZDZiMGRjNDkyYzUwMTZkZjEwODA1YTNhNTRlYjljMTg0YTVhNjEzN2U3ZjRiZWNmYTBjMjIwNTUwZDAifQ"
         },
         {
-          "resource": "repo://src/main.rs#L60-L60",
+          "resource": "repo://src/main.rs#L61-L61",
           "version": "repo-lines-v1:sha256:4c448a0646f36833c45781a40616440b41ad3d485b8faf7089d78b8a85c580d6:eyJzZWxlY3RlZExpbmVDb3VudCI6MSwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiNGM0NDhhMDY0NmYzNjgzM2M0NTc4MWE0MDYxNjQ0MGI0MWFkM2Q0ODViOGZhZjcwODlkNzhiOGE4NWM1ODBkNiIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNGM0NDhhMDY0NmYzNjgzM2M0NTc4MWE0MDYxNjQ0MGI0MWFkM2Q0ODViOGZhZjcwODlkNzhiOGE4NWM1ODBkNiIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiOTJiNWYyOWYxMjhmYzYwODc1ZmE5YTYxMGI2MWExMTQ5ZjMzYmJhMDc2ZmU0NzdkZGU0NDA1OTUxNzdjYjAyYyIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiZGQ4NmY4ZmE5MDAwN2QyYmY5ZDk4ZTE3NzNkMDg5ZWE4M2IzMWI4NzMwZjViMWIxZmRjYmViZmM5ZTZiZTMxYSJ9"
         }
       ]
@@ -215,11 +215,11 @@
       "statement": "A successful reload replaces the application configuration and rebuilds the AI client, while a failed reload keeps the current configuration and shows 'Configurazione non ricaricata: <error>'; the web panel port is bound only at startup.",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L383-L400",
+          "resource": "repo://src/app.rs#L417-L434",
           "version": "repo-lines-v1:sha256:11076b2b498b6d19b7a0629886f2cdb37229105049de848490f4e1d9faead710:eyJzZWxlY3RlZExpbmVDb3VudCI6MTgsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjVhYTEzNjY2OGNiMzc5NzgyYTUwMTY4ZDNlOWM4Njg4MmM5YTBhZTAxOGQwNTZiNTEzZWU2ODA5OWYxNjc3YzYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjVmMjAxZDg2ZGM3NzY0YWFmOTA5MGNhNDRkODcxNDM3Y2JiODk4ZGE5ZWJlMGU0ZDhlNjVlNDZiZDJkZGVkYjQiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjAyNjE2ZDk0ZDJmZDg4NWViMDZhMGFmZjVkYjQ1YWUyOTdiMWQ5NzViYzU0MjRlZGIyYWUwZmE2NzZkMzE0ZTYifQ"
         },
         {
-          "resource": "repo://src/main.rs#L95-L104",
+          "resource": "repo://src/main.rs#L96-L105",
           "version": "repo-lines-v1:sha256:54c6448bd45241fd4b90479b1a91e2fd528e7a38f4ac5d543eda728fbf3756d5:eyJzZWxlY3RlZExpbmVDb3VudCI6MTAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImYwZjA3MmY1NzA4NTRiMGM0ODg3YmRlZDU4YzU2NWRiZGExOTNlYmY3NGIwODZmMjYyNTFmZjZhOTZiMmJiYWMiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImRmYWI3MTBkZGY3ODU5ZTU3NTc2NGE5MTNhMDAyYzc0NTg1ZmZhNzAwMTUxMGNiODgxMWRhODNkMmNmNzRiMzQiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImI1OTAxZTAwNGRkYTg1ZmUwNjg0OGVkYjhkMWIzM2Q1NTNmYWM4ZjkyMzZlZTFlMjlkZTNmNGUzMWRmNTM4NTkifQ"
         }
       ]
@@ -237,6 +237,6 @@
   ],
   "verification": {
     "by": "openwiki/0.7.2",
-    "at": "2026-10-23T21:26:24.156Z"
+    "at": "2026-10-24T16:39:50.206Z"
   }
 }

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

@@ -1,6 +1,6 @@
 {
   "schemaVersion": 1,
-  "pageVersion": "sha256:3763c54c3a6a1ddee93531d89a303f3b8c99e547a730b01dfb8e4c97d5211efc",
+  "pageVersion": "sha256:bb1caa7405aaac3db2f806e6cfc1c06f35c73367e4aa23ce6c97e2b4b5fcb861",
   "claims": [
     {
       "id": "claim_6b1c328bf7f7452992ea8eeef561edd4",
@@ -27,11 +27,11 @@
       "statement": "config.toml, session.db, history.db and the optional ai_requests.log are located in the wh directory under the platform config directory.",
       "evidence": [
         {
-          "resource": "repo://src/config.rs#L164-L168",
+          "resource": "repo://src/config.rs#L206-L210",
           "version": "repo-lines-v1:sha256:dd4141940976146e96656f2de294f92e1275a16efba97aa2c7318676f34bdfa4:eyJzZWxlY3RlZExpbmVDb3VudCI6NSwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiMzVhMDYyMDkxMmIzMTUwZDZlYmQwNDQyOTUxZTdjNWEwODlmODMwMzYzNmJjODZiNmQxOWRmN2U3YTFmMWEzZCIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDEyY2EzNDVjY2Y3NWJmOWMwODA2YmNlNjk1YmU4ZGU4MDhiNzk5ODQyNTFhN2E1NGQyMDJjZjYxMDFkZDQ1MSIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiNGU1ZGI4YzkwMjczZTAyOTc3MTY3ZWMwNDM0N2M0MWQ5MmE1MTE4NDMxMTA0M2UyMTU2YmM4MGFiYjkwMWQ0NyIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiMDY4YzgxNzM5NzQwNTE2YzI4OWQ4ZWRlZWM0N2IwMjcxMGFmOWVmNWM4MjE4M2NhNWQwMTc4MWVhMjI1YjU1MCJ9"
         },
         {
-          "resource": "repo://src/config.rs#L45-L54",
+          "resource": "repo://src/config.rs#L55-L64",
           "version": "repo-lines-v1:sha256:6624d58d13d3982b8829e7b759fc7c36a643278f2d243526a887428e34a0d469:eyJzZWxlY3RlZExpbmVDb3VudCI6MTAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImU1MDllZGUxYTUxZmE5MGM4ZmQ0YTNkODdmYjJhMGU5OTMwNzhkMjc0MzU1NGFjMGM4MmIzMTdmNzQ0OTY5ZmYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjFmNzZjMzcwNTRkNWY0YzFhNWZhZDE4YzFlYjI0NTNmODIxNTI0NDQ2M2ZkNjA4MzIwZDEzZDYxNmFlYmVhNmQiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjMwY2VjY2EzYjg3NDgwNDU3ZGVmZTlkOWVkY2I1N2VmMDdlN2ZlZTE2MTljZjdmYWEyNzM0Y2Y2M2Q5MzIxYjEiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjQ1N2RlMDJkYWE2M2RmYzAxZmQxNjkyNDc1ODY2ODA3OGM5NmJlMGM1ODkyZjQ4M2UzMDdmNjAyYTU0MDRkMzQifQ"
         }
       ]
@@ -41,7 +41,7 @@
       "statement": "Unless --version or -V is given, main loads the configuration, opens the history, starts the WhatsApp client, the optional web panel, the configuration watcher and the input thread, and runs the TUI loop.",
       "evidence": [
         {
-          "resource": "repo://src/main.rs#L42-L71",
+          "resource": "repo://src/main.rs#L43-L72",
           "version": "repo-lines-v1:sha256:1f8efd5e596ab7a7d1da0bfd9bc62005e6be9c40accb54842253ec733e4abfe3:eyJzZWxlY3RlZExpbmVDb3VudCI6MzAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImExZDA2ZTA0OGNkMjcyNzM4ZDgzN2IwYzBlYzZhYWIwYjAzMTQ1N2U0NzVmZDQ5YWJiY2JhZmM1ZjczYzAyMGQiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImE5NjlkZGFlYmE5MjRhNThlYTY2OGUyMDQzMDFiZWE5ZThjMzZjNjg5NTU3NTc3NmYzZDkzZWNiZmJlNzdjMzUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjAxZmI2ZDM1MThiYTRjMzUwOWRiMWM5MTAyMTk2NjQwMzI4ZjgzM2M3MDJmMzgwNmNmZjFlZGI5MzU4Y2Q5YTgifQ"
         }
       ]
@@ -61,7 +61,7 @@
       "statement": "The AI autoreply can be enabled for private chats and groups but not for broadcasts or newsletters.",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L1544-L1557",
+          "resource": "repo://src/app.rs#L1889-L1902",
           "version": "repo-lines-v1:sha256:76ea2311dd543d33bf1c52b4cee8997184cb67643a8ba3b2f715abf2fea34667:eyJzZWxlY3RlZExpbmVDb3VudCI6MTQsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjA0NDI0MGFiOGNkMTM1NDE2ZTQ5MjMyYTEzMGIzYzlkYmI2NWZmYzg0OGNjYzY3OWJhYTQxYWQzNjY4ZTA3MjQiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjE5ZTc2MzU1Y2YxMTk5NDgwYzNmOTk4MzZiMTQ4NmM4NDFkODkxZmFhZDkwNGM2MDhlMDkzMDIyZDM3NTdhZWUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6Ijg5NGUwY2FkODFjNTBiOWY4MWE5ODI2ZjA1YzUyZjY0MGE0ZGM3NzdiYWQyNGM2MGEwMjQ5NzhlZWJmMWRjMGMifQ"
         },
         {
@@ -75,15 +75,15 @@
       "statement": "Commands use function keys: F2 switches chats/contacts, F3 toggles the autoreply of the selected or open chat depending on focus, F4 toggles the global autoreply, F8 twice relinks WhatsApp and F10 quits.",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L1101-L1116",
+          "resource": "repo://src/app.rs#L1342-L1357",
           "version": "repo-lines-v1:sha256:f3a02114e65f3b57e1e9609cdb5227848e7d4a8d0d2d3abc6b38c40f5286461f:eyJzZWxlY3RlZExpbmVDb3VudCI6MTYsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjBjMWZhZjI0NjQzNTZlZjY0NzAyNjI4YzM0MGYyOGNhZDljMjY3OGYxMTViNTAxNDZkMGMyNTBmNzhmZDk2NWMiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjkzNzNhMTgxNzI2YzQ3ZjdjNzlmOTI3ZDI2NzhhYzQ3NjYzY2VhMzI1OGYxOGEwNTM0ZTg2NDVlYmFlOTlkMGUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6Ijc1YjdhNGU5MWJmNTc3ZDQwNjIyM2IzMjg2ZTFhYmI0NGRkNzUwYmViNmU1MzIwYTMyMGZlZDJkOTBjOWQ3ZDQifQ"
         },
         {
-          "resource": "repo://src/app.rs#L23-L32",
+          "resource": "repo://src/app.rs#L24-L33",
           "version": "repo-lines-v1:sha256:683c16567f4074fbe88d85ef8581c16739d5ca0531826b0fb49c5df95ac18f20:eyJzZWxlY3RlZExpbmVDb3VudCI6MTAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImJkMTkzOTVkMDA1MWQ2YTA4MzBkMmQ5OWZhNTk4ZDU4N2EyOWZkNTA0OTkwNGQ1OWNiNTI2YmQ3Yzc4ODBiMGUiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6ImFmNjIzNzA4OWI5MWQxMDdiNjY0NTc5NzNhOGJhMmE3MWQ4NzA0NGVhOTFlOTE4Y2RlMjA1Zjg2YWNkNDZjOTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjFlZmRkMDA4YTE2MzY2YTNmMTdiNjY1M2IyNzcyMThkYzQ2NTRiMGFjYTE5MGI1YmFiZDcwNmIyM2IxMDQwNzciLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImI2ZmI3YjljNTEzODFiZmNlNmIwYzU4ZTY0ZmZhMjhjODMyNjU5NTkyOWQ1MDNlYTUwYjBlMDRiNTk0N2QwNTMifQ"
         },
         {
-          "resource": "repo://src/app.rs#L884-L929",
+          "resource": "repo://src/app.rs#L1122-L1167",
           "version": "repo-lines-v1:sha256:6e737f1163f7fc1e0f873a9756a9a2c32d247edaf022b24c9d62e4f5d3ecdc71:eyJzZWxlY3RlZExpbmVDb3VudCI6NDYsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjlhZGRmNjgzMjdhYTA4ZWZjNDYxNGFmZDIwOTkzZTFkNzZiZjUxNmQ3NzRjM2QyMmIzOTUyOTEwNmNkOTBhZmMiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjE2MmVlNTdlNDFhOGMyMGFlMDE2NGRmMjYxNDM5ZGNjNDk5ZDM1MjA4MzQxNTE1MTZkMDQ3ZThkZWFlOGE0NjYiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6Ijk2ZWU5ZDllODg2ODFmN2JmM2M4ZmZiZjEzZDgyODk2ODM4MGY2Y2E0M2Y1ZGE4ODI5MmU1MjY3ZDRjMDhlOWYifQ"
         }
       ]
@@ -93,11 +93,11 @@
       "statement": "The system prompt is read from SOUL.md in the data directory rather than from config.toml, and the optional VISION.md replaces the built-in image description request.",
       "evidence": [
         {
-          "resource": "repo://src/config.rs#L19-L25",
+          "resource": "repo://src/config.rs#L23-L29",
           "version": "repo-lines-v1:sha256:755a1cd95dc7a5dbe595fb5421d48c62b8a21f7908ec1d68ac66efa32eaabe47:eyJzZWxlY3RlZExpbmVDb3VudCI6NywiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiMjhhODQwNDU2NTU5OWJhNzUzM2Q2NWYzYTBjODNmMzE3NzRlM2MxYjg2N2EzNzA2ZjViOTg3OWNkNTg4YTMyYyIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiYTRjYmFkZDRlOTdmMDhjMDkyYTdmZGQ4M2I0Mzg2NTA2MWU0MGZkOWEyMTFmNmVkOGRiNDNhOTE1NGUxMjgxNyIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiMDg2ZGU3NmMzYjVlNGIwN2Q2MmNkZTNjNDY1ZDEwN2QzNmE4NDEyMjE2ZThlMWFhMjZjMjE2ZmFmMWE4ZDY5OSIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiNzEzMTUyNmUyZDdlYWQwODA2YmQ3MmViNGMwYmUyMTU2MjhlZmIwYzMyNzc5MzQ2NWRiMzU2ZDY1NDg0MDM3ZiJ9"
         },
         {
-          "resource": "repo://src/config.rs#L265-L281",
+          "resource": "repo://src/config.rs#L319-L335",
           "version": "repo-lines-v1:sha256:4ade9876bd8e1653384dcebde47d413e1aab36995b53055ab5441d82e8b28290:eyJzZWxlY3RlZExpbmVDb3VudCI6MTcsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImQ4MDc5NGE2MDA2MzNhZTY5ZGFlMWYzOThjYTk3ZTc5Yzg3NDc5ZDFjNjE3MDg0ZTRkMTgyODZmMTlkODFjMTUiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjgzZDI4NDFkZmUxZTBkZDIxMmQwYzE1ZDAxZTVhYzkyMTBmNTgwMGI4YmRmZTk5M2FkZDAwYmQ5NGYwMTRkNmMiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImU2YzkxMzJjZTZhZmY4ZmJjMmFmMzRiY2JmYWE1ODVhN2RmMWUxODViYjc5OTFlY2I5YzgxYTUxODg3NWUyOGUifQ"
         }
       ]
@@ -119,6 +119,6 @@
   ],
   "verification": {
     "by": "openwiki/0.7.2",
-    "at": "2026-10-23T21:26:24.156Z"
+    "at": "2026-10-24T16:39:50.206Z"
   }
 }

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

@@ -1,6 +1,6 @@
 {
   "schemaVersion": 1,
-  "pageVersion": "sha256:0f5f5df1184eb187831e0b8e3f9e8a2516c314386ce7360f1c4915c9450c587f",
+  "pageVersion": "sha256:50c64fbc1c28a488dc62c88f83e98a06ce449b71661690714c90854f832928e9",
   "claims": [
     {
       "id": "claim_2e232262c6d940c7ba52a945b96928f6",
@@ -11,7 +11,7 @@
           "version": "repo-lines-v1:sha256:03c5b4b27fb001def5489d5e0264e20f2538b8214c3df8ceda3354aaee53c2c8:eyJzZWxlY3RlZExpbmVDb3VudCI6MiwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiMjVjZjVmYzIzZjJhNmQyM2MzZWE1MWU3MmY1ZTg5NmM3NTU1ZmJhYTA1Y2Q1YjFlYTE4MmJmOTYxOGE4ZTgxYiIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiMWUwYWYzZmQ0YTRjM2ExZDQ3YzdmNDcwZWQwZDg2ZDkwZDlkZTIxMGE1ZWIxNDU1ZmJkOTQ0ODJhNDc2NWI3MiIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiZWRjODMxMjQ5NjYxNGM2YzllMTE0NTc0YTc5ZDkxODU3MDM0YmJlM2IzMDk5MmYyNzYwZjcxNjY0ZjAwNWM2OSIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiOTM0OTA4YTE2NjZlZThkZWFiMmIyYTc2NjdjODQ0OGZiYjRkNDZlMzJhYjZjNTY4YmI3MjMyMGU1ZWJmMDlmMyJ9"
         },
         {
-          "resource": "repo://src/app.rs#L1596-L1597",
+          "resource": "repo://src/app.rs#L1941-L1942",
           "version": "repo-lines-v1:sha256:03c5b4b27fb001def5489d5e0264e20f2538b8214c3df8ceda3354aaee53c2c8:eyJzZWxlY3RlZExpbmVDb3VudCI6MiwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiMjVjZjVmYzIzZjJhNmQyM2MzZWE1MWU3MmY1ZTg5NmM3NTU1ZmJhYTA1Y2Q1YjFlYTE4MmJmOTYxOGE4ZTgxYiIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiMWUwYWYzZmQ0YTRjM2ExZDQ3YzdmNDcwZWQwZDg2ZDkwZDlkZTIxMGE1ZWIxNDU1ZmJkOTQ0ODJhNDc2NWI3MiIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiZjY5NGY2NjJmYWIyMzFlOGQ4ZGIwOTRiNTU3OTgxNWYwOTlkYTQwZGU3ZThiOTY1YWUwNDU2YWRjMDQ5YjM4NiIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiOGJlOTk2NWNlNGEzMmY5N2Y5ZjMzYzU1MzhjZWY5ZDE0Njc4YjE0NGQ4ODg1ZDY2Mjk4YTZkZTJjODg3NTZlMSJ9"
         },
         {
@@ -19,11 +19,11 @@
           "version": "repo-lines-v1:sha256:03c5b4b27fb001def5489d5e0264e20f2538b8214c3df8ceda3354aaee53c2c8:eyJzZWxlY3RlZExpbmVDb3VudCI6MiwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiMjVjZjVmYzIzZjJhNmQyM2MzZWE1MWU3MmY1ZTg5NmM3NTU1ZmJhYTA1Y2Q1YjFlYTE4MmJmOTYxOGE4ZTgxYiIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiMWUwYWYzZmQ0YTRjM2ExZDQ3YzdmNDcwZWQwZDg2ZDkwZDlkZTIxMGE1ZWIxNDU1ZmJkOTQ0ODJhNDc2NWI3MiIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiOTMyZjlmMDdlZDQ0ZTc0ZjliN2Y1ZGEzNTY1NjIxZjExNGRiNzdlZGY1NDc2NzZjNjZiNzRjMWMwNzQzN2MyMSIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiN2M0NzcyZjY4MTNiYmFmYmNhYjU4MWZlN2JiZmIwOTI4NDRlMGZhOWU3MjQwNmMxNzNjNmYyNTMxMjgyMTZmMSJ9"
         },
         {
-          "resource": "repo://src/config.rs#L350-L351",
+          "resource": "repo://src/config.rs#L410-L411",
           "version": "repo-lines-v1:sha256:03c5b4b27fb001def5489d5e0264e20f2538b8214c3df8ceda3354aaee53c2c8:eyJzZWxlY3RlZExpbmVDb3VudCI6MiwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiMjVjZjVmYzIzZjJhNmQyM2MzZWE1MWU3MmY1ZTg5NmM3NTU1ZmJhYTA1Y2Q1YjFlYTE4MmJmOTYxOGE4ZTgxYiIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiMWUwYWYzZmQ0YTRjM2ExZDQ3YzdmNDcwZWQwZDg2ZDkwZDlkZTIxMGE1ZWIxNDU1ZmJkOTQ0ODJhNDc2NWI3MiIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiMTllNzYzNTVjZjExOTk0ODBjM2Y5OTgzNmIxNDg2Yzg0MWQ4OTFmYWFkOTA0YzYwOGUwOTMwMjJkMzc1N2FlZSIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiNGEwYTUzOWYxODQ2ODA0NzgzMDRlNjEzNTEzNGQyM2FhMjE2ZmZkZTBmMTYwMzJmYTZkNzYyOGQ0MzIxNzFhNSJ9"
         },
         {
-          "resource": "repo://src/history.rs#L543-L544",
+          "resource": "repo://src/history.rs#L557-L558",
           "version": "repo-lines-v1:sha256:03c5b4b27fb001def5489d5e0264e20f2538b8214c3df8ceda3354aaee53c2c8:eyJzZWxlY3RlZExpbmVDb3VudCI6MiwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiMjVjZjVmYzIzZjJhNmQyM2MzZWE1MWU3MmY1ZTg5NmM3NTU1ZmJhYTA1Y2Q1YjFlYTE4MmJmOTYxOGE4ZTgxYiIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiMWUwYWYzZmQ0YTRjM2ExZDQ3YzdmNDcwZWQwZDg2ZDkwZDlkZTIxMGE1ZWIxNDU1ZmJkOTQ0ODJhNDc2NWI3MiIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiNjA2M2M0ZDFhZjhjZDg5YmIwYmRkNzY1N2ZmY2JmNjgwNmE2MTBkNzlkNmM4ZDRjMzA3YzI0MWRmMzkwMDUyMiIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiMjgzMTBhMmUzZTliNGU0MTRjY2FiZmY2ZTdkMTc0YjgwMzVlNmZjZTRmODVmMTdjZTA4MTNmOWZlZTkzOTYxZiJ9"
         },
         {
@@ -31,7 +31,7 @@
           "version": "repo-lines-v1:sha256:03c5b4b27fb001def5489d5e0264e20f2538b8214c3df8ceda3354aaee53c2c8:eyJzZWxlY3RlZExpbmVDb3VudCI6MiwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiMjVjZjVmYzIzZjJhNmQyM2MzZWE1MWU3MmY1ZTg5NmM3NTU1ZmJhYTA1Y2Q1YjFlYTE4MmJmOTYxOGE4ZTgxYiIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiMWUwYWYzZmQ0YTRjM2ExZDQ3YzdmNDcwZWQwZDg2ZDkwZDlkZTIxMGE1ZWIxNDU1ZmJkOTQ0ODJhNDc2NWI3MiIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiMzViMjE2ODczZmZmNmEwNTc3MDU5NWM1OWNhZDNlNmE5NzM1MmJiNDE3ZDYwYzc2MmVhNDM4MWYxOTkwMjg3OSIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiNDVlYjIxMDg3NDU0NjZhOTVjZjg5YjVhZjdmMDZhMDA2NGU0OGRiMDVkMmMyOWU5Y2YzZDZkMGQxNmY3MjY5ZiJ9"
         },
         {
-          "resource": "repo://src/whatsapp.rs#L1140-L1141",
+          "resource": "repo://src/whatsapp.rs#L1215-L1216",
           "version": "repo-lines-v1:sha256:03c5b4b27fb001def5489d5e0264e20f2538b8214c3df8ceda3354aaee53c2c8:eyJzZWxlY3RlZExpbmVDb3VudCI6MiwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiMjVjZjVmYzIzZjJhNmQyM2MzZWE1MWU3MmY1ZTg5NmM3NTU1ZmJhYTA1Y2Q1YjFlYTE4MmJmOTYxOGE4ZTgxYiIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiMWUwYWYzZmQ0YTRjM2ExZDQ3YzdmNDcwZWQwZDg2ZDkwZDlkZTIxMGE1ZWIxNDU1ZmJkOTQ0ODJhNDc2NWI3MiIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiZDZkMTNkYTUzZGRmNTRlMDlhOWZiMzc3ODc0MTRhOTYxNDZkNDcxYzVlYmM2Y2UyNmNmOWEzODRmMTg3NzhmMSIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiMWU1MWM0MjBiN2E3NDU0MmZlNGYwMjY4Yzk0YTYwZjRmYjAwZTdhODI0NmFhNTlkNzQ0NTkzYmY3OGUyN2M5MCJ9"
         }
       ]
@@ -58,11 +58,11 @@
     },
     {
       "id": "claim_879517863bdd4ae5b7cc952b3e39e8a4",
-      "statement": "history tests cover deduplication, ordering, messages_since, chat names, per-chat and global autoreply flags, message_exists, image descriptions, chat deletion, contacts and on-disk opening.",
+      "statement": "history tests cover deduplication, ordering, messages_since, chat names, per-chat and global autoreply flags (including survival across incoming messages), message_exists, update_message_text, image descriptions, chat deletion, contacts and on-disk opening.",
       "evidence": [
         {
-          "resource": "repo://src/history.rs#L543-L852",
-          "version": "repo-lines-v1:sha256:a4d3c1973b9e386233ba1511ab6e3adcf322e06bb30f97f8fb8f6a96258e6360:eyJzZWxlY3RlZExpbmVDb3VudCI6MzEwLCJmaXJzdFNlbGVjdGVkTGluZUhhc2giOiIyNWNmNWZjMjNmMmE2ZDIzYzNlYTUxZTcyZjVlODk2Yzc1NTVmYmFhMDVjZDViMWVhMTgyYmY5NjE4YThlODFiIiwibGFzdFNlbGVjdGVkTGluZUhhc2giOiI0MTJjYTM0NWNjZjc1YmY5YzA4MDZiY2U2OTViZThkZTgwOGI3OTk4NDI1MWE3YTU0ZDIwMmNmNjEwMWRkNDUxIiwicHJlY2VkaW5nQ29udGV4dExpbmVDb3VudCI6MywicHJlY2VkaW5nQ29udGV4dEhhc2giOiI2MDYzYzRkMWFmOGNkODliYjBiZGQ3NjU3ZmZjYmY2ODA2YTYxMGQ3OWQ2YzhkNGMzMDdjMjQxZGYzOTAwNTIyIiwiZm9sbG93aW5nQ29udGV4dExpbmVDb3VudCI6MCwiZm9sbG93aW5nQ29udGV4dEhhc2giOiJlM2IwYzQ0Mjk4ZmMxYzE0OWFmYmY0Yzg5OTZmYjkyNDI3YWU0MWU0NjQ5YjkzNGNhNDk1OTkxYjc4NTJiODU1In0"
+          "resource": "repo://src/history.rs#L563-L872",
+          "version": "repo-lines-v1:sha256:5a7faac412b418e8d4aca9502c8b2c44ebacbab61bb9be1912c746fbcfa3ea92:eyJzZWxlY3RlZExpbmVDb3VudCI6MzEwLCJmaXJzdFNlbGVjdGVkTGluZUhhc2giOiI0ZDJkMDQ1MDNjYWJjZWRhODhiZTgwM2NmMjYxNGExZTI1MWJjZjQwZDE3YTc5ZGRlZmI3MWExMjlmMDNkMjNmIiwibGFzdFNlbGVjdGVkTGluZUhhc2giOiIwMWJhNDcxOWM4MGI2ZmU5MTFiMDkxYTdjMDUxMjRiNjRlZWVjZTk2NGUwOWMwNThlZjhmOTgwNWRhY2E1NDZiIiwicHJlY2VkaW5nQ29udGV4dExpbmVDb3VudCI6MywicHJlY2VkaW5nQ29udGV4dEhhc2giOiIzNmU0YjEyNGNhM2YzZjM2MGQxOWQyMTgwMjQxZDk2NzAyMjE4OWUzOTljZmY3ZTM4M2E0YTVmNmY2MmJmZDliIiwiZm9sbG93aW5nQ29udGV4dExpbmVDb3VudCI6MywiZm9sbG93aW5nQ29udGV4dEhhc2giOiI5MmY2MjMxNmZlZTgwNWMwNjg2MzgxZWUyODkwNzEzNWM2ZjE5NTc0ZTlmNzY0YWE3NWMzNDNhOTMxZDJlOTRmIn0"
         }
       ]
     },
@@ -81,7 +81,7 @@
       "statement": "History tests use an in-memory database helper and the on-disk test writes under the system temporary directory.",
       "evidence": [
         {
-          "resource": "repo://src/history.rs#L837-L851",
+          "resource": "repo://src/history.rs#L864-L878",
           "version": "repo-lines-v1:sha256:47bb26a3e316232f3605730556cc5136a6388c441f30c963b8d723852177dc9f:eyJzZWxlY3RlZExpbmVDb3VudCI6MTUsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjkyYjVlZGY1OTQ5Y2ZlYTY3ODIyY2Y2OTExMzIzNjNkNjhkNmYxZTE2OWY1ZTBkNGY3YzE0ZTVmYTFlNmQ0ZTUiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjdjOWI2OGRlMGIyZmFhYWM3OTQwMzEwNmMxNGRkOWE5MDgyODA4NzEzZmE0MjBjMWUyMmE2OGM1MTE3ZTE0MWMiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjoxLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEifQ"
         },
         {
@@ -92,36 +92,40 @@
     },
     {
       "id": "claim_1814788a665c474fbe9623328521c079",
-      "statement": "The suite contains 209 test functions: autoreply 55, config 37, ai 27, whatsapp 23 (one of them an ignored live test), app 20, history 18, ui 15, web 11, main 2, config_watch 1.",
+      "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.",
       "evidence": [
         {
-          "resource": "repo://src/ai.rs#L581-L910",
-          "version": "repo-lines-v1:sha256:d19f1348b5c8956f0781aa1895f12dd84dfe7f18a54ac9b446ef380fc38b9548:eyJzZWxlY3RlZExpbmVDb3VudCI6MzMwLCJmaXJzdFNlbGVjdGVkTGluZUhhc2giOiIyNWNmNWZjMjNmMmE2ZDIzYzNlYTUxZTcyZjVlODk2Yzc1NTVmYmFhMDVjZDViMWVhMTgyYmY5NjE4YThlODFiIiwibGFzdFNlbGVjdGVkTGluZUhhc2giOiI0MTJjYTM0NWNjZjc1YmY5YzA4MDZiY2U2OTViZThkZTgwOGI3OTk4NDI1MWE3YTU0ZDIwMmNmNjEwMWRkNDUxIiwicHJlY2VkaW5nQ29udGV4dExpbmVDb3VudCI6MywicHJlY2VkaW5nQ29udGV4dEhhc2giOiJlZGM4MzEyNDk2NjE0YzZjOWUxMTQ1NzRhNzlkOTE4NTcwMzRiYmUzYjMwOTkyZjI3NjBmNzE2NjRmMDA1YzY5IiwiZm9sbG93aW5nQ29udGV4dExpbmVDb3VudCI6MCwiZm9sbG93aW5nQ29udGV4dEhhc2giOiJlM2IwYzQ0Mjk4ZmMxYzE0OWFmYmY0Yzg5OTZmYjkyNDI3YWU0MWU0NjQ5YjkzNGNhNDk1OTkxYjc4NTJiODU1In0"
+          "resource": "repo://src/ai.rs#L589-L914",
+          "version": "repo-lines-v1:sha256:563cc7b7eeb9a5af67ee42215a0be871ea267497ef548419e7715973caa737c6:eyJzZWxlY3RlZExpbmVDb3VudCI6MzI2LCJmaXJzdFNlbGVjdGVkTGluZUhhc2giOiIwMWJhNDcxOWM4MGI2ZmU5MTFiMDkxYTdjMDUxMjRiNjRlZWVjZTk2NGUwOWMwNThlZjhmOTgwNWRhY2E1NDZiIiwibGFzdFNlbGVjdGVkTGluZUhhc2giOiI0MTJjYTM0NWNjZjc1YmY5YzA4MDZiY2U2OTViZThkZTgwOGI3OTk4NDI1MWE3YTU0ZDIwMmNmNjEwMWRkNDUxIiwicHJlY2VkaW5nQ29udGV4dExpbmVDb3VudCI6MywicHJlY2VkaW5nQ29udGV4dEhhc2giOiJjNGI2ODI1NzRkYjY3YzNjOGJmNjVmZjk2NmZlMzJiZWE0OGRmYzA0MTNjODMwMzcwNjIxMDEwMzkyYTYyMGI5IiwiZm9sbG93aW5nQ29udGV4dExpbmVDb3VudCI6MCwiZm9sbG93aW5nQ29udGV4dEhhc2giOiJlM2IwYzQ0Mjk4ZmMxYzE0OWFmYmY0Yzg5OTZmYjkyNDI3YWU0MWU0NjQ5YjkzNGNhNDk1OTkxYjc4NTJiODU1In0"
         },
         {
-          "resource": "repo://src/app.rs#L1596-L1799",
-          "version": "repo-lines-v1:sha256:49775da3ba12a328474dedc2a4e2a0d82d1fb6d37c8adda1940d1fc34c97ec42:eyJzZWxlY3RlZExpbmVDb3VudCI6MjA0LCJmaXJzdFNlbGVjdGVkTGluZUhhc2giOiIyNWNmNWZjMjNmMmE2ZDIzYzNlYTUxZTcyZjVlODk2Yzc1NTVmYmFhMDVjZDViMWVhMTgyYmY5NjE4YThlODFiIiwibGFzdFNlbGVjdGVkTGluZUhhc2giOiI0MTJjYTM0NWNjZjc1YmY5YzA4MDZiY2U2OTViZThkZTgwOGI3OTk4NDI1MWE3YTU0ZDIwMmNmNjEwMWRkNDUxIiwicHJlY2VkaW5nQ29udGV4dExpbmVDb3VudCI6MywicHJlY2VkaW5nQ29udGV4dEhhc2giOiJmNjk0ZjY2MmZhYjIzMWU4ZDhkYjA5NGI1NTc5ODE1ZjA5OWRhNDBkZTdlOGI5NjVhZTA0NTZhZGMwNDliMzg2IiwiZm9sbG93aW5nQ29udGV4dExpbmVDb3VudCI6MCwiZm9sbG93aW5nQ29udGV4dEhhc2giOiJlM2IwYzQ0Mjk4ZmMxYzE0OWFmYmY0Yzg5OTZmYjkyNDI3YWU0MWU0NjQ5YjkzNGNhNDk1OTkxYjc4NTJiODU1In0"
+          "resource": "repo://src/app.rs#L1941-L2171",
+          "version": "repo-lines-v1:sha256:4246f63e0a5bf224e095e0ed67f18427d444f16f51597b335864cac6b00c053c:eyJzZWxlY3RlZExpbmVDb3VudCI6MjMxLCJmaXJzdFNlbGVjdGVkTGluZUhhc2giOiIyNWNmNWZjMjNmMmE2ZDIzYzNlYTUxZTcyZjVlODk2Yzc1NTVmYmFhMDVjZDViMWVhMTgyYmY5NjE4YThlODFiIiwibGFzdFNlbGVjdGVkTGluZUhhc2giOiI0MTJjYTM0NWNjZjc1YmY5YzA4MDZiY2U2OTViZThkZTgwOGI3OTk4NDI1MWE3YTU0ZDIwMmNmNjEwMWRkNDUxIiwicHJlY2VkaW5nQ29udGV4dExpbmVDb3VudCI6MywicHJlY2VkaW5nQ29udGV4dEhhc2giOiJmNjk0ZjY2MmZhYjIzMWU4ZDhkYjA5NGI1NTc5ODE1ZjA5OWRhNDBkZTdlOGI5NjVhZTA0NTZhZGMwNDliMzg2IiwiZm9sbG93aW5nQ29udGV4dExpbmVDb3VudCI6MCwiZm9sbG93aW5nQ29udGV4dEhhc2giOiJlM2IwYzQ0Mjk4ZmMxYzE0OWFmYmY0Yzg5OTZmYjkyNDI3YWU0MWU0NjQ5YjkzNGNhNDk1OTkxYjc4NTJiODU1In0"
         },
         {
-          "resource": "repo://src/autoreply.rs#L505-L1093",
-          "version": "repo-lines-v1:sha256:73d6683e98d1a479be5b3c5c82533ad41e429042edcbbc8f6c7d722a1eca2628:eyJzZWxlY3RlZExpbmVDb3VudCI6NTg5LCJmaXJzdFNlbGVjdGVkTGluZUhhc2giOiIyNWNmNWZjMjNmMmE2ZDIzYzNlYTUxZTcyZjVlODk2Yzc1NTVmYmFhMDVjZDViMWVhMTgyYmY5NjE4YThlODFiIiwibGFzdFNlbGVjdGVkTGluZUhhc2giOiI0MTJjYTM0NWNjZjc1YmY5YzA4MDZiY2U2OTViZThkZTgwOGI3OTk4NDI1MWE3YTU0ZDIwMmNmNjEwMWRkNDUxIiwicHJlY2VkaW5nQ29udGV4dExpbmVDb3VudCI6MywicHJlY2VkaW5nQ29udGV4dEhhc2giOiI5MzJmOWYwN2VkNDRlNzRmOWI3ZjVkYTM1NjU2MjFmMTE0ZGI3N2VkZjU0NzY3NmM2NmI3NGMxYzA3NDM3YzIxIiwiZm9sbG93aW5nQ29udGV4dExpbmVDb3VudCI6MCwiZm9sbG93aW5nQ29udGV4dEhhc2giOiJlM2IwYzQ0Mjk4ZmMxYzE0OWFmYmY0Yzg5OTZmYjkyNDI3YWU0MWU0NjQ5YjkzNGNhNDk1OTkxYjc4NTJiODU1In0"
+          "resource": "repo://src/autoreply.rs#L551-L1093",
+          "version": "repo-lines-v1:sha256:c45662796f439d33fcddbeb69c7dff5d616cdf1c336cabc0814728361d089191:eyJzZWxlY3RlZExpbmVDb3VudCI6NTQzLCJmaXJzdFNlbGVjdGVkTGluZUhhc2giOiJjNTBjNWU1NDBmMDhkZWZhYTY4ZTQwODlhMTRlNDAzNWIyM2MwODUwM2UwNDcyYmU4ZmUzOTkyNmEyMWRhOThkIiwibGFzdFNlbGVjdGVkTGluZUhhc2giOiI0MTJjYTM0NWNjZjc1YmY5YzA4MDZiY2U2OTViZThkZTgwOGI3OTk4NDI1MWE3YTU0ZDIwMmNmNjEwMWRkNDUxIiwicHJlY2VkaW5nQ29udGV4dExpbmVDb3VudCI6MywicHJlY2VkaW5nQ29udGV4dEhhc2giOiI2M2I4NDA4NjIxZjgzYzM1N2YwMWRjM2FhMWU0NGJkZDcxYzYyMDE0NWI3Yjg5NDMxOTQ1MmNhZTczZjhmYmQwIiwiZm9sbG93aW5nQ29udGV4dExpbmVDb3VudCI6MCwiZm9sbG93aW5nQ29udGV4dEhhc2giOiJlM2IwYzQ0Mjk4ZmMxYzE0OWFmYmY0Yzg5OTZmYjkyNDI3YWU0MWU0NjQ5YjkzNGNhNDk1OTkxYjc4NTJiODU1In0"
         },
         {
-          "resource": "repo://src/config.rs#L350-L699",
-          "version": "repo-lines-v1:sha256:f633f3663fa11b33dd8d6391a60b3b839aaf69dfaa341e10d37f025c069d49b3:eyJzZWxlY3RlZExpbmVDb3VudCI6MzUwLCJmaXJzdFNlbGVjdGVkTGluZUhhc2giOiIyNWNmNWZjMjNmMmE2ZDIzYzNlYTUxZTcyZjVlODk2Yzc1NTVmYmFhMDVjZDViMWVhMTgyYmY5NjE4YThlODFiIiwibGFzdFNlbGVjdGVkTGluZUhhc2giOiI0MTJjYTM0NWNjZjc1YmY5YzA4MDZiY2U2OTViZThkZTgwOGI3OTk4NDI1MWE3YTU0ZDIwMmNmNjEwMWRkNDUxIiwicHJlY2VkaW5nQ29udGV4dExpbmVDb3VudCI6MywicHJlY2VkaW5nQ29udGV4dEhhc2giOiIxOWU3NjM1NWNmMTE5OTQ4MGMzZjk5ODM2YjE0ODZjODQxZDg5MWZhYWQ5MDRjNjA4ZTA5MzAyMmQzNzU3YWVlIiwiZm9sbG93aW5nQ29udGV4dExpbmVDb3VudCI6MCwiZm9sbG93aW5nQ29udGV4dEhhc2giOiJlM2IwYzQ0Mjk4ZmMxYzE0OWFmYmY0Yzg5OTZmYjkyNDI3YWU0MWU0NjQ5YjkzNGNhNDk1OTkxYjc4NTJiODU1In0"
+          "resource": "repo://src/config.rs#L410-L804",
+          "version": "repo-lines-v1:sha256:5cd3b7a51ee76024e35b0337d198566fb9c818591ca051940cf26fbc8226e22b:eyJzZWxlY3RlZExpbmVDb3VudCI6Mzk1LCJmaXJzdFNlbGVjdGVkTGluZUhhc2giOiIyNWNmNWZjMjNmMmE2ZDIzYzNlYTUxZTcyZjVlODk2Yzc1NTVmYmFhMDVjZDViMWVhMTgyYmY5NjE4YThlODFiIiwibGFzdFNlbGVjdGVkTGluZUhhc2giOiJkYTFhMzRjY2E4NGI3ZWZjMzBhMDRmOGVkMGFlZmE1MmIzZGNhNzEzNWE1ZDQzNDM3OGE4YzI4MDQ1NTllNGZiIiwicHJlY2VkaW5nQ29udGV4dExpbmVDb3VudCI6MywicHJlY2VkaW5nQ29udGV4dEhhc2giOiIxOWU3NjM1NWNmMTE5OTQ4MGMzZjk5ODM2YjE0ODZjODQxZDg5MWZhYWQ5MDRjNjA4ZTA5MzAyMmQzNzU3YWVlIiwiZm9sbG93aW5nQ29udGV4dExpbmVDb3VudCI6MywiZm9sbG93aW5nQ29udGV4dEhhc2giOiJkODJkZGZhZmJiMTU0NzFhNDQ3NzU4YjJhNTRjMDc5NzQ4Njg5YmEyOWM0ZmMwNjg4ZGMzYzg3NmJkOGZlMTcwIn0"
         },
         {
           "resource": "repo://src/config_watch.rs#L54-L74",
           "version": "repo-lines-v1:sha256:e1c5a3b6eb06c5bf32c7c814c75a97ed0aedf8c45c474dcd004bb5f4c4023d58:eyJzZWxlY3RlZExpbmVDb3VudCI6MjEsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjI1Y2Y1ZmMyM2YyYTZkMjNjM2VhNTFlNzJmNWU4OTZjNzU1NWZiYWEwNWNkNWIxZWExODJiZjk2MThhOGU4MWIiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImQ4ODU5MjEwZWJjYjEyY2Y2OGM2MTYyMjJmYzU2MGMxZjY4ZTJmYTMxZjYzMDQ5M2RmODA5NTg4ODdkNTdhZjkiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjowLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImUzYjBjNDQyOThmYzFjMTQ5YWZiZjRjODk5NmZiOTI0MjdhZTQxZTQ2NDliOTM0Y2E0OTU5OTFiNzg1MmI4NTUifQ"
         },
         {
-          "resource": "repo://src/history.rs#L543-L852",
-          "version": "repo-lines-v1:sha256:a4d3c1973b9e386233ba1511ab6e3adcf322e06bb30f97f8fb8f6a96258e6360:eyJzZWxlY3RlZExpbmVDb3VudCI6MzEwLCJmaXJzdFNlbGVjdGVkTGluZUhhc2giOiIyNWNmNWZjMjNmMmE2ZDIzYzNlYTUxZTcyZjVlODk2Yzc1NTVmYmFhMDVjZDViMWVhMTgyYmY5NjE4YThlODFiIiwibGFzdFNlbGVjdGVkTGluZUhhc2giOiI0MTJjYTM0NWNjZjc1YmY5YzA4MDZiY2U2OTViZThkZTgwOGI3OTk4NDI1MWE3YTU0ZDIwMmNmNjEwMWRkNDUxIiwicHJlY2VkaW5nQ29udGV4dExpbmVDb3VudCI6MywicHJlY2VkaW5nQ29udGV4dEhhc2giOiI2MDYzYzRkMWFmOGNkODliYjBiZGQ3NjU3ZmZjYmY2ODA2YTYxMGQ3OWQ2YzhkNGMzMDdjMjQxZGYzOTAwNTIyIiwiZm9sbG93aW5nQ29udGV4dExpbmVDb3VudCI6MCwiZm9sbG93aW5nQ29udGV4dEhhc2giOiJlM2IwYzQ0Mjk4ZmMxYzE0OWFmYmY0Yzg5OTZmYjkyNDI3YWU0MWU0NjQ5YjkzNGNhNDk1OTkxYjc4NTJiODU1In0"
+          "resource": "repo://src/history.rs#L563-L872",
+          "version": "repo-lines-v1:sha256:5a7faac412b418e8d4aca9502c8b2c44ebacbab61bb9be1912c746fbcfa3ea92:eyJzZWxlY3RlZExpbmVDb3VudCI6MzEwLCJmaXJzdFNlbGVjdGVkTGluZUhhc2giOiI0ZDJkMDQ1MDNjYWJjZWRhODhiZTgwM2NmMjYxNGExZTI1MWJjZjQwZDE3YTc5ZGRlZmI3MWExMjlmMDNkMjNmIiwibGFzdFNlbGVjdGVkTGluZUhhc2giOiIwMWJhNDcxOWM4MGI2ZmU5MTFiMDkxYTdjMDUxMjRiNjRlZWVjZTk2NGUwOWMwNThlZjhmOTgwNWRhY2E1NDZiIiwicHJlY2VkaW5nQ29udGV4dExpbmVDb3VudCI6MywicHJlY2VkaW5nQ29udGV4dEhhc2giOiIzNmU0YjEyNGNhM2YzZjM2MGQxOWQyMTgwMjQxZDk2NzAyMjE4OWUzOTljZmY3ZTM4M2E0YTVmNmY2MmJmZDliIiwiZm9sbG93aW5nQ29udGV4dExpbmVDb3VudCI6MywiZm9sbG93aW5nQ29udGV4dEhhc2giOiI5MmY2MjMxNmZlZTgwNWMwNjg2MzgxZWUyODkwNzEzNWM2ZjE5NTc0ZTlmNzY0YWE3NWMzNDNhOTMxZDJlOTRmIn0"
         },
         {
-          "resource": "repo://src/main.rs#L207-L229",
+          "resource": "repo://src/main.rs#L208-L230",
           "version": "repo-lines-v1:sha256:6f0ef80029cd8b3a3ed5570438cff5846f3f70c6e0fe4866184c8210aac3873f:eyJzZWxlY3RlZExpbmVDb3VudCI6MjMsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjI1Y2Y1ZmMyM2YyYTZkMjNjM2VhNTFlNzJmNWU4OTZjNzU1NWZiYWEwNWNkNWIxZWExODJiZjk2MThhOGU4MWIiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjM1ZjZmNDY2ZTU5Y2ZjMzA3MDNjNDVmNzJhZjY1ZGU1NGFjMzYzZDM1NGNjMWEzMzMwMzUwYWNkNDUyNDM0YzQiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjowLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImUzYjBjNDQyOThmYzFjMTQ5YWZiZjRjODk5NmZiOTI0MjdhZTQxZTQ2NDliOTM0Y2E0OTU5OTFiNzg1MmI4NTUifQ"
         },
+        {
+          "resource": "repo://src/transcribe.rs#L299-L520",
+          "version": "repo-lines-v1:sha256:eb93f4822bab56a7517cacc45065b07127faad5b517122ba2c16df003643152f:eyJzZWxlY3RlZExpbmVDb3VudCI6MjIyLCJmaXJzdFNlbGVjdGVkTGluZUhhc2giOiIyNWNmNWZjMjNmMmE2ZDIzYzNlYTUxZTcyZjVlODk2Yzc1NTVmYmFhMDVjZDViMWVhMTgyYmY5NjE4YThlODFiIiwibGFzdFNlbGVjdGVkTGluZUhhc2giOiI0MTJjYTM0NWNjZjc1YmY5YzA4MDZiY2U2OTViZThkZTgwOGI3OTk4NDI1MWE3YTU0ZDIwMmNmNjEwMWRkNDUxIiwicHJlY2VkaW5nQ29udGV4dExpbmVDb3VudCI6MywicHJlY2VkaW5nQ29udGV4dEhhc2giOiJmYWY3ZThkZTJiMTIxN2YzNzM1YTc0YTJkMmNlNjAxZmZhODFjNmMwYWRlNmU0OTJhNmJlYThjZjU2ZDhmNGVhIiwiZm9sbG93aW5nQ29udGV4dExpbmVDb3VudCI6MCwiZm9sbG93aW5nQ29udGV4dEhhc2giOiJlM2IwYzQ0Mjk4ZmMxYzE0OWFmYmY0Yzg5OTZmYjkyNDI3YWU0MWU0NjQ5YjkzNGNhNDk1OTkxYjc4NTJiODU1In0"
+        },
         {
           "resource": "repo://src/ui.rs#L579-L756",
           "version": "repo-lines-v1:sha256:e85913975a63028c40bc84fd3c1b842d226f277fb56a1588907f49fff7f23119:eyJzZWxlY3RlZExpbmVDb3VudCI6MTc4LCJmaXJzdFNlbGVjdGVkTGluZUhhc2giOiIyNWNmNWZjMjNmMmE2ZDIzYzNlYTUxZTcyZjVlODk2Yzc1NTVmYmFhMDVjZDViMWVhMTgyYmY5NjE4YThlODFiIiwibGFzdFNlbGVjdGVkTGluZUhhc2giOiI0MTJjYTM0NWNjZjc1YmY5YzA4MDZiY2U2OTViZThkZTgwOGI3OTk4NDI1MWE3YTU0ZDIwMmNmNjEwMWRkNDUxIiwicHJlY2VkaW5nQ29udGV4dExpbmVDb3VudCI6MywicHJlY2VkaW5nQ29udGV4dEhhc2giOiIwNmE2YTY2NDFiOGNiOGMwMjgxMWEzMzBhMmYyYTMyMzRkOTk5Zjg4YWJlYWNhYzEyYThlYTU1N2ZjZWQ4MzQzIiwiZm9sbG93aW5nQ29udGV4dExpbmVDb3VudCI6MCwiZm9sbG93aW5nQ29udGV4dEhhc2giOiJlM2IwYzQ0Mjk4ZmMxYzE0OWFmYmY0Yzg5OTZmYjkyNDI3YWU0MWU0NjQ5YjkzNGNhNDk1OTkxYjc4NTJiODU1In0"
@@ -131,8 +135,8 @@
           "version": "repo-lines-v1:sha256:c5f71abad037e96401b479b47443f71e3ace10e2d54fca94fb9318d2f14bbd6b:eyJzZWxlY3RlZExpbmVDb3VudCI6OTMsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjI1Y2Y1ZmMyM2YyYTZkMjNjM2VhNTFlNzJmNWU4OTZjNzU1NWZiYWEwNWNkNWIxZWExODJiZjk2MThhOGU4MWIiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjhmZDM2OGIzODI2OTRjMDYxZWMzMDkyOTYzNjNkYTBlYWNkNzI3NDlmZTQzYmFiNTM4ZWMyMjlhMmM5MDE0NTkiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjowLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImUzYjBjNDQyOThmYzFjMTQ5YWZiZjRjODk5NmZiOTI0MjdhZTQxZTQ2NDliOTM0Y2E0OTU5OTFiNzg1MmI4NTUifQ"
         },
         {
-          "resource": "repo://src/whatsapp.rs#L1140-L1415",
-          "version": "repo-lines-v1:sha256:2057c5d8d4b99b0ca63b7bbd08e92640033c0d0ce471759da73c510b4d3af30a:eyJzZWxlY3RlZExpbmVDb3VudCI6Mjc2LCJmaXJzdFNlbGVjdGVkTGluZUhhc2giOiIyNWNmNWZjMjNmMmE2ZDIzYzNlYTUxZTcyZjVlODk2Yzc1NTVmYmFhMDVjZDViMWVhMTgyYmY5NjE4YThlODFiIiwibGFzdFNlbGVjdGVkTGluZUhhc2giOiI0MTJjYTM0NWNjZjc1YmY5YzA4MDZiY2U2OTViZThkZTgwOGI3OTk4NDI1MWE3YTU0ZDIwMmNmNjEwMWRkNDUxIiwicHJlY2VkaW5nQ29udGV4dExpbmVDb3VudCI6MywicHJlY2VkaW5nQ29udGV4dEhhc2giOiJkNmQxM2RhNTNkZGY1NGUwOWE5ZmIzNzc4NzQxNGE5NjE0NmQ0NzFjNWViYzZjZTI2Y2Y5YTM4NGYxODc3OGYxIiwiZm9sbG93aW5nQ29udGV4dExpbmVDb3VudCI6MCwiZm9sbG93aW5nQ29udGV4dEhhc2giOiJlM2IwYzQ0Mjk4ZmMxYzE0OWFmYmY0Yzg5OTZmYjkyNDI3YWU0MWU0NjQ5YjkzNGNhNDk1OTkxYjc4NTJiODU1In0"
+          "resource": "repo://src/whatsapp.rs#L1216-L1492",
+          "version": "repo-lines-v1:sha256:41d01ec83c54ff5591cb09d8237121d972db1aa080e4f8aa6cae6678d9eea12b:eyJzZWxlY3RlZExpbmVDb3VudCI6Mjc3LCJmaXJzdFNlbGVjdGVkTGluZUhhc2giOiIxZTBhZjNmZDRhNGMzYTFkNDdjN2Y0NzBlZDBkODZkOTBkOWRlMjEwYTVlYjE0NTVmYmQ5NDQ4MmE0NzY1YjcyIiwibGFzdFNlbGVjdGVkTGluZUhhc2giOiI5MmI1ZWRmNTk0OWNmZWE2NzgyMmNmNjkxMTMyMzYzZDY4ZDZmMWUxNjlmNWUwZDRmN2MxNGU1ZmExZTZkNGU1IiwicHJlY2VkaW5nQ29udGV4dExpbmVDb3VudCI6MywicHJlY2VkaW5nQ29udGV4dEhhc2giOiJhNTIxNjA0YjM1ZmM0MGFmMDBjYmE0ZWVhMmZkNWUyYzNhZjE4NjNiMzkxMTg1ZDdiZjRiYjA5ZTcyZDBjOGIzIiwiZm9sbG93aW5nQ29udGV4dExpbmVDb3VudCI6MywiZm9sbG93aW5nQ29udGV4dEhhc2giOiI2N2IyMGRmN2RkMmVlZjRhMjFmNmM1YjYxMWM5ZTcxZTAzMmI2Y2E3NmE2NTQ5ZDZjNzkwYjVhYTc1ZmJiODEzIn0"
         }
       ]
     },
@@ -141,7 +145,7 @@
       "statement": "AI tests check log records and append-only writes, using a per-process temporary file for the append test.",
       "evidence": [
         {
-          "resource": "repo://src/ai.rs#L825-L873",
+          "resource": "repo://src/ai.rs#L829-L877",
           "version": "repo-lines-v1:sha256:28e9f0691f3d1a68739be49bd3325b406b41e5a83c60e4c0c671eed81f91a958:eyJzZWxlY3RlZExpbmVDb3VudCI6NDksImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjkyYjVlZGY1OTQ5Y2ZlYTY3ODIyY2Y2OTExMzIzNjNkNjhkNmYxZTE2OWY1ZTBkNGY3YzE0ZTVmYTFlNmQ0ZTUiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjAxYmE0NzE5YzgwYjZmZTkxMWIwOTFhN2MwNTEyNGI2NGVlZWNlOTY0ZTA5YzA1OGVmOGY5ODA1ZGFjYTU0NmIiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImIwOTRhNzlkYTZkYmY5M2I2MzljZGM3ZTA3OGUxYWI3YzJkZmI5YWE2MzE1YjMyN2YzOWM1NjA0NWU1MjZkNWQiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjczZDVlZjE2ZDc2NzNhODk3ZWNhNGZmY2E5YzQ2NjExNmY2NGMxMzQxMGU4YjljMGM2YmJmMWNmNTAyMjY5OWQifQ"
         }
       ]
@@ -161,11 +165,11 @@
       "statement": "Config prompt-file tests write per-process scratch files under the target directory and remove them after reading.",
       "evidence": [
         {
-          "resource": "repo://src/config.rs#L357-L383",
+          "resource": "repo://src/config.rs#L417-L443",
           "version": "repo-lines-v1:sha256:074ff1586edc8fcdfee555ac451b2810d5deb73073328d3f7f989ee02aca2fb4:eyJzZWxlY3RlZExpbmVDb3VudCI6MjcsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjdmY2Y5NzI1NmRkZTlkNzYwNGJkZmZmYTMyZjVjOTI5ZGM1ZjIzN2UxNmZhNThhMWEzMmNiMTQyMWY1ZTY4NmEiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImNhYWZmODY5NzZkYWEyYzYwYjNlZmU4MDE2MTQxZDI5ODM3N2NhNTI2OGQ0ZWJlNTk1ZWVhZDVkYzQyMzJiNmIiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjU1NWQ1YWZiMzJlOWU4YmJlMjI3ZGY0NzAyMGQwNzU4YzcyZjc1Yjk0ZTBjOTVlODg2MGQyOTFlNmQ2MWJhMWMifQ"
         },
         {
-          "resource": "repo://src/config.rs#L426-L462",
+          "resource": "repo://src/config.rs#L486-L522",
           "version": "repo-lines-v1:sha256:1860a1e1d35b88e371e7056a3261ef07ddfe79442e512b5a65c493d79e7c6a47:eyJzZWxlY3RlZExpbmVDb3VudCI6MzcsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjkyYjVlZGY1OTQ5Y2ZlYTY3ODIyY2Y2OTExMzIzNjNkNjhkNmYxZTE2OWY1ZTBkNGY3YzE0ZTVmYTFlNmQ0ZTUiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjEzZWE5YzkzNGFmY2Y5Yzc5NDE0NjE0MTZhODY4ODAxNjNjYzc5NGZkZjQ4ZDk3NWVjNTAxZDE5MWJhY2UxMDkiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjMzMDUyMTUxNzFiNzNmMmE3MTliYTZkYzhmZTYzMGM1ZGUyNDExN2JmYzIwNTU3NmZjNjhmODY1NDg1YzQ0YzUifQ"
         }
       ]
@@ -175,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#L1143-L1183",
+          "resource": "repo://src/whatsapp.rs#L1218-L1258",
           "version": "repo-lines-v1:sha256:041308c138b61aed5f622ca006de95e62d877c7ece53ef7e68db634046e5e877:eyJzZWxlY3RlZExpbmVDb3VudCI6NDEsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjAxYmE0NzE5YzgwYjZmZTkxMWIwOTFhN2MwNTEyNGI2NGVlZWNlOTY0ZTA5YzA1OGVmOGY5ODA1ZGFjYTU0NmIiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImQxMzYwZTc4OTM3Mjc0OTA3NjlhYzM4ZTYxMjRhNzZhMGIyOGZkZGJiZjFlYTM1ZDJiOTk4MDYzMjNiMWZjODUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImNjZWY2YTcxMDEyOTdmNmI5ZGNmZDFhOTE1YjE4Y2QyY2EzZTcyMmY5NWI2YzA2YWY5ZDYyNmZmYWNmZTFiYTYifQ"
         }
       ]
@@ -193,6 +197,6 @@
   ],
   "verification": {
     "by": "openwiki/0.7.2",
-    "at": "2026-10-23T21:26:24.156Z"
+    "at": "2026-10-24T16:39:50.206Z"
   }
 }

+ 54 - 50
openwiki/.claims/workflows/ai-autoreply.json

@@ -1,6 +1,6 @@
 {
   "schemaVersion": 1,
-  "pageVersion": "sha256:976c69745a1473cf4fe63a8e29ad6081b01d9823cb1d12071204bb9bcd039b6f",
+  "pageVersion": "sha256:51f277c84ac8b779ca6c470fcddd420be0463aa6c621620207ab047c32ec60ae",
   "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#L1101-L1137",
+          "resource": "repo://src/app.rs#L1342-L1378",
           "version": "repo-lines-v1:sha256:0186ad31b1302456cd660841125a254c3e2f46e2074372b97855046cd705d5de:eyJzZWxlY3RlZExpbmVDb3VudCI6MzcsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjBjMWZhZjI0NjQzNTZlZjY0NzAyNjI4YzM0MGYyOGNhZDljMjY3OGYxMTViNTAxNDZkMGMyNTBmNzhmZDk2NWMiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjkzNzNhMTgxNzI2YzQ3ZjdjNzlmOTI3ZDI2NzhhYzQ3NjYzY2VhMzI1OGYxOGEwNTM0ZTg2NDVlYmFlOTlkMGUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImUzZWNmNGIzNzRlN2M0ODFlZjUzODQ1Y2Y1YjlmNDI5MDk3NmQxYTlmNDlkOWY5NWJjNjIxNWEyY2VhMDYyOTAifQ"
         },
         {
-          "resource": "repo://src/app.rs#L1544-L1557",
+          "resource": "repo://src/app.rs#L1889-L1902",
           "version": "repo-lines-v1:sha256:76ea2311dd543d33bf1c52b4cee8997184cb67643a8ba3b2f715abf2fea34667:eyJzZWxlY3RlZExpbmVDb3VudCI6MTQsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjA0NDI0MGFiOGNkMTM1NDE2ZTQ5MjMyYTEzMGIzYzlkYmI2NWZmYzg0OGNjYzY3OWJhYTQxYWQzNjY4ZTA3MjQiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjE5ZTc2MzU1Y2YxMTk5NDgwYzNmOTk4MzZiMTQ4NmM4NDFkODkxZmFhZDkwNGM2MDhlMDkzMDIyZDM3NTdhZWUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjhkYTViNjcyY2ViZDYwMjY1YTM5NmE4NTAxZmMwMmEyYzg2YTZkNWJkYzMyNzRlMGQyZDA5ZDYwMTJhY2Q1ZTgifQ"
         }
       ]
@@ -41,16 +41,16 @@
       "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#L450-L483",
-          "version": "repo-lines-v1:sha256:6c4f5e0458f8089356df9af12ebae1223140c0e16a7db40e5cd5c048fe12b1a0:eyJzZWxlY3RlZExpbmVDb3VudCI6MzQsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6Ijk2ZjNjMTFjYTY5OWY4OTA5MTUwMjAyYzhkZTVlMjdlZjAwNTIyOGRmZmNjMzAzOTlmNDg4OTdmYzdhOTVlMTEiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjdmYzAyNDRhNGJkOTI4YWRmMDE3YjljOWQ0ODIxYjU4NjgwZGRhMmI2YTBhZDEyYWU2MWVhNDBlOWZkMjM3OGMiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImI1YjUzYzQyYTJjZjU2YmRlZWIxOWJhYTZhZjdhYTQzZmRkNjFmN2VhNTQzOGVjOWRmYTAwYjc0Nzg3NWM1ZGQifQ"
+          "resource": "repo://src/app.rs#L485-L532",
+          "version": "repo-lines-v1:sha256:25898277ac10619d92709af2d4e7204132d29205e0a7f7d7888b7f4f15606eed:eyJzZWxlY3RlZExpbmVDb3VudCI6NDgsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjBlYWIwYmIzYTMyYTBlZGUzMGY0ZTk1MmQ1MjcxYzRhMTgwMTZjMGU4Y2M4N2VhYmE1ODA1OTYyZDNjOGE4NTYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjgyODRmZjk5YWRlYTg2MmRlOTFhNTQwZTNjOTcwNzc4NWU0ODY1ODg2YjZlOTFmMzFiNzdmNzdlOGQzNzI4OGMiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImI1YjUzYzQyYTJjZjU2YmRlZWIxOWJhYTZhZjdhYTQzZmRkNjFmN2VhNTQzOGVjOWRmYTAwYjc0Nzg3NWM1ZGQifQ"
         },
         {
-          "resource": "repo://src/app.rs#L638-L644",
-          "version": "repo-lines-v1:sha256:9190cc2d950eb45e132eae01c1ae01b5fb4af82470bb42fae8b4e0227a315710:eyJzZWxlY3RlZExpbmVDb3VudCI6NywiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiYmMzZGZhMjZjNzdiNTNkZDcyZWZmZGI1MmEyNWFlNGY5YzMwNTdiZTk2NWI0MzRlMTMyOWIwMTM2MGZmNGViNyIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDg5M2Y0MGUzNzgxZWI4OTZjODJkMDQ2YTA4MDY3YTExZTM1Y2RmMmYxZGZhZjI2ZmVhOTBiOGRiNjY3NjQxMyIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiMTYyZWU1N2U0MWE4YzIwYWUwMTY0ZGYyNjE0MzlkY2M0OTlkMzUyMDgzNDE1MTUxNmQwNDdlOGRlYWU4YTQ2NiIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiOTEyYzE5ODI4NjExZDJhZjdmZWFmNjQyN2ZmNzE0MjFiN2JhMTU4MzM5OWY0YWU5ODBiMzJkNThlNDUzYTJiYSJ9"
+          "resource": "repo://src/app.rs#L858-L860",
+          "version": "repo-lines-v1:sha256:2f717e0ab27c971d848f3c4b787b63e1843cdddb271adaa95d5811bab8ccb1c6:eyJzZWxlY3RlZExpbmVDb3VudCI6MywiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiZjZkNGY2ZDY0NTVjYjVlMWJiMTEwOTU0NzU5ZjliOTU2YzhmMTU5YTE1NTExZjVkODZhZTZhMGUwMGMyMTQzMiIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDg5M2Y0MGUzNzgxZWI4OTZjODJkMDQ2YTA4MDY3YTExZTM1Y2RmMmYxZGZhZjI2ZmVhOTBiOGRiNjY3NjQxMyIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiYWE0OGNmOWNjZDU2MDljYjc0MmQxYjNkMmY5MjIyNmYzZjQ2YjU4MWE3Zjk5YWM5N2Q4NDdmYTBjOWQ5MWI3OSIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiOTEyYzE5ODI4NjExZDJhZjdmZWFmNjQyN2ZmNzE0MjFiN2JhMTU4MzM5OWY0YWU5ODBiMzJkNThlNDUzYTJiYSJ9"
         },
         {
-          "resource": "repo://src/app.rs#L745-L751",
-          "version": "repo-lines-v1:sha256:8954f8190e201652fd68eb454a728e5e4173257eddebb901b8a0b8d4c773564e:eyJzZWxlY3RlZExpbmVDb3VudCI6NywiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiODA1ZTgyYjdjMjlkNzVkZDI3YmZhYTZiYjkxMzZiOTMwMjQyYWEzYjJkNmM5NjI4ZWVmMmY3ZjIxNzdlMzhhNiIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDg5M2Y0MGUzNzgxZWI4OTZjODJkMDQ2YTA4MDY3YTExZTM1Y2RmMmYxZGZhZjI2ZmVhOTBiOGRiNjY3NjQxMyIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiMjRmNGQ3NzU2OTA1OWI4NjMyNGU2YTVmNGY2YWU4YWI4NjM4YmUzNTY5YjM5MjRlZDJkOWJiZjY0MzczMjUyNCIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiYTZlYTYzN2IyZDc3YWVjZjY1Y2EyOWFjNTgzOGQ4NmRmNGEwODM4NTdmMDQwM2U5OWM2NWU3NzQyMzI5NDMwYyJ9"
+          "resource": "repo://src/app.rs#L987-L989",
+          "version": "repo-lines-v1:sha256:a9005f55bfc9d8f0fe3bcd2d8af41d7200648c575a982bd6184e0a709f666f1d:eyJzZWxlY3RlZExpbmVDb3VudCI6MywiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiY2ZhYjYzNzAzMWNiMGJiMTEzOGI0YjQ2OTE3MDhiNjRhNzA3MTQ3NTZhMWExYTQzZTE0NGZmY2MwY2E2NTllMCIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDg5M2Y0MGUzNzgxZWI4OTZjODJkMDQ2YTA4MDY3YTExZTM1Y2RmMmYxZGZhZjI2ZmVhOTBiOGRiNjY3NjQxMyIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiYTZjZWVhMWQ3YmU1MjdlYWYxYTAzZTNhYWU0MjQwYjg3ZWQyZDg3NzI1YWIxNzQyZmNlMjRmZjczNTE2ZTllMiIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiYTZlYTYzN2IyZDc3YWVjZjY1Y2EyOWFjNTgzOGQ4NmRmNGEwODM4NTdmMDQwM2U5OWM2NWU3NzQyMzI5NDMwYyJ9"
         }
       ]
     },
@@ -87,16 +87,16 @@
       "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#L293-L317",
-          "version": "repo-lines-v1:sha256:bce7d6f44230304cb06893520f9fcf066f4371503113d1fed37a26d6019c2cbc:eyJzZWxlY3RlZExpbmVDb3VudCI6MjUsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjVkOGIyMTkxMzczNzkzN2Y5NWE1M2YzYzg2ODM3Y2JhZmY3MWVmZDM0MjhlMDM3NGVkNGUxZGUzODg3Mzg1YzgiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImEwOTM2MDA2NDY1Y2FkNmM5ZDNiZjBlYjFkNWUxZDUzYTEyMDdhYjdiZTBhMmU0YjA3NDhjZjJhNGRiNTExMTIiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjVlYjYyZTI2M2M3OTNlYjM0MWRiYjMxMDMxMTFkNWNjYzM5NmFhNjZiZGFjNGU0MmUwNDgzMTJiMzNkNjEzOTIifQ"
+          "resource": "repo://src/app.rs#L324-L332",
+          "version": "repo-lines-v1:sha256:00767085b96f8924c277929b72de9745e84060a578a404e5382f4fe6e07a3b7b:eyJzZWxlY3RlZExpbmVDb3VudCI6OSwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiNWQ4YjIxOTEzNzM3OTM3Zjk1YTUzZjNjODY4MzdjYmFmZjcxZWZkMzQyOGUwMzc0ZWQ0ZTFkZTM4ODczODVjOCIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNTUyMmM0MmNkMTljNmQwM2Q4MDhjNDUyNzIwNTUyODI1YjdlYTFjNWI0OTFiMjE4NTExY2FjNzQxNWNmOWQ5YiIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiYTA5MzYwMDY0NjVjYWQ2YzlkM2JmMGViMWQ1ZTFkNTNhMTIwN2FiN2JlMGEyZTRiMDc0OGNmMmE0ZGI1MTExMiIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiZmY3ZThiZWFmYzVhNjZlNTc3ZDk0MmMyNWZiNWYzOWE0ZTRjZGRlMTJkODg3NjBlZmM4ZTE1Y2U4OTEyOWEwNiJ9"
         },
         {
-          "resource": "repo://src/app.rs#L485-L493",
-          "version": "repo-lines-v1:sha256:df493befb56dd10251865172577fc5c0bae10fe73f070b409398d5734657fd5f:eyJzZWxlY3RlZExpbmVDb3VudCI6OSwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiMjVhYmM1NWJiNzAyNjM3YTU5MGU3ZjU3ZjVlNGQ1ZDI3MTc5NmYzNmY4YWQxNzM5ODI3NWZlZThlOWY3ODM4NCIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDg5M2Y0MGUzNzgxZWI4OTZjODJkMDQ2YTA4MDY3YTExZTM1Y2RmMmYxZGZhZjI2ZmVhOTBiOGRiNjY3NjQxMyIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiOTk5YmRlZmVlYWNhNjM0MGUwMjkwZGM4NmU4ODljODlkNWUxODlhYTViYTQ0YjU4NWIzYjcwNzM0ZjRhNjJkOCIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiMGZkOTUxZjMzMzEyYjBkYjE5OTA3M2NkMjM0MDgxYzczMzI0MjFmNjBmNGM2ZDgwMDdiZTZiMzVjMzA4NjE0ZSJ9"
+          "resource": "repo://src/app.rs#L539-L542",
+          "version": "repo-lines-v1:sha256:dd72c631d7b9707b384154ff042bea6cc03745c04b4c6aa82dd6b8d77676a250:eyJzZWxlY3RlZExpbmVDb3VudCI6NCwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiODgxNjNhYjZmYzg4NDRiMTBjZWNhZTQ3YjVlYWY5YmYzNjg5NGRjMDk0N2IzYjAxNmRkYmRlODI5MTc2YjNiOSIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDg5M2Y0MGUzNzgxZWI4OTZjODJkMDQ2YTA4MDY3YTExZTM1Y2RmMmYxZGZhZjI2ZmVhOTBiOGRiNjY3NjQxMyIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiM2ZiYTBkNGUwM2NhZGQzMTgwZmNhZDlhYzRiZmZlZGNhOTU5ZTY1MDUwYjI3YWFmYzA1Mjc2MjUxMDE4MzI4NCIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiMGZkOTUxZjMzMzEyYjBkYjE5OTA3M2NkMjM0MDgxYzczMzI0MjFmNjBmNGM2ZDgwMDdiZTZiMzVjMzA4NjE0ZSJ9"
         },
         {
-          "resource": "repo://src/app.rs#L616-L636",
-          "version": "repo-lines-v1:sha256:9c95e2b6201e18825840eb6e7889d5b1dd8fdc2f20ba7da3471f9d4641456107:eyJzZWxlY3RlZExpbmVDb3VudCI6MjEsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ1NDc2YjZjNjU4MzI4NGViNzUyMTA1Mzg3NGFjZmU3NDY1MGQ0NTJhOTRhMjYxYzg4YTFhYzBjYTYzNWYwY2MiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6Ijk5OWJkZWZlZWFjYTYzNDBlMDI5MGRjODZlODg5Yzg5ZDVlMTg5YWE1YmE0NGI1ODViM2I3MDczNGY0YTYyZDgiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImNmMGY4ZWU3NGMyYmNhMjZlNTlmY2NmYjUzZmQyYWI2OGJjNzIwZTY2MGZiZjM3M2IwZjFhNTY4ZmIyNjBjOTUifQ"
+          "resource": "repo://src/app.rs#L837-L852",
+          "version": "repo-lines-v1:sha256:ca8594a3de60f6943b74187223fab353c0af7a724cf3ef68f1d2f06141510c59:eyJzZWxlY3RlZExpbmVDb3VudCI6MTYsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjNmN2EzMzI2Mzk4MGZiOTQ4OGI5NDcyZTI4ZGU0NjE4ZWVjMzc3MTE2ZmFmZDViODZlNmUzYTZhZGVjYjM2YTEiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjRjZTljYzQ1ZjFjOTkzN2IyMGMyZmJjOTM3YmUxN2QxYzhhMmU0MTdlMjUwMDI2ZjM4MzU3N2MyY2ViNGRkN2UiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImNmMGY4ZWU3NGMyYmNhMjZlNTlmY2NmYjUzZmQyYWI2OGJjNzIwZTY2MGZiZjM3M2IwZjFhNTY4ZmIyNjBjOTUifQ"
         }
       ]
     },
@@ -120,15 +120,19 @@
     },
     {
       "id": "claim_3e40d76522d14d6a8010a842c8b4d312",
-      "statement": "Before a reply, images in the context without a stored or in-progress description, at most the 5 most recent, are downloaded and described in parallel by the vision model; the reply starts after their ImagesDescribed event.",
+      "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#L1372-L1433",
-          "version": "repo-lines-v1:sha256:3fb067a4234c53059cbc431004a05bb2d3c9fba17d27286a1860ffc3bbc41547:eyJzZWxlY3RlZExpbmVDb3VudCI6NjIsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjBmNDNmYzY5OWMxYzQyODRjMjI1MWJmMDdiZDk5ZDcyNjQzMGU4YzhhYWE2YzM2YTQyM2ZkZTQyYzc2Mzk2YmYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImRmYWI3MTBkZGY3ODU5ZTU3NTc2NGE5MTNhMDAyYzc0NTg1ZmZhNzAwMTUxMGNiODgxMWRhODNkMmNmNzRiMzQiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjU0MDAyNGFmZmIxODk1NGEyZjYxODAzYjk5ODBiOGY1NmVkNTlmZjJjN2FiODAyZDU5NDI2NGZiMDFkNmMyOGUifQ"
+          "resource": "repo://src/app.rs#L656-L679",
+          "version": "repo-lines-v1:sha256:a257dd09ef5fad9478ec541c5e5e2b908a11cdc448654986a2150099337f2e63:eyJzZWxlY3RlZExpbmVDb3VudCI6MjQsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjE2N2NhYmY5MDc5Njk4OWRkMTE4ZmVmZjZmYjJhNzI0ZjE5OTAxMjJhOGYzNmQxNmEyM2E5MDU5ZjdiNzEzM2EiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjQxZjkzM2MwZjY2NWU1OWJiMWU0ZTk5NjBmNjA2ZGVjMjM1YjBiYzA2ZGM0ZTUwYmYwZGUzNTI5M2E2Nzg0NmEiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjM5YzEwY2I0YTNhZmI3NWE4N2VmNmE4MmZmYzVhMjBiZDZiN2U0ZmQ0NDRhMmNjMWRlMGZmZDg3MjljODQzZGUifQ"
+        },
+        {
+          "resource": "repo://src/app.rs#L784-L829",
+          "version": "repo-lines-v1:sha256:2da3a90f0f2862e08e4c2b56a2b26f8f018449d7b0112fc46e11f214fbeca676:eyJzZWxlY3RlZExpbmVDb3VudCI6NDYsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImJkYmFiZjFkNjI2OTdjZWE2NTk5ZWQyNjg5MDQzY2RhYzAyOWUxMWQ3NWIwY2U4MTRlYWVkNzhjZTk2Zjg5ZGMiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjNiOWQxNjk5ZDM4YTRkZDQ3ZjgxMGFmMDViYTIzZTM2N2NiNTE5NzljODAxM2Q3YTZmYWQyYjAzY2Q0MzA0MTEiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjBhN2YyNDA4YTExOTJiY2VmZTI0ODJhNTg2MGM1MTVhMmQ4NGIxMzQzZTE1ZDdmMGVmNGRhMTA0YzY1ZTJiM2EifQ"
         },
         {
-          "resource": "repo://src/app.rs#L547-L580",
-          "version": "repo-lines-v1:sha256:9715aab5bcb9a42cb7f22b16e2ae0f531098ff6d6be6ca1ed6da8c95353afde8:eyJzZWxlY3RlZExpbmVDb3VudCI6MzQsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjRmODNjYmFiZjFkYzg0MDU0ZTM0OWEyOWI5M2QzODU0NTQ3NGZjYjkzMDU0ZjNkOTYzMDg4MWQxODNjZWI0MzciLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjI4ZTNjM2Y4MmI0ZjZmMzU0Mzg1NWNmMjZkNjljZmI0M2NjN2RmMTgxYzE5N2NmNDI0YmJhZjM0ODExZDA5M2UiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImUzZjUxYTZkNTgwMzYzOTIyNGZmZWQwOTU0MTEyNTZhNzhlMzA2NTU5NDZjZWY1ZWFmMzlmZjJkNDExMGM1MjIifQ"
+          "resource": "repo://src/app.rs#L888-L918",
+          "version": "repo-lines-v1:sha256:a6beeca1efcc61a62e317b875ec0455fe4adfd64a193fd9393c5b484952ab986:eyJzZWxlY3RlZExpbmVDb3VudCI6MzEsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImNlNGZiYTJmZGY0ZTk4MmNjZmE1ZThkYzBmOTA1ZWQ2NjVhY2E1YzgxZWZiZjRjNDNlMjAwMGQ5YjBlYTBhN2QiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImI2MmYxYzgxNDE1YjNjNTQ5ZmRlNjE3MThkZDRiOGQ0Nzg2MzU5MjEyNWI1NzEwMDZkODhmODkyYzc3NDgxNjUiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjIyMGUyNjkzMzFhZTU2NTJiYTMxNWViYTdmYzRlNWMxNWFhNDlhMzdiMjE2NGY3NjRlYmNiYTA1ODE5ZGMyNDAifQ"
         }
       ]
     },
@@ -137,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#L820-L882",
+          "resource": "repo://src/app.rs#L1058-L1120",
           "version": "repo-lines-v1:sha256:dc263d71dc225c968c33c780874494b82440bfd214546483d0aef9fd6a02bf46:eyJzZWxlY3RlZExpbmVDb3VudCI6NjMsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjBkNzQ1Mzc3ODY0MzI0MmZhZDE3MGZhNjcxNTUxYTJiYjI4ODM4Y2M2NmQ5ZjMwZWNkODhjZTNmNjFhYjRlMGEiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6Ijk5OWJkZWZlZWFjYTYzNDBlMDI5MGRjODZlODg5Yzg5ZDVlMTg5YWE1YmE0NGI1ODViM2I3MDczNGY0YTYyZDgiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjYzYjQ0YTU2ODZkYjM0MjNlMDVmOGMxOWMwYmMyNzlmMDA1NTE4NDJmMzgwZjIxMTM1ZTkzMTZhY2IwNmQ5ODEifQ"
         }
       ]
@@ -147,15 +151,15 @@
       "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#L1340-L1350",
+          "resource": "repo://src/app.rs#L1581-L1591",
           "version": "repo-lines-v1:sha256:be3fc24d7db22727c3dddd8c5116b8fd64a13c8a473f99818b61f3bb6ac76204:eyJzZWxlY3RlZExpbmVDb3VudCI6MTEsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImMxMmIwOGNlNWY0N2RmY2RjMTQ1MTA4ZDFkNmQ2NzhiYTllNDhjZjBmMGY5YzdiYTBhNzMxYTkzMzFjYjc2ZDMiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjRjNjg0ZTk4OWVmNzk4ODJiNmIyOTMwNDdiMTJiNDg4YjkwNjlhYmUzMzc2ZTVhNTNhOGEyZTI5YjY4ODcyMTAiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjA1ZDJlYmFjZThlMTE0NjAzZTYyZDc1YjVmNmRjODFjMzJiMzgzMmI4YjBiMGUzZDVhZDY0OTMyMDQ4YTZkMzgifQ"
         },
         {
-          "resource": "repo://src/app.rs#L485-L493",
+          "resource": "repo://src/app.rs#L534-L542",
           "version": "repo-lines-v1:sha256:df493befb56dd10251865172577fc5c0bae10fe73f070b409398d5734657fd5f:eyJzZWxlY3RlZExpbmVDb3VudCI6OSwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiMjVhYmM1NWJiNzAyNjM3YTU5MGU3ZjU3ZjVlNGQ1ZDI3MTc5NmYzNmY4YWQxNzM5ODI3NWZlZThlOWY3ODM4NCIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDg5M2Y0MGUzNzgxZWI4OTZjODJkMDQ2YTA4MDY3YTExZTM1Y2RmMmYxZGZhZjI2ZmVhOTBiOGRiNjY3NjQxMyIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiOTk5YmRlZmVlYWNhNjM0MGUwMjkwZGM4NmU4ODljODlkNWUxODlhYTViYTQ0YjU4NWIzYjcwNzM0ZjRhNjJkOCIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiMGZkOTUxZjMzMzEyYjBkYjE5OTA3M2NkMjM0MDgxYzczMzI0MjFmNjBmNGM2ZDgwMDdiZTZiMzVjMzA4NjE0ZSJ9"
         },
         {
-          "resource": "repo://src/config.rs#L38-L39",
+          "resource": "repo://src/config.rs#L42-L43",
           "version": "repo-lines-v1:sha256:3841a56ef56846ac61df41e175eaa085bd0d61eea8f2b034ebc6e5737f160c6e:eyJzZWxlY3RlZExpbmVDb3VudCI6MiwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiMDZiYjVjMWVhMTg4OTM2NGMyMzE4M2JjYmU1ZDY2YjhmNmMxNTVlMjgxMGRmY2JkZTc4NjY5ZTBkZjhkN2YzYSIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiZDcxNDZlZjZhMzU1NWEzNTk2Y2EyNzZlMDU0MmExN2Y4ZGNmOGMxNDY5MjNlZmZiNGNkOTBlOTZmM2Y1YjRjZiIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiYWNlODhhMjI1Nzc5MzI5MWRhYzNiM2E5YmUxMTVjODQ2MDM2NmU3ZGNlODM5NTNiYzZkNTI5NzdlMDc3NjNhNCIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiZmZlNWFjNDU3ZTA5MTNkNmMzN2UzMDkxZGE4MWNiYzg4MzU0NDRlODk0MjA1Njk1ODQ0NGM4MzkxNWE1M2YxYiJ9"
         }
       ]
@@ -165,12 +169,12 @@
       "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#L534-L568",
-          "version": "repo-lines-v1:sha256:a63345aecce224550feeba0b579240e2e6f55ea65600d2512c4b1e8291f1ed06:eyJzZWxlY3RlZExpbmVDb3VudCI6MzUsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjhhMGZkOGQ4YmVjYTA3ZDNkNGZkZDViOGFiMWI4NmQ1Y2MwNGVlYzdkNzY3ZTc1Y2Y3ODgxMDk2OGI2N2Q3NDUiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjE2MmVlNTdlNDFhOGMyMGFlMDE2NGRmMjYxNDM5ZGNjNDk5ZDM1MjA4MzQxNTE1MTZkMDQ3ZThkZWFlOGE0NjYiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjU3ODQ1MTBjNTZmNWE5YTNiMmY0ZTdkNmZjMDFjNGM4NTEyOWJjYzUzM2U1YTZjZTA2MTYyY2FiMmIwNGY4YzIifQ"
+          "resource": "repo://src/app.rs#L634-L679",
+          "version": "repo-lines-v1:sha256:15f93324b5c3304cece9ae39892bf0620866af782fe276ac9cb827e11f6c89cb:eyJzZWxlY3RlZExpbmVDb3VudCI6NDYsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjIxMGU4Y2Q4NGRkNGVlMzUzOTU2ODAxODRjZTNlYTEwZDlhMTI3Mjg1Yzk2MDVkY2RmNDJiZjkwOWJkMDZlNjYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjZjMThiNWFhYjhlMGE5MGZhY2E2ZTI5ZmMwYjdmN2NiZDQ0NWFhN2MxZmQyNzA2YjQ4M2ViMDU0ZjJkNjU1ZTkiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjM5YzEwY2I0YTNhZmI3NWE4N2VmNmE4MmZmYzVhMjBiZDZiN2U0ZmQ0NDRhMmNjMWRlMGZmZDg3MjljODQzZGUifQ"
         },
         {
-          "resource": "repo://src/app.rs#L646-L664",
-          "version": "repo-lines-v1:sha256:45691eccde8d968061d2835ed9d342c7665536f57d65eba18d6818bf6cdbaccc:eyJzZWxlY3RlZExpbmVDb3VudCI6MTksImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImRlNjMzYzk0ZmI0NTZhMDkxMTliNmQyOWYyZGMzNjQ2MTI5ZTVlY2M4MDYwMTdlNzVkYzk0MTM3N2RjZDQxNjMiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImJkYTA0OWEwZDljNDFjMmNjZGQ4OWY2MmQ2Zjc1OTA0ODBjNjQyZWI3Mzc5NTcwM2Q0MTQ1Y2I5MzQ2Y2VhZjIiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjNhMzhmYjI3YWI5N2YyMTc1ZGQ0MDc1NDhmMTcyMGUxNzMwYzY4YjA5ZGI1M2MzZTZiZTQ4OGQyYjMzMzMxZmYifQ"
+          "resource": "repo://src/app.rs#L868-L880",
+          "version": "repo-lines-v1:sha256:7ecf503d147e02720d6b1181aff0c941477154a712db724f4f85db692d7aadbd:eyJzZWxlY3RlZExpbmVDb3VudCI6MTMsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImIxNjE1NThmYWRiMWZlYjc4NjlhNTk4NzEzNzMxNTk4YTMyM2Y1NWZiNTFhOGY0NTA3OTAyOTlmZWE5ODljM2UiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjI0M2Y0MzIxMzM1MDhkOTFhODZlMWI0Mjc2Y2YyMjgwN2MyMzhlMDIxZDk3ZTJiNDFmY2U0NDU2YTIzMzZiMzQiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjNhMzhmYjI3YWI5N2YyMTc1ZGQ0MDc1NDhmMTcyMGUxNzMwYzY4YjA5ZGI1M2MzZTZiZTQ4OGQyYjMzMzMxZmYifQ"
         },
         {
           "resource": "repo://src/autoreply.rs#L231-L239",
@@ -180,15 +184,15 @@
     },
     {
       "id": "claim_d8b08d3eeb4d432f82bfd00abc49cbd2",
-      "statement": "On the first OfflineSyncCompleted per process, catch_up prepares one reply without delay for each enabled non-group chat (all chats when the global autoreply is on) whose last message is from the contact, older than app start and within catch_up_seconds, including its offline images, and reports the count in the status.",
+      "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#L684-L743",
-          "version": "repo-lines-v1:sha256:662ddb2a45a183036d34d982acfd7438be6f423e58df87505555df7b3c0a5c12:eyJzZWxlY3RlZExpbmVDb3VudCI6NjAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImM5ZmJjMjYxZDk1MDVkNTBjZTU1MWY4ZDI4OTQ2MTZjMWM2MGY3NTlhZTAwYTE1OWY4Yzg3OGVmMWJjM2Y2ZWEiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6Ijg0ZTkyMmFmNjJhMTJlOTYxYjczZWM0ZGMyMzNjMTk2MDhjNzRmNDU1MzExNzUxOWQzZWU3MjViZDZkNWNkNmQiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6Ijc4OTU5NGE5NzIzN2Y2NjVkMmU0Y2RhMTIyZmY1NTIyNzJjMjVmZTFhMTI0NmZjMGNkNjllMDQ4MjE2ZTNlYWMifQ"
+          "resource": "repo://src/app.rs#L924-L981",
+          "version": "repo-lines-v1:sha256:c43af5df86c402afe0390574efa8d5408228a57e12985a9f712e1691b2f8c63b:eyJzZWxlY3RlZExpbmVDb3VudCI6NTgsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjJiYWY4NjRiNzJmMjM1YjJhNzAxNzllOWRiZTlkYTg1ZmY0NWM1OTg4YjY1MTUzNmQyYjNmMDM0MzRkMTA3YjgiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjJkOWY0YzAxNDlmZTYwZWNjMDY0NDU2Y2VhNTgwNWIyMmI4YjgzZTljYzY0YzVmZTc3YzhjZDNjMzQ5ODg1OWMiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6Ijc4OTU5NGE5NzIzN2Y2NjVkMmU0Y2RhMTIyZmY1NTIyNzJjMjVmZTFhMTI0NmZjMGNkNjllMDQ4MjE2ZTNlYWMifQ"
         },
         {
-          "resource": "repo://src/autoreply.rs#L267-L290",
-          "version": "repo-lines-v1:sha256:07065945215d59bcdce533cf448c284c0de51f32fc4f1311433dbb8a554ba7c4:eyJzZWxlY3RlZExpbmVDb3VudCI6MjQsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjNlZTNjZmNlMWFhMTdjOTk2YzhlZDhiZmVmN2UzODZkZjU1NDZmZjk1ODUyNzUxNTdhMjEwMzI3OGE1NGE0ZjQiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImUwZGY3NGRlMGI2ZGVlYjk5NjI4MGU2YzFiZDRlOGIyNTQ1MDUyODZhNzBhZjZkN2U4MWE0MTRjOWJhYzA3MGEiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjhjYTUwMTNjY2ZiYWFhNGY4YTdiMmRlYTU1MDU2NTRhN2VmN2ExNGVmNTZhZGI3NzEyNGM2Y2NkMWVhMWNjN2UifQ"
+          "resource": "repo://src/autoreply.rs#L278-L290",
+          "version": "repo-lines-v1:sha256:978337272e69a0817dc2c97ec4cf141555c1153662f4d02558b1f314701d2555:eyJzZWxlY3RlZExpbmVDb3VudCI6MTMsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjMwOTdhNjIzNjEzOTc1ZTZhZGVhNzc4Y2ZiZjAyY2RhY2ZmMjIxMzBlZWNlMTA1NGY5MTZiMjY4MmE4MjYyYWMiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjlmOWZkZjU2MjAyM2ZlMGUwZTk0ZDE3YmZiNjg5NzA1YzM4ZGU5YTcyOTE2ZDMzNzhjNzAzNTAwYTFmY2E2MjAiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjhjYTUwMTNjY2ZiYWFhNGY4YTdiMmRlYTU1MDU2NTRhN2VmN2ExNGVmNTZhZGI3NzEyNGM2Y2NkMWVhMWNjN2UifQ"
         }
       ]
     },
@@ -197,7 +201,7 @@
       "statement": "The AI context is loaded with messages_since from local midnight of yesterday, capped at CONTEXT_MESSAGES.",
       "evidence": [
         {
-          "resource": "repo://src/app.rs#L753-L760",
+          "resource": "repo://src/app.rs#L991-L998",
           "version": "repo-lines-v1:sha256:7c77ded5ac6d33c39416f329b08cebb428d4745fd351bf74af8942d0b7a7ca02:eyJzZWxlY3RlZExpbmVDb3VudCI6OCwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiNTUxNGM3ODU1Zjc2MTlkMWNmNzRmYjkxNzViOTNiMDc1ZTAwMGFjNTA0MjFlMWRmMWQ0Nzc4NWQ3NDkxYTdmOSIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDg5M2Y0MGUzNzgxZWI4OTZjODJkMDQ2YTA4MDY3YTExZTM1Y2RmMmYxZGZhZjI2ZmVhOTBiOGRiNjY3NjQxMyIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiMWJhZTkzZGRjNWYwMDFlOWRhMTI5YmY0ODA5MzQ3MGI1ZWQ4ZjNjNzdjYzA2MjQ2YjJjZTkxODdmMTIzNGI0ZCIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiNmZlMTlkZGI5ZjIxNTk1NWQzNzM5NjBlODk1MTI1ZGE0ZmYwYTYwNTAxMGY1ZmVmYWExYTI2ZDE0NWQ4NmNkMiJ9"
         },
         {
@@ -221,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#L1186-L1203",
+          "resource": "repo://src/app.rs#L1427-L1444",
           "version": "repo-lines-v1:sha256:05cde78eab7927a8b5c33ce971f85c0e210ce60936eada88f8c9481585c1519e:eyJzZWxlY3RlZExpbmVDb3VudCI6MTgsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjlhMjY2MjVlMDNiMmE0MDg1NDg0YjQ2NjUzOTllOWRmOWFhZDQzYmVkNGQxNWFkODEwODMzMGIyMDhjNmM5ZTYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6Ijk5OWJkZWZlZWFjYTYzNDBlMDI5MGRjODZlODg5Yzg5ZDVlMTg5YWE1YmE0NGI1ODViM2I3MDczNGY0YTYyZDgiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImU2ZWQ1MmZlYjhjZTliMjNiYmNiYTkwMGFkYjk0MzE3MjA4ODNhZTk0ODY4NmQyNjhlNmRkMjFlNjljYTU3YjkifQ"
         },
         {
@@ -232,15 +236,15 @@
     },
     {
       "id": "claim_fd5c7a184d1e434a85a1c63419bbadce",
-      "statement": "After a reply, if a newer message that passes should_schedule arrived after the answered context, finish_reply schedules one new reply keeping the images received meanwhile.",
+      "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#L1448-L1455",
-          "version": "repo-lines-v1:sha256:b961fc471f97ce4ba30ba2d05c7240d1d8e6038ccae49f417602f58eb534920b:eyJzZWxlY3RlZExpbmVDb3VudCI6OCwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiYzNhMDkyMjQ3MTNmYjQ0MDIzMjJlMGFjODEwMWI3OTI4ZWQwNThlZTExZTU5NmZkYjMwM2NjMGRhOGJiZTc3YyIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDEyY2EzNDVjY2Y3NWJmOWMwODA2YmNlNjk1YmU4ZGU4MDhiNzk5ODQyNTFhN2E1NGQyMDJjZjYxMDFkZDQ1MSIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiMTVlMTI4NTMzNjllMzNkZDlkMmUyZDQxNjFlYmJhNWNjYWMxMDkxYzIwMzg1ZTViMzNlMjkwMGIyNjdlOTVlYiIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiNDlhNTk5ZGY5ZDUwOWVhMzBiNDMzZjkzYjY4ZjE4ZGQ1ODY2ZDVmMGJjMGMzMGEyMTU1OWFkN2U4MWM1ZmMwMCJ9"
+          "resource": "repo://src/app.rs#L1696-L1698",
+          "version": "repo-lines-v1:sha256:f837e4630ef0ee6fe89e07bf810f5851838ae48e227e0659b648d4ea480392fa:eyJzZWxlY3RlZExpbmVDb3VudCI6MywiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiNTk5ZjgxMThmMmE5ODIzYzNmZDQwNDcyYjlhMmUwNDE1ZTRhZGQzMWQ5NTkwNjE0NzY3MzlhYjk4YWIyNTUxMCIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDEyY2EzNDVjY2Y3NWJmOWMwODA2YmNlNjk1YmU4ZGU4MDhiNzk5ODQyNTFhN2E1NGQyMDJjZjYxMDFkZDQ1MSIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiNGQ4NDkzOWY4ODU5NjI0YmY1NGNiOTdjYWJjMDE2ZjRhYmY3OTU0YWUwNGU0MTVmODUzMjQ3ZTRmYWY2Zjc3ZCIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiNDlhNTk5ZGY5ZDUwOWVhMzBiNDMzZjkzYjY4ZjE4ZGQ1ODY2ZDVmMGJjMGMzMGEyMTU1OWFkN2U4MWM1ZmMwMCJ9"
         },
         {
-          "resource": "repo://src/app.rs#L616-L636",
-          "version": "repo-lines-v1:sha256:9c95e2b6201e18825840eb6e7889d5b1dd8fdc2f20ba7da3471f9d4641456107:eyJzZWxlY3RlZExpbmVDb3VudCI6MjEsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ1NDc2YjZjNjU4MzI4NGViNzUyMTA1Mzg3NGFjZmU3NDY1MGQ0NTJhOTRhMjYxYzg4YTFhYzBjYTYzNWYwY2MiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6Ijk5OWJkZWZlZWFjYTYzNDBlMDI5MGRjODZlODg5Yzg5ZDVlMTg5YWE1YmE0NGI1ODViM2I3MDczNGY0YTYyZDgiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImNmMGY4ZWU3NGMyYmNhMjZlNTlmY2NmYjUzZmQyYWI2OGJjNzIwZTY2MGZiZjM3M2IwZjFhNTY4ZmIyNjBjOTUifQ"
+          "resource": "repo://src/app.rs#L837-L852",
+          "version": "repo-lines-v1:sha256:ca8594a3de60f6943b74187223fab353c0af7a724cf3ef68f1d2f06141510c59:eyJzZWxlY3RlZExpbmVDb3VudCI6MTYsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjNmN2EzMzI2Mzk4MGZiOTQ4OGI5NDcyZTI4ZGU0NjE4ZWVjMzc3MTE2ZmFmZDViODZlNmUzYTZhZGVjYjM2YTEiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjRjZTljYzQ1ZjFjOTkzN2IyMGMyZmJjOTM3YmUxN2QxYzhhMmU0MTdlMjUwMDI2ZjM4MzU3N2MyY2ViNGRkN2UiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImNmMGY4ZWU3NGMyYmNhMjZlNTlmY2NmYjUzZmQyYWI2OGJjNzIwZTY2MGZiZjM3M2IwZjFhNTY4ZmIyNjBjOTUifQ"
         }
       ]
     },
@@ -263,7 +267,7 @@
       "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#L771-L790",
+          "resource": "repo://src/app.rs#L1009-L1028",
           "version": "repo-lines-v1:sha256:0a937f62450210fb1b4a45c2eb392c8533cab8fc5e7f2ca88caec3ba64669dbb:eyJzZWxlY3RlZExpbmVDb3VudCI6MjAsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImZkMjA2ZWQ4NGMzNGNhMWE0ZmNmZmQ1NmFlOTBiNDgyZDIyNGM2OWQ0ZmRlMDY4MmI2MTllODNlMDRlNWJhYjYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjEyZTA5MzVlNDEzNWQ5MWEyZGFlMDUxOGE3ZTY5NjQ0MmIxYmM5ZGYxOThiNDA1ZmVmMGQzMjUwY2VmOWU1ZWQiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImQwMjI2ZGQ4YmZkY2JkMDhlNjUxMjUzOGY4MmU5YWNiOTEyNzg2NTgwNDEwMmQ2YjRlMzQ4NmRmMjU4YjMxNmYifQ"
         },
         {
@@ -281,26 +285,26 @@
       "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#L1457-L1465",
+          "resource": "repo://src/app.rs#L1700-L1708",
           "version": "repo-lines-v1:sha256:a271b225aba65e2765d3d7b1ab0f8a02114cdf60cf3a5298066b80ba40c06576:eyJzZWxlY3RlZExpbmVDb3VudCI6OSwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiZmI5YzZkZjEyNGI2NDgxODMyZDk5NTgyMjRmNzFjYjRjZDJjM2Y3YTU3Njc1OWQ0OGM5NjBkZDNmZTE4OTliZCIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDEyY2EzNDVjY2Y3NWJmOWMwODA2YmNlNjk1YmU4ZGU4MDhiNzk5ODQyNTFhN2E1NGQyMDJjZjYxMDFkZDQ1MSIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiZjk2YWY2OWU3OGEwNDBlNTAxNjM2YjMwNDhkNGVjNTQ2Yzc0OTU1ZTEyN2IyY2M5OTU4MDFiZDI2YWM2NjZkZSIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiOTViMzliZDc5NmMyYjVjYThiYjhjMWEzNjUzODk5NzY5MGE0OTE5ZDY5NzVlNGMxMGZiMWE1ZGE3MGU1Mjc3OSJ9"
         },
         {
-          "resource": "repo://src/app.rs#L582-L614",
+          "resource": "repo://src/app.rs#L738-L770",
           "version": "repo-lines-v1:sha256:49bdb09f74d399910bca1c218286bf995bae8e8457efe83cb97034814f6d67d5:eyJzZWxlY3RlZExpbmVDb3VudCI6MzMsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6ImJkOGRiMTAzNzg3MWI3NGU1OTVmYTZhZThkNjAwNTE4NDIyNDNkNzJkODZkNDM2OGUyZGU1OWI2MTdkYWM2NmIiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6ImJhMGM2MzVlMzlkMTFmNTgyOGE1Mzk1MzhkYjE0ZTdlNzU1YWFiMzlhZDNhNzljYmM0ZTFmZTQ4ZDgyZGZmYzIiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjBhN2YyNDA4YTExOTJiY2VmZTI0ODJhNTg2MGM1MTVhMmQ4NGIxMzQzZTE1ZDdmMGVmNGRhMTA0YzY1ZTJiM2EifQ"
         }
       ]
     },
     {
       "id": "claim_d7ecd7ebb6e94bf093bec3de37a63db1",
-      "statement": "Each image download makes up to 3 attempts, each abandoned after 60 seconds, waiting 2 seconds times the attempt number between attempts.",
+      "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#L1477-L1503",
-          "version": "repo-lines-v1:sha256:503c0157ef6bb7cec106cb2a0c2a270703d688580b0dcdc4e710dc7b9dc1fc08:eyJzZWxlY3RlZExpbmVDb3VudCI6MjcsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjZmMjVmNjQxZDQ5ZGRkYTllYmMyNjgyOGViZDJmNzM2OWFkNDBmMjBmODRhNDAwZDFkYjliYzE1YTNlMmQyNTEiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjAwOTlhMTBhNjE5YjUxNmM0MDM5NjFmZmQ4NzhlYWJiMDM2NGRjYjEzYjcxNWFlODI5OTg2ZjhkY2VjMzA1ZGIiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjY5OTNmMTFjZmFmODg0N2M5ZTgxNmQ0NDhiMWE0MzZiNjlhMjk1NjQzZmVmMGRlNDliYTMyY2FkYmFjNTIxM2UifQ"
+          "resource": "repo://src/app.rs#L1737-L1784",
+          "version": "repo-lines-v1:sha256:b0f4c6798d97128a8721a067c8b836f5c71583de7b58ebdd10d60b683f0e944e:eyJzZWxlY3RlZExpbmVDb3VudCI6NDgsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjJmNWJhZjEwNWExMDViNTk0MjMyNjIwYzMxMGFlZWJmOTYyNzMwMjIzMmY2OTVhNGM4ZWQ4NmRlMzI1YmVmN2YiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQxMmNhMzQ1Y2NmNzViZjljMDgwNmJjZTY5NWJlOGRlODA4Yjc5OTg0MjUxYTdhNTRkMjAyY2Y2MTAxZGQ0NTEiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6Ijk0M2IxZmMzZTY5ZTA3NDk5NTEzYjJlZTRmNTYyZGMwZWM0MTkzN2EwZTVlNTQwYWFhODAwNTAyOWEzMWU0ZWYiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6ImU4MzE5MzEzYWQ5OGM5ZTYxMjViMjE3ZTNhMWVlNDc4MzQ4N2EwNWYyMGI5ZjQ1MmFiYzM2OWQ0MDBmODMxNDkifQ"
         },
         {
-          "resource": "repo://src/app.rs#L66-L74",
-          "version": "repo-lines-v1:sha256:165e39ca6d47b5010cdaa8900029d4f8be1acafd1768902f613c6b9232914b22:eyJzZWxlY3RlZExpbmVDb3VudCI6OSwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiYmYzYmQxMmI3NWRiNDMzNWFmZjQwOGNhYzM3YjU4YjIxZDEyYWZlZTZlODgyYWM0ODlkYmNlN2JlMWJjOWYzNiIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiNDg4NjhmM2IxNzUxODE3YTdlZTgwYTMwNTU1MzhhNDBmNmU4ZTlhMmE3N2QwZTZmNDYwMGIwOTAwOTkyN2I2YiIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiODQyYTI1MTg4ZDEyNDhhNDk1MmZjN2E4MGI2OWM5ZThhNzE1NjJmYjhiOGFiNTQ4MmU5MGRmYjQwZDUwOWUwOSIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiYzhkMTA1ZjI4ZjVmMWMwMzFmYTE3MjY3NDE4MDgxN2NkZWUxZDJlODBiZmRiNWM4MzUzOWY3YTFhNzY5MGUwYSJ9"
+          "resource": "repo://src/app.rs#L78-L85",
+          "version": "repo-lines-v1:sha256:f10417291b9d10b83650107ab69d41f81c507b42f7c0e929eb19a135e58454eb:eyJzZWxlY3RlZExpbmVDb3VudCI6OCwiZmlyc3RTZWxlY3RlZExpbmVIYXNoIjoiZWIxNzBiYmI3MzI2NmFkYjU4ZmZkNDVjMTk5ZTIwNGM0N2Y0YWI0ZjU5OWJhN2IwZjU5OTZiODIxZGQzYzhmNiIsImxhc3RTZWxlY3RlZExpbmVIYXNoIjoiZDEwYjU5YmJmMDhjN2M2MmE0YzllNjQxYjNjODc5OGU0ZjFhNTQ0Yjc5MWNkMWE0NmM1M2FhN2NhZmJkNWEwNyIsInByZWNlZGluZ0NvbnRleHRMaW5lQ291bnQiOjMsInByZWNlZGluZ0NvbnRleHRIYXNoIjoiZmM0YTFmYzc0OTU1OGMxMzYyNGRmZjI5MjFlNWVlODgzNGJmMThhMGIzMzBhMmJkMGQzOGJiNzMyNGE3MjA0OCIsImZvbGxvd2luZ0NvbnRleHRMaW5lQ291bnQiOjMsImZvbGxvd2luZ0NvbnRleHRIYXNoIjoiYzhkMTA1ZjI4ZjVmMWMwMzFmYTE3MjY3NDE4MDgxN2NkZWUxZDJlODBiZmRiNWM4MzUzOWY3YTFhNzY5MGUwYSJ9"
         }
       ]
     },
@@ -309,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#L383-L400",
+          "resource": "repo://src/app.rs#L417-L434",
           "version": "repo-lines-v1:sha256:11076b2b498b6d19b7a0629886f2cdb37229105049de848490f4e1d9faead710:eyJzZWxlY3RlZExpbmVDb3VudCI6MTgsImZpcnN0U2VsZWN0ZWRMaW5lSGFzaCI6IjVhYTEzNjY2OGNiMzc5NzgyYTUwMTY4ZDNlOWM4Njg4MmM5YTBhZTAxOGQwNTZiNTEzZWU2ODA5OWYxNjc3YzYiLCJsYXN0U2VsZWN0ZWRMaW5lSGFzaCI6IjQ4OTNmNDBlMzc4MWViODk2YzgyZDA0NmEwODA2N2ExMWUzNWNkZjJmMWRmYWYyNmZlYTkwYjhkYjY2NzY0MTMiLCJwcmVjZWRpbmdDb250ZXh0TGluZUNvdW50IjozLCJwcmVjZWRpbmdDb250ZXh0SGFzaCI6IjVmMjAxZDg2ZGM3NzY0YWFmOTA5MGNhNDRkODcxNDM3Y2JiODk4ZGE5ZWJlMGU0ZDhlNjVlNDZiZDJkZGVkYjQiLCJmb2xsb3dpbmdDb250ZXh0TGluZUNvdW50IjozLCJmb2xsb3dpbmdDb250ZXh0SGFzaCI6IjAyNjE2ZDk0ZDJmZDg4NWViMDZhMGFmZjVkYjQ1YWUyOTdiMWQ5NzViYzU0MjRlZGIyYWUwZmE2NzZkMzE0ZTYifQ"
         },
         {
@@ -321,6 +325,6 @@
   ],
   "verification": {
     "by": "openwiki/0.7.2",
-    "at": "2026-10-23T21:26:24.156Z"
+    "at": "2026-10-24T16:39:50.206Z"
   }
 }

+ 1 - 1
openwiki/.claims/workflows/relink-and-pairing.json

@@ -1,6 +1,6 @@
 {
   "schemaVersion": 1,
-  "pageVersion": "sha256:2e5dcb382a443a271be7c245304c8aad46d09a5722939de151a90fe1c5e0c86a",
+  "pageVersion": "sha256:4b312458a35b3e82b48821c6414d376325ab935da8d66022b63d67277491f3e9",
   "claims": [
     {
       "id": "claim_5f458a6b32c4452a9d25edf481e1f07e",

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

@@ -1,8 +1,8 @@
 {
-  "updatedAt": "2026-10-23T21:42:31.279Z",
+  "updatedAt": "2026-10-24T17:14:13.422Z",
   "command": "update",
   "gitHead": "5c2fe04925ebbc998a25ef3427b9228fe16c419c",
-  "model": "host-agent/claude",
-  "status": "complete",
+  "model": "deepseek-v4.1-flash:1017",
+  "status": "interrupted",
   "language": "en"
 }

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

@@ -2,88 +2,88 @@
   "schemaVersion": 1,
   "pages": {
     "/openwiki/architecture/app-state.md": {
-      "pageVersion": "sha256:5a08c085203eed7c6af86a5ee968fd9f61a42c7dec64787f196a9cfc4a404452",
-      "completedBy": "claude-code",
-      "completedRunId": "a6d2d910-a412-4673-9738-ac2cf39deacc",
-      "gitHead": "5c2fe04925ebbc998a25ef3427b9228fe16c419c",
-      "sourceFingerprint": "sha256:c24f8ad189dd06410a92d4dfd7b616ebadbe9a4f3c5a1177586200d86d308d05"
+      "pageVersion": "sha256:37292ba995a3a70e902fd75c163ea0039a8724324712200d9fa529205b98e321",
+      "completedBy": "openwiki/0.7.2",
+      "completedRunId": "16d279c7-60b4-4a76-84ef-65ff9d091a51",
+      "gitHead": "dc69f5fc7cd2bf7d732e4ccc8ff34398a48d2d2b",
+      "sourceFingerprint": "sha256:d4dd3e842183a686e381a839ed385a71d14b3f62e2199d238c83b5e38549d98d"
     },
     "/openwiki/architecture/runtime-and-event-loop.md": {
-      "pageVersion": "sha256:e2f63ba48d127d1aaa5a3dc9ea68ed4b0137c88abb763235e4ed0f305b984766",
-      "completedBy": "claude-code",
-      "completedRunId": "a6d2d910-a412-4673-9738-ac2cf39deacc",
-      "gitHead": "5c2fe04925ebbc998a25ef3427b9228fe16c419c",
-      "sourceFingerprint": "sha256:c24f8ad189dd06410a92d4dfd7b616ebadbe9a4f3c5a1177586200d86d308d05"
+      "pageVersion": "sha256:1c12bcb75120e45d7940b31833bbe96fe727146b35af7e2e965eff9da4c51980",
+      "completedBy": "opencode",
+      "completedRunId": "96dd7fcb-e901-4ad4-a53d-a3a9e5f45e4d",
+      "gitHead": "dc69f5fc7cd2bf7d732e4ccc8ff34398a48d2d2b",
+      "sourceFingerprint": "sha256:d4dd3e842183a686e381a839ed385a71d14b3f62e2199d238c83b5e38549d98d"
     },
     "/openwiki/architecture/terminal-ui.md": {
-      "pageVersion": "sha256:63e03e3e582fd585379805751a335bfe7b208a8a64451a79c8c177698950d3a0",
-      "completedBy": "claude-code",
-      "completedRunId": "a6d2d910-a412-4673-9738-ac2cf39deacc",
-      "gitHead": "5c2fe04925ebbc998a25ef3427b9228fe16c419c",
-      "sourceFingerprint": "sha256:c24f8ad189dd06410a92d4dfd7b616ebadbe9a4f3c5a1177586200d86d308d05"
+      "pageVersion": "sha256:b917b7bf26bb55ba79b12fcf2c7cfe5936d6463a6bd9c6bfe7eed55735c000d3",
+      "completedBy": "opencode",
+      "completedRunId": "96dd7fcb-e901-4ad4-a53d-a3a9e5f45e4d",
+      "gitHead": "dc69f5fc7cd2bf7d732e4ccc8ff34398a48d2d2b",
+      "sourceFingerprint": "sha256:d4dd3e842183a686e381a839ed385a71d14b3f62e2199d238c83b5e38549d98d"
     },
     "/openwiki/concepts/history-store.md": {
-      "gitHead": "e147fe64421138ee5b6068d78986d551fd7e6be7",
-      "sourceFingerprint": "sha256:c91de01aa07805e76fe5ac531adad9e790a95e925f32e6ef21aa6b743d82f3e9",
-      "pageVersion": "sha256:fb61743878eebfa7a936223b03e0c9a3d4247468218b42035889a35e1283bc0a",
-      "completedBy": "claude-code",
-      "completedRunId": "0da681ea-cfc0-4af9-a2fe-1b34cb0fad26"
+      "pageVersion": "sha256:9d1f1a31451538c0c3bbc043ab35ce90cbffd32e26263470d8b43f7dc2486279",
+      "completedBy": "openwiki/0.7.2",
+      "completedRunId": "16d279c7-60b4-4a76-84ef-65ff9d091a51",
+      "gitHead": "dc69f5fc7cd2bf7d732e4ccc8ff34398a48d2d2b",
+      "sourceFingerprint": "sha256:d4dd3e842183a686e381a839ed385a71d14b3f62e2199d238c83b5e38549d98d"
     },
     "/openwiki/integrations/ai-client.md": {
-      "pageVersion": "sha256:fddcffbcc714b9d61c2a530a57c5b40dbc6dd4279ee7a363ee96b0d7a44fb7bb",
-      "completedBy": "claude-code",
-      "completedRunId": "a6d2d910-a412-4673-9738-ac2cf39deacc",
-      "gitHead": "5c2fe04925ebbc998a25ef3427b9228fe16c419c",
-      "sourceFingerprint": "sha256:c24f8ad189dd06410a92d4dfd7b616ebadbe9a4f3c5a1177586200d86d308d05"
+      "pageVersion": "sha256:4927c894dab42fbe05e52e939703259666aa47947ef7945c623bfa2116a02e4d",
+      "completedBy": "opencode",
+      "completedRunId": "96dd7fcb-e901-4ad4-a53d-a3a9e5f45e4d",
+      "gitHead": "dc69f5fc7cd2bf7d732e4ccc8ff34398a48d2d2b",
+      "sourceFingerprint": "sha256:d4dd3e842183a686e381a839ed385a71d14b3f62e2199d238c83b5e38549d98d"
     },
     "/openwiki/integrations/web-control-panel.md": {
-      "pageVersion": "sha256:5fefadb20f2b8cf87afcc027db804249288fd112025b45dd9e740fc8bca85d7e",
-      "completedBy": "claude-code",
-      "completedRunId": "a6d2d910-a412-4673-9738-ac2cf39deacc",
-      "gitHead": "5c2fe04925ebbc998a25ef3427b9228fe16c419c",
-      "sourceFingerprint": "sha256:c24f8ad189dd06410a92d4dfd7b616ebadbe9a4f3c5a1177586200d86d308d05"
+      "pageVersion": "sha256:deebb3a40f405864bed483e185086a069b8cd23cd1464a3e108c07917ca9b04c",
+      "completedBy": "opencode",
+      "completedRunId": "96dd7fcb-e901-4ad4-a53d-a3a9e5f45e4d",
+      "gitHead": "dc69f5fc7cd2bf7d732e4ccc8ff34398a48d2d2b",
+      "sourceFingerprint": "sha256:d4dd3e842183a686e381a839ed385a71d14b3f62e2199d238c83b5e38549d98d"
     },
     "/openwiki/integrations/whatsapp-client.md": {
-      "pageVersion": "sha256:18ed8fdf1d8ab4689b600aa550246fd3d6b6b26340ea7a345c775d128f40bc56",
-      "completedBy": "claude-code",
-      "completedRunId": "a6d2d910-a412-4673-9738-ac2cf39deacc",
-      "gitHead": "5c2fe04925ebbc998a25ef3427b9228fe16c419c",
-      "sourceFingerprint": "sha256:c24f8ad189dd06410a92d4dfd7b616ebadbe9a4f3c5a1177586200d86d308d05"
+      "gitHead": "dc69f5fc7cd2bf7d732e4ccc8ff34398a48d2d2b",
+      "sourceFingerprint": "sha256:d4dd3e842183a686e381a839ed385a71d14b3f62e2199d238c83b5e38549d98d",
+      "pageVersion": "sha256:4bfc94bcec7d0629fe535e916bfc05dd51ef04c1fe94a8b004f82f7f5cfb98d9",
+      "completedBy": "opencode",
+      "completedRunId": "96dd7fcb-e901-4ad4-a53d-a3a9e5f45e4d"
     },
     "/openwiki/operations/configuration-and-data.md": {
-      "pageVersion": "sha256:4424aa91ddf23d979235806fa52b23f096e57c324e97fc229b803823db189ab9",
-      "completedBy": "claude-code",
-      "completedRunId": "a6d2d910-a412-4673-9738-ac2cf39deacc",
-      "gitHead": "5c2fe04925ebbc998a25ef3427b9228fe16c419c",
-      "sourceFingerprint": "sha256:c24f8ad189dd06410a92d4dfd7b616ebadbe9a4f3c5a1177586200d86d308d05"
+      "gitHead": "dc69f5fc7cd2bf7d732e4ccc8ff34398a48d2d2b",
+      "sourceFingerprint": "sha256:d4dd3e842183a686e381a839ed385a71d14b3f62e2199d238c83b5e38549d98d",
+      "pageVersion": "sha256:8c0a6de343db6a1114f9ba81a51b8cb1b5f189feadb28cbed319933c98ef98d2",
+      "completedBy": "opencode",
+      "completedRunId": "96dd7fcb-e901-4ad4-a53d-a3a9e5f45e4d"
     },
     "/openwiki/quickstart.md": {
-      "pageVersion": "sha256:3763c54c3a6a1ddee93531d89a303f3b8c99e547a730b01dfb8e4c97d5211efc",
-      "completedBy": "claude-code",
-      "completedRunId": "a6d2d910-a412-4673-9738-ac2cf39deacc",
-      "gitHead": "5c2fe04925ebbc998a25ef3427b9228fe16c419c",
-      "sourceFingerprint": "sha256:c24f8ad189dd06410a92d4dfd7b616ebadbe9a4f3c5a1177586200d86d308d05"
+      "gitHead": "dc69f5fc7cd2bf7d732e4ccc8ff34398a48d2d2b",
+      "sourceFingerprint": "sha256:d4dd3e842183a686e381a839ed385a71d14b3f62e2199d238c83b5e38549d98d",
+      "pageVersion": "sha256:bb1caa7405aaac3db2f806e6cfc1c06f35c73367e4aa23ce6c97e2b4b5fcb861",
+      "completedBy": "opencode",
+      "completedRunId": "96dd7fcb-e901-4ad4-a53d-a3a9e5f45e4d"
     },
     "/openwiki/testing/unit-tests.md": {
-      "pageVersion": "sha256:0f5f5df1184eb187831e0b8e3f9e8a2516c314386ce7360f1c4915c9450c587f",
-      "completedBy": "claude-code",
-      "completedRunId": "a6d2d910-a412-4673-9738-ac2cf39deacc",
-      "gitHead": "5c2fe04925ebbc998a25ef3427b9228fe16c419c",
-      "sourceFingerprint": "sha256:c24f8ad189dd06410a92d4dfd7b616ebadbe9a4f3c5a1177586200d86d308d05"
+      "gitHead": "dc69f5fc7cd2bf7d732e4ccc8ff34398a48d2d2b",
+      "sourceFingerprint": "sha256:d4dd3e842183a686e381a839ed385a71d14b3f62e2199d238c83b5e38549d98d",
+      "pageVersion": "sha256:50c64fbc1c28a488dc62c88f83e98a06ce449b71661690714c90854f832928e9",
+      "completedBy": "opencode",
+      "completedRunId": "96dd7fcb-e901-4ad4-a53d-a3a9e5f45e4d"
     },
     "/openwiki/workflows/ai-autoreply.md": {
-      "pageVersion": "sha256:976c69745a1473cf4fe63a8e29ad6081b01d9823cb1d12071204bb9bcd039b6f",
-      "completedBy": "claude-code",
-      "completedRunId": "a6d2d910-a412-4673-9738-ac2cf39deacc",
-      "gitHead": "5c2fe04925ebbc998a25ef3427b9228fe16c419c",
-      "sourceFingerprint": "sha256:c24f8ad189dd06410a92d4dfd7b616ebadbe9a4f3c5a1177586200d86d308d05"
+      "gitHead": "dc69f5fc7cd2bf7d732e4ccc8ff34398a48d2d2b",
+      "sourceFingerprint": "sha256:d4dd3e842183a686e381a839ed385a71d14b3f62e2199d238c83b5e38549d98d",
+      "pageVersion": "sha256:51f277c84ac8b779ca6c470fcddd420be0463aa6c621620207ab047c32ec60ae",
+      "completedBy": "opencode",
+      "completedRunId": "96dd7fcb-e901-4ad4-a53d-a3a9e5f45e4d"
     },
     "/openwiki/workflows/relink-and-pairing.md": {
-      "pageVersion": "sha256:2e5dcb382a443a271be7c245304c8aad46d09a5722939de151a90fe1c5e0c86a",
-      "completedBy": "claude-code",
-      "completedRunId": "a6d2d910-a412-4673-9738-ac2cf39deacc",
-      "gitHead": "5c2fe04925ebbc998a25ef3427b9228fe16c419c",
-      "sourceFingerprint": "sha256:c24f8ad189dd06410a92d4dfd7b616ebadbe9a4f3c5a1177586200d86d308d05"
+      "pageVersion": "sha256:4b312458a35b3e82b48821c6414d376325ab935da8d66022b63d67277491f3e9",
+      "completedBy": "opencode",
+      "completedRunId": "96dd7fcb-e901-4ad4-a53d-a3a9e5f45e4d",
+      "gitHead": "dc69f5fc7cd2bf7d732e4ccc8ff34398a48d2d2b",
+      "sourceFingerprint": "sha256:d4dd3e842183a686e381a839ed385a71d14b3f62e2199d238c83b5e38549d98d"
     }
   }
 }

+ 1 - 0
openwiki/INSTRUCTIONS.md

@@ -0,0 +1 @@
+A code wiki for this repository.

+ 252 - 128
openwiki/architecture/app-state.md

@@ -1,15 +1,17 @@
 ---
 type: architecture
 title: Application state and event handling
-description: How the App struct in src/app.rs holds UI state, routes AppEvent values (keys, WhatsApp, AI replies, image descriptions, reply timers, web panel, configuration reloads, relink outcomes), handles function keys (including F8 relink), the list filter and chat deletion, toggles global and per-chat autoreply, and runs background tasks.
-tags: [app, state, events, keybindings, tokio, web-panel, relink, config-reload]
+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]
 verified:
   - by: openwiki/0.7.2
-    at: 2026-10-23T21:26:24.156Z
+    at: 2026-10-24T16:42:08.550Z
 sources:
   - id: openwiki-source-9fec9d1af408501c9c455161
     resource: repo://src/app.rs
-generated: { by: "claude-code", at: "2026-10-23T21:26:24.156Z" }
+  - id: openwiki-source-ce0c4ea8cd3b15431fa691ce
+    resource: repo://src/history.rs
+generated: { by: "openwiki/0.7.2", at: "2026-10-24T16:42:08.550Z" }
 ---
 
 # Application state and event handling
@@ -23,58 +25,66 @@ and [web panel](../integrations/web-control-panel.md) activity.
 
 ## Events
 
-`AppEvent` (`src/app.rs:106-141`) is the single message type of the main channel:
+`AppEvent` (`src/app.rs:117-161`) is the single message type of the main
+channel:
 
 | Variant | Producer | Meaning |
 |---|---|---|
-| `Key(KeyEvent)` | input thread | A key press (release/repeat events are filtered out upstream). |
+| `Key(KeyEvent)` | input thread (`wh-input`) | A key press; release and repeat events are filtered out in `main.rs`. |
 | `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` (model called the `no_reply` tool) or an error string. |
-| `ImagesDescribed { target, results }` | image description task | One description (or error) per image of a chat, keyed by message id; `target` says whether to answer the chat afterwards. |
+| `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. |
+| `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. |
-| `Web(WebRequest)` | `wh-web` thread | A command from the [web control panel](../integrations/web-control-panel.md) with a reply channel. |
+| `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. |
 
-`handle_event` (`src/app.rs:293-317`) dispatches each variant; after an
-`AiReply` it always calls `finish_reply` to end the chat's pending state. Only
-fatal history (SQLite) errors propagate as `Err`; every other failure is turned
-into a `status` text shown in the footer.
+`handle_event` (`src/app.rs:319-351`) 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
+a `status` text shown in the footer.
 
 ## State
 
-`App` (`src/app.rs:188-239`) exposes UI fields read by the renderer: `screen`,
+`App` (`src/app.rs:208-267`) exposes UI fields read by the renderer: `screen`,
 `qr_code`, `focus`, `list_mode`, `chats`, `contacts`, `selected`, `filter`,
 `open_chat`, `open_chat_name`, `open_chat_autoreply`, `global_autoreply`,
 `messages`, `input`, `status` and `should_quit`. Private fields hold the
-`History`, the `Config`, an optional `AiClient`, the autoreply `ReplyGate`,
-the WhatsApp handle and a clone of the event sender, plus the reply
-bookkeeping:
+`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:
 
 | Field | Purpose |
 |---|---|
-| `pending_images` | images received by chats with a scheduled reply, per chat, in arrival order |
+| `pending_images` | images received by chats with a scheduled reply, per chat, in arrival order, each with its message id |
 | `backlog_images` | images delivered with offline messages, kept until the catch-up runs |
 | `describing` | message ids whose image description is in progress (never described twice) |
+| `pending_audios` | audios received by chats with a scheduled reply, per chat, in arrival order, each with its message id |
+| `backlog_audios` | audios delivered with offline messages, kept until the catch-up runs |
+| `transcribing` | message ids whose audio transcription is in progress (never transcribed twice) |
 | `answered_up_to` | per chat, the id of the last message in the context of the reply in progress |
 | `own_jids_known`, `catch_up_done` | one-shot flags for own JIDs and the startup catch-up |
 | `pending_delete` | the chat awaiting a deletion confirmation |
 | `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 |
 
-- `Screen` (`src/app.rs:151-157`): `Connecting` → `Pairing` (QR shown) → `Main`.
-- `Focus` (`src/app.rs:159-164`): `List` or `Input`.
-- `ListMode` (`src/app.rs:166-171`): `Chats` or `Contacts`.
+- `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`.
 
-`App::new` (`src/app.rs:246-286`) 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 and no pending
-deletion or relink, and creates the `ReplyGate` with the current time, so that
-backlog messages are never auto-answered live.
+`App::new` (`src/app.rs:269-317`) takes an `AppDeps` (`src/app.rs:193-199`) —
+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
+that backlog messages are never auto-answered live.
 
-`build_ai` (`src/app.rs:1534-1542`) returns no client when the system prompt
+`build_ai` (`src/app.rs:1874-1887`) 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
@@ -84,38 +94,40 @@ 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:388-400`) keeps the current configuration
+`apply_reloaded_config` (`src/app.rs:417-434`) 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
 `Configurazione ricaricata`, or the `build_ai` status when the AI is not
 configured or unavailable. Later replies therefore use the new prompt, model,
-delays and retries without a restart.
+delays, retries and `transcribe_*` settings without a restart.
 
 ## WhatsApp events
 
-`handle_wa_event` (`src/app.rs:406-429`):
+`handle_wa_event` (`src/app.rs:436-463`):
 
 - `Qr` → switch to `Pairing` and store the QR payload.
 - `Connected` → switch to `Main`, clear the QR, status `Connesso`, refresh own JIDs.
-- `Disconnected` / `Error` → update `status` only.
-- `LoggedOut` → back to `Connecting`, status `Sessione scollegata`.
+- `Disconnected` / `Error` → copy the text into `status` only.
+- `LoggedOut` → 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:435-443`).
-- `Message { message, image }` → `on_message` (`src/app.rs:450-459`).
+  refresh the open chat name (`src/app.rs:465-477`).
+- `Message { message, image, audio }` → `on_message` (`src/app.rs:479-499`).
 
 `on_message` saves the message and stops immediately when
-`History::save_message` reports a duplicate, so history sync replays never
+`History::save_message` reports it was not new, so history sync replays never
 trigger UI reloads or autoreplies twice. New messages reload the chat list, the
-open conversation and then go through `maybe_autoreply` (see
+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:797-804`); the list is
-empty while the device is not paired (or no session is running during a
-relink), so the refresh is retried later (`own_jids_known` stays false until it
-succeeds).
+Own JIDs are fetched by `refresh_own_jids` (`src/app.rs:1030-1042`), 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
+on `Connected`, before scheduling a reply and at the start of the catch-up.
 
 ## Relink
 
@@ -123,19 +135,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:361-369`) cancels a pending chat deletion; the
+- `request_relink` (`src/app.rs:390-403`) 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:375-381`) clears the flag on any other
+- `cancel_pending_relink` (`src/app.rs:405-415`) clears the flag on any other
   key and shows `Nuovo abbinamento annullato`.
-- `start_relink` (`src/app.rs:326-341`) is a no-op while `relinking`;
+- `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
   `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:348-354`) clears `relinking` and shows
+- `finish_relink` (`src/app.rs:377-388`) 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.
@@ -143,52 +155,155 @@ 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:642-644`). `ai_setup`
-(`src/app.rs:749-751`) returns nothing, and so stops every path, when the AI
+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
 client or the system prompt is missing. The full workflow is in
 [AI autoreply](../workflows/ai-autoreply.md); the state machine in `App` is:
 
-- **Schedule** — `maybe_autoreply` (`src/app.rs:468-483`) calls
+```mermaid
+flowchart TD
+    M["WaEvent::Message with image or audio"] --> MA["maybe_autoreply"]
+    MA -->|"should_schedule"| PEND["gate.mark_pending and ReplyDue timer"]
+    MA -->|"enabled chat, allowed sender"| KEEP["keep_image and keep_audio"]
+    KEEP -->|"chat pending"| PI["pending_images and pending_audios"]
+    KEEP -->|"backlog before catch-up"| BI["backlog_images and backlog_audios"]
+    KEEP -->|"backlog after catch-up"| LATE["describe_late_image or transcribe_late_audio, no reply"]
+    PEND --> RD["ReplyDue"]
+    PI --> PREP["prepare_reply"]
+    BI --> PREP
+    RD --> PREP
+    PREP -->|"reply_context returns nothing"| FIN["finish_reply"]
+    PREP -->|"audios need transcription"| TR["spawn_transcriptions, images postponed"]
+    TR --> AT["AudiosTranscribed"]
+    AT -->|"postponed images need description"| DE["spawn_descriptions"]
+    AT -->|"nothing left"| SR["start_reply"]
+    PREP -->|"no audio, images need description"| DE
+    PREP -->|"nothing left"| SR
+    DE --> ID["ImagesDescribed"]
+    ID --> SR
+    SR --> AI["AI request"]
+    AI --> AR["AiReply"]
+    AR --> FIN
+    FIN -->|"newer schedulable message"| PEND
+    FIN -->|"otherwise"| DROP["drop kept images and audios"]
+```
+
+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_reply` when `ReplyGate::should_schedule` allows it: the chat is
-  marked pending and `spawn_reply_timer` (`src/app.rs:1345-1350`) emits
+  marked pending and `spawn_reply_timer` (`src/app.rs:1581-1591`) emits
   `ReplyDue` after `reply_delay_seconds`. A chat already pending gets no second
-  timer, so messages arriving meanwhile are answered together. An image sent by
-  an allowed sender in an enabled chat is kept by `keep_image`
-  (`src/app.rs:501-515`): in `pending_images` for a pending chat, in
-  `backlog_images` for an offline image before the catch-up, or described at
-  once without reply (`describe_late_image`) after it.
-- **Reply due** — `handle_reply_due` (`src/app.rs:539-545`) takes the kept
-  images and, if the chat is still pending, calls `prepare_reply`
-  (`src/app.rs:556-568`). `reply_context` (`src/app.rs:652-664`) returns
-  nothing when the autoreply was disabled, the AI is missing, the chat is
-  empty or its last message was written by hand by the owner; then the
-  pending state ends. Otherwise images still lacking a description go to
+  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
+  `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
+  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
+  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
+  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
+  `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.
-- **Descriptions** — `handle_images_described` (`src/app.rs:591-614`) stores
+- **Transcriptions** — `handle_audios_transcribed` (`src/app.rs:772-829`)
+  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
   each description whose message still exists with
   `History::save_image_description`, sets one status for failures
   (`Descrizione immagine fallita (N): <first error>`) and, when `then_reply`
-  holds and the chat is still pending, starts the reply.
-- **Start** — `start_reply` (`src/app.rs:771-790`) records the last context
-  message in `answered_up_to`, marks own mentions, reads the chat name and the
-  stored image descriptions, builds the turns and calls `spawn_ai_request`.
-- **Finish** — `finish_reply` (`src/app.rs:622-636`) clears the pending state
+  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
+  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
   and `answered_up_to`; if a newer message that passes `should_schedule`
-  arrived after the answered one, it schedules a new reply (keeping its
-  images), otherwise it drops the kept images.
-- **Startup catch-up** — `on_offline_sync_completed` (`src/app.rs:688-694`)
+  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`)
   runs `catch_up` once per process (`catch_up_done`). `catch_up`
-  (`src/app.rs:704-724`) asks `catch_up_chats` (`src/app.rs:731-743`) 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 its offline images (no
-  reply delay); the status becomes `Risposte di recupero: N` when N > 0.
+  (`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
+  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
+  `Risposte di recupero: N` when N > 0. Backlog media of the other chats is
+  dropped.
+
+### Audio transcription path
+
+Voice notes take the same route as images, one step earlier:
+
+- **Gate** — `transcription_enabled` (`src/app.rs:717-724`) 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.
+- **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
+  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
+  `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
+  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
+  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
+  `AudiosTranscribed`, a failed audio never stopping the others and the bytes
+  staying in memory.
+- **Storage and statuses** — transcriptions are written by
+  `handle_audios_transcribed` as `[messaggio vocale] <text>`
+  (`VOICE_NOTE_TEXT`, `src/app.rs:38-41`), so a message is transcribed once;
+  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`).
+
+### Ordering invariant
+
+For one reply the media work is strictly ordered: transcriptions first, then
+image descriptions, then the AI request. `prepare_reply` never describes
+images while an audio still needs a transcription; it hands the whole image
+list to `spawn_transcriptions` instead, and `handle_audios_transcribed`
+describes those postponed images once the transcriptions are stored. A chat's
+pending state, and therefore its media bookkeeping, is released only by
+`finish_reply`, which keeps the kept media only when a newer schedulable
+message arrived after the answered one — otherwise both maps are emptied.
 
 ## Key bindings
 
-Commands use function keys (`src/app.rs:23-32`); `handle_key`
-(`src/app.rs:884-929`) dispatches them:
+Commands use function keys (`src/app.rs:24-33`); `handle_key`
+(`src/app.rs:1122-1167`) dispatches them:
 
 | Key | Where | Action |
 |---|---|---|
@@ -208,13 +323,12 @@ Commands use function keys (`src/app.rs:23-32`); `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 to the open chat. |
+| Enter | input focus | Send the trimmed input (`submit_input`, `src/app.rs:1317-1329`). |
 
-F10 and F8 are checked first, on every screen. Any other key cancels a
-pending relink; an Esc that does so is consumed, every other key then
-continues normally. Non-main screens ignore everything else. Ctrl chords are
+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:1592-1594`)
+type into the input or the filter; `is_command` (`src/app.rs:1932-1939`)
 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
@@ -223,109 +337,119 @@ 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:937-973`). `visible_chats` / `visible_contacts`
-(`src/app.rs:1048-1058`) keep the entries whose name contains the filter,
-case-insensitively (`matches_filter`, `src/app.rs:1289-1291`); selection,
+`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,
 opening, F3 and deletion all work on the filtered list.
 
 ### Local chat deletion
 
-`request_delete` (`src/app.rs:992-1009`) works only in chat mode: the first
+`request_delete` (`src/app.rs:1225-1247`) 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:1016-1031`), which removes
-the chat from the history (messages, image descriptions and row; no WhatsApp
-call), drops its pending reply, kept images and answered snapshot, closes it
-if open and shows `Chat <name> eliminata`. Any other key (Tab, F2, F3, F4, F8,
-list keys, Ctrl chords) cancels the pending deletion with
-`Eliminazione annullata` (`cancel_pending_delete`, `src/app.rs:979-985`).
+the same chat calls `confirm_delete` (`src/app.rs:1249-1272`), 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`).
 
 ### Opening and toggling
 
-`selected_jid` (`src/app.rs:1221-1232`) returns the JID on the selected row of
+`selected_jid` (`src/app.rs:1462-1473`) 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:1205-1219`) uses it to load display name,
+`open_selected` (`src/app.rs:1446-1460`) 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:1249-1274`) keep
-`selected` inside the active filtered list.
+moves focus to the input. The `reload_*` helpers
+(`src/app.rs:1486-1515`) keep `selected` inside the active filtered list.
 
-`toggle_autoreply` (F3, `src/app.rs:1101-1116`) picks the target from the
+`toggle_autoreply` (F3, `src/app.rs:1342-1357`) 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:1124-1137`), which always
+delegates to `toggle_chat_autoreply` (`src/app.rs:1359-1378`), 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:1549-1557`), which first requires an AI client and then a chat
+(`src/app.rs:1889-1902`), 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:1192-1203`) refuses to enable
+`toggle_global_autoreply` (F4, `src/app.rs:1427-1444`) 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:1139-1156`) applies a `WebCommand` and then
+`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:
 
 - `State` → nothing, just the snapshot;
 - `ToggleGlobal` → `toggle_global_autoreply` (same rules as F4);
-- `ToggleChat(jid)` → `toggle_web_chat` (`src/app.rs:1163-1169`): if the JID is
+- `ToggleChat(jid)` → `toggle_web_chat` (`src/app.rs:1399-1410`): 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:1171-1184`) returns the global flag, the status line,
-`web_chats` (`src/app.rs:1575-1585`) — the TUI chat list in the same order,
-restricted to chats that support autoreply — the current screen as
-`connecting` / `pairing` / `main` (`web_screen`, `src/app.rs:1563-1569`) and,
-while a QR payload is stored, the QR code rendered as SVG by `web::qr_svg`. See
+`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 control panel](../integrations/web-control-panel.md).
 
 ## Sending and background tasks
 
 All network work runs in tokio tasks that report back through the event
 channel; `send_event` ignores a closed channel because that only happens on
-shutdown (`src/app.rs:1335-1338`).
+shutdown (`src/app.rs:1572-1579`).
 
-- `submit_input` (`src/app.rs:1080-1088`) ignores blank input or no open chat,
+- `submit_input` (`src/app.rs:1317-1329`) ignores blank input or no open chat,
   clears the input and calls `spawn_send` with `is_ai: false`.
-- `spawn_send` (`src/app.rs:1518-1527`) calls `WaHandle::send_text` and emits
+- `spawn_send` (`src/app.rs:1859-1872`) 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:858-882`) stores a successful send as a
-  `from_me` message (with `is_ai` preserved) and reloads the views; a failure
-  sets `Invio fallito: …`.
-- `spawn_ai_request` (`src/app.rs:1359-1370`) runs the text completion in one
-  task and waits for it in another, so `AiReply` is always emitted — even when
-  the request task panics — and the chat never stays pending.
-- `spawn_image_descriptions` (`src/app.rs:1381-1404`) runs one task per image
+- `handle_sent` (`src/app.rs:1092-1120`) 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
+  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
   in parallel (download with up to 3 attempts and a 60 s timeout each, then
   `AiClient::describe_image`) and a collector task that emits a single
   `ImagesDescribed`; a failed image does not stop the others and the bytes
   stay in memory.
-- `handle_ai_reply` (`src/app.rs:812-818`) shows
+- `spawn_audio_transcriptions` (`src/app.rs:1786-1830`) 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
   `AI: nessuna risposta necessaria` for `Reply::NoReply`, `Errore AI: …` for
   an error, and passes text to `send_ai_reply_if_enabled`
-  (`src/app.rs:826-839`), which sends it only if the chat is still enabled and
-  the owner did not answer by hand meanwhile (`owner_answered_meanwhile`);
-  otherwise it shows `Risposta AI scartata: autoreply disattivato` or
+  (`src/app.rs:1058-1077`), 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
+  `Risposta AI scartata: autoreply disattivato` or
   `Risposta AI scartata: hai già risposto`.
 - The relink task (see [Relink](#relink)) runs `WaHandle::relink` and emits
   `Relinked`.
 
 ## Tests
 
-Unit tests at `src/app.rs:1596` onward cover `can_enable_autoreply` (private,
+Unit tests at `src/app.rs:1941` onward cover `can_enable_autoreply` (private,
 self-chat LID, groups allowed; broadcasts/newsletters and missing AI refused),
-`web_chats` filtering and order, `vision_applies`,
-`select_images_to_describe` (last five, skip known and out-of-context),
-`arrived_after`, `manual_reply_after`, `description_failure_status`,
-`matches_filter`, selection clamping/stepping and page steps, the toggle
-helpers, `is_command` and the fixed function-key codes (F2, F3, F4, F10; F8 is
-not part of that check). Relink, configuration reload and F3 targeting have no
-`App`-level tests. See [Unit tests](../testing/unit-tests.md).
+`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`,
+`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
+[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 holds UI state, routes AppEvent values (keys, WhatsApp, AI replies, image descriptions, reply timers, web panel, configuration reloads, relink outcomes), handles function keys (including F8 relink), the list filter and chat deletion, toggles global and per-chat autoreply, and runs 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), including the image and audio bookkeeping maps and sets, 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.

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

@@ -10,7 +10,7 @@ sources:
     resource: repo://src/config_watch.rs
   - id: openwiki-source-b55a21a31ede1b56cd31a6a6
     resource: repo://src/main.rs
-generated: { by: "claude-code", at: "2026-10-23T21:26:24.156Z" }
+generated: { by: "openwiki/0.7.2", at: "2026-10-24T16:42:08.550Z" }
 verified:
   - by: openwiki/0.7.2
     at: 2026-10-23T21:26:24.156Z
@@ -29,14 +29,14 @@ 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:30-31`), which the [terminal UI](terminal-ui.md) shows at the
+(`src/main.rs:32`), which the [terminal UI](terminal-ui.md) shows at the
 bottom right.
 
 ## Startup sequence
 
-`main` (`src/main.rs:35-71`) runs on the multi-threaded tokio runtime
+`main` (`src/main.rs:44-72`) runs on the multi-threaded tokio runtime
 (`#[tokio::main]`). If any command-line argument is `--version` or `-V`
-(`wants_version`, `src/main.rs:73-79`) it only prints `wh <BUILD_VERSION>`
+(`wants_version`, `src/main.rs:78-80`) it only prints `wh <BUILD_VERSION>`
 and exits, without reading the configuration or touching the terminal.
 Otherwise it:
 
@@ -51,11 +51,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:95-104`): nothing happens when
+   with `start_web_panel` (`src/main.rs:100-105`): 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:60`).
-8. Builds `App` from `AppDeps { config, history, wa, events }`.
+   `config_watch::spawn(config.source_files(), app_tx)` (`src/main.rs:61`).
+8. Builds `App` from `AppDeps { config, history, wa, events }`
+   (`src/main.rs:63-68`).
 9. Spawns the input thread.
 10. Runs the TUI until quit.
 
@@ -69,18 +70,18 @@ so a changed `web_port` only applies after a restart.
 
 ```
  crossterm (blocking)   whatsapp-rust bot   tiny_http (blocking)  config_watch task   tokio tasks in App
-   "wh-input" thread     Translator task     "wh-web" thread      (2 s polling)       (AI, image description,
-        |                      |                   |                   |               send, reply timers, relink)
-        | Key/Redraw           | WaEvent           | Web(WebRequest)   | ConfigReloaded       |
-        v                      v                   v                   v                      | AiReply / Sent /
-   app_tx  <----  WhatsApp forwarder (Wa(..))  <---+-------------------+                      | ImagesDescribed /
-        ^                                                                                     | ReplyDue / Relinked
-        +-------------------------------------------------------------------------------------+
-        |
-     app_rx  -->  event_loop: draw ui::render -> recv -> app.handle_event
+   "wh-input" thread     Translator task     "wh-web" thread      (2 s polling)       (AI, media description
+        |                      |                   |                   |               and transcription,
+        | Key/Redraw           | WaEvent           | Web(WebRequest)   | ConfigReloaded  sends, reply timers,
+        v                      v                   v                   v                 relink)
+   app_tx  <----  WhatsApp forwarder (Wa(..))  <---+-------------------+                      | AiReply / Sent /
+        ^                                                                                     | ImagesDescribed /
+        +-------------------------------------------------------------------------------------+ AudiosTranscribed
+        |                                                                                     | ReplyDue / Relinked
+     app_rx --> event_loop: draw ui::render -> recv -> app.handle_event
 ```
 
-- **WhatsApp forwarder** (`src/main.rs:81-93`): a tokio task that wraps each
+- **WhatsApp forwarder** (`src/main.rs:86-94`): 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
@@ -96,27 +97,27 @@ 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:106-141`): a named OS thread (`wh-input`)
+- **Input thread** (`src/main.rs:111-142`): 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:143-154`): only `KeyEventKind::Press` keys
+- **Event mapping** (`src/main.rs:149-155`): 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 downloads and
-  descriptions, sends, reply-delay timers, relink) hold a clone of the sender
-  and report with `AiReply` / `ImagesDescribed` / `Sent` / `ReplyDue` /
-  `Relinked`.
+- **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`.
 
 ## TUI loop
 
-`run_tui` (`src/main.rs:156-166`) calls `ratatui::init`, runs the loop, then
-wipes the screen with `clear_screen` (`src/main.rs:168-175`: clear all and
+`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
 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:177-205`) repeats while `!app.should_quit`:
+`event_loop` (`src/main.rs:183-206`) 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.

+ 28 - 29
openwiki/architecture/terminal-ui.md

@@ -6,7 +6,7 @@ tags: [ui, ratatui, tui, qr-code, rendering]
 sources:
   - id: openwiki-source-6bddea306b9b87f180f1fc02
     resource: repo://src/ui.rs
-generated: { by: "claude-code", at: "2026-10-23T21:26:24.156Z" }
+generated: { by: "openwiki/0.7.2", at: "2026-10-24T16:42:08.550Z" }
 verified:
   - by: openwiki/0.7.2
     at: 2026-10-23T21:26:24.156Z
@@ -22,7 +22,7 @@ All user-facing strings are Italian constants at the top of the file
 
 ## Screens
 
-`render` (`src/ui.rs:105-116`) dispatches on `app.screen`:
+`render` (`src/ui.rs:109-120`) dispatches on `app.screen`:
 
 | Screen | Function | Content |
 |---|---|---|
@@ -30,9 +30,9 @@ All user-facing strings are Italian constants at the top of the file
 | `Pairing` | `render_pairing` | Title, QR code, pairing steps ("WhatsApp > Impostazioni > Dispositivi collegati > Collega un dispositivo"), status, dimmed key help `F8: nuovo QR   F10: esci` |
 | `Main` | `render_main` | List pane, messages pane, input pane, footer |
 
-`render_centered` (`src/ui.rs:152-169`) centers a block of lines vertically
+`render_centered` (`src/ui.rs:156-173`) centers a block of lines vertically
 and horizontally. The key help on the two pre-connection screens
-(`TEXT_SCREEN_KEYS`, `src/ui.rs:80-83`) advertises the only keys that work
+(`TEXT_SCREEN_KEYS`, `src/ui.rs:81`) advertises the only keys that work
 there: F8 to delete the session and pair again (see
 [WhatsApp relink and pairing](../workflows/relink-and-pairing.md)) and F10 to
 quit. During a relink the app shows the `Connecting` screen with the status
@@ -41,13 +41,13 @@ quit. During a relink the app shows the `Connecting` screen with the status
 ### QR code
 
 The QR payload comes from the [WhatsApp client](../integrations/whatsapp-client.md)
-(`WaEvent::Qr`). `qr_to_text` (`src/ui.rs:191-205`) encodes it with the
+(`WaEvent::Qr`). `qr_to_text` (`src/ui.rs:196-209`) encodes it with the
 `qrcode` crate using `Dense1x2` half-block characters and a quiet zone;
-colors are inverted (dark modules drawn as empty cells) so the code scans on a
-dark terminal background. The [web control panel](../integrations/web-control-panel.md)
+colors are inverted (dark modules drawn as empty cells) so the code scans on
+a dark terminal background. The [web control panel](../integrations/web-control-panel.md)
 renders the same payload separately as SVG.
 
-`qr_block_lines` (`src/ui.rs:171-189`) replaces the QR with a single line when:
+`qr_block_lines` (`src/ui.rs:175-194`) replaces the QR with a single line when:
 
 - no code has arrived yet: "In attesa del codice QR...";
 - encoding fails: "Errore generazione QR: <error>";
@@ -57,7 +57,7 @@ renders the same payload separately as SVG.
 
 ## Main screen layout
 
-`render_main` (`src/ui.rs:207-226`):
+`render_main` (`src/ui.rs:211-230`):
 
 ```
 +---------------------------------+--------------------------------------+
@@ -75,66 +75,65 @@ renders the same payload separately as SVG.
 (The list title is a single line: base title, then the global marker, then
 the search text.)
 
-- **List pane** (30% width, `src/ui.rs:242-265`): the filtered chats or
+- **List pane** (30% width, `src/ui.rs:246-270`): the filtered chats or
   contacts (`App::visible_chats` / `visible_contacts`) depending on
-  `list_mode`. The title (`list_title`, `src/ui.rs:267-281`) is
+  `list_mode`. The title (`list_title`, `src/ui.rs:272-285`) is
   "Chat [F2: Contatti]" / "Contatti [F2: Chat]", followed — in chat mode
   only — by `●AI tutte` when the global autoreply is on, and by
   `Cerca: <filter>` when a filter is typed. The selection is reversed with a
   `"> "` marker and clamped by `selected_index`. Chats whose per-chat
   autoreply is enabled show a green ` ●AI` marker (`chat_item`,
-  `src/ui.rs:291-302`). With list focus, F3 toggles the selected row.
-- **Messages pane** (`src/ui.rs:312-352`): title is
+  `src/ui.rs:295-306`). With list focus, F3 toggles the selected row.
+- **Messages pane** (`src/ui.rs:329-356`): title is
   ` <name> ── AI: ON|OFF ` or " Nessuna chat aperta "; `AI: ON` is shown when
   the open chat's flag **or** the global autoreply is on. All messages are
   wrapped to the inner width and only the last rows that fit are kept, so the
   newest messages are always visible (no scrolling).
-- **Input pane** (3 rows, `src/ui.rs:494-510`): shows `"> " + input`; when
+- **Input pane** (3 rows, `src/ui.rs:498-514`): shows `"> " + input`; when
   the text is wider than the box only the tail is shown (`tail_fitting`), and
   the terminal cursor is placed after it while the input has focus.
-- **Footer** (1 row, `src/ui.rs:530-577`): split into a left area with the
+- **Footer** (1 row, `src/ui.rs:554-569`): split into a left area with the
   bold status text followed by the key help
   `Tab:focus  Enter:apri/invia  F2:chat/contatti  Canc:elimina  F3:AI chat  F4:AI tutte  F8:nuovo QR  F10:esci`
-  (`src/ui.rs:53-59`), and a right area, exactly as wide as needed, with the
+  (`src/ui.rs:54-59`), and a right area, exactly as wide as needed, with the
   dimmed build version `v<version> (<git hash>) ` (`version_label`, from
   `BUILD_VERSION`, see [Runtime and event loop](runtime-and-event-loop.md)).
   The help is dimmed, except `F4:AI tutte`, which is drawn green while the
   global autoreply is on (`key_help_spans`).
 
 The focused pane (list or input) has a yellow border (`pane_block`,
-`src/ui.rs:228-240`).
+`src/ui.rs:232-244`).
 
 ## Message formatting
 
-`format_message_line` (`src/ui.rs:379-399`) renders a
+`format_message_line` (`src/ui.rs:383-403`) renders a
 [stored message](../concepts/history-store.md) as `HH:MM <sender>: <text>` in
 the local time zone; out-of-range timestamps show `--:--`
-(`format_clock_time`, `src/ui.rs:401-413`).
+(`format_clock_time`, `src/ui.rs:405-418`).
 
-`sender_label` (`src/ui.rs:416-433`) picks:
+`sender_label` (`src/ui.rs:420-437`) picks:
 
 1. `Tu` for own messages, `[AI] Tu` when the message was generated by the
    [AI autoreply](../workflows/ai-autoreply.md);
 2. otherwise the trimmed, non-blank sender name;
 3. otherwise the user part of the sender JID (text before `@`).
 
-Colors (`message_style`, `src/ui.rs:367-377`): AI messages cyan, own messages
+Colors (`message_style`, `src/ui.rs:371-381`): AI messages cyan, own messages
 green, incoming messages default.
 
 ## Wrapping and widths
 
 Widths are measured in terminal cells through ratatui's `Line::width`, so
-wide characters (CJK, emoji) count as two cells (`src/ui.rs:443-459`).
-`wrap_text` (`src/ui.rs:461-492`) splits on `\n`, wraps each paragraph by
+wide characters (CJK, emoji) count as two cells (`src/ui.rs:447-463`).
+`wrap_text` (`src/ui.rs:465-497`) splits on `\n`, wraps each paragraph by
 characters (not words), drops control characters and always emits at least
 one row per paragraph.
 
 ## Tests
 
-Unit tests in `src/ui.rs:579` onward cover the footer key help (F4
+Unit tests at `src/ui.rs:579` onward cover the footer key help (F4
 highlighted only when the global autoreply is on), the version label, sender
-labels and fallbacks, clock
-formatting in a fixed time zone, QR text generation and its error/missing
-cases, wrapping, keeping the latest visible lines, `tail_fitting`, the
-messages title, the list title (global marker and filter) and selection
-clamping. See [Unit tests](../testing/unit-tests.md).
+labels and fallbacks, clock formatting in a fixed time zone, QR text
+generation and its error/missing cases, wrapping, keeping the latest visible
+lines, `tail_fitting`, the messages title, the list title (global marker and
+filter) and selection clamping. See [Unit tests](../testing/unit-tests.md).

+ 265 - 85
openwiki/concepts/history-store.md

@@ -1,143 +1,323 @@
 ---
 type: concept
 title: Message history store
-description: The local SQLite database in src/history.rs that stores messages, chat summaries, contacts, the per-chat AI autoreply flag, the global autoreply setting and the AI descriptions of images, and supports local chat deletion.
-tags: [sqlite, rusqlite, history, persistence, schema]
+description: The local SQLite database in src/history.rs — its schema and no-migration lifecycle, the deduplicating save, the read queries, contacts, the per-chat and global autoreply flags, image descriptions, chat deletion, and update_message_text as the only in-place rewrite of a stored message body (voice-note transcriptions stored inside messages.text).
+tags: [sqlite, rusqlite, history, persistence, schema, transcription]
 sources:
+  - id: openwiki-source-9fec9d1af408501c9c455161
+    resource: repo://src/app.rs
+  - id: openwiki-source-043e6252badc57a1ee6bfb48
+    resource: repo://src/autoreply.rs
   - id: openwiki-source-ce0c4ea8cd3b15431fa691ce
     resource: repo://src/history.rs
-generated: { by: "claude-code", at: "2026-10-21T21:49:12.291Z" }
+  - id: openwiki-source-7ea5eac45e5030178a6ea05b
+    resource: repo://src/whatsapp.rs
 verified:
   - by: openwiki/0.7.2
-    at: 2026-10-21T21:49:12.291Z
+    at: 2026-10-24T16:42:08.550Z
+generated: { by: "openwiki/0.7.2", at: "2026-10-24T16:42:08.550Z" }
 ---
 
 # Message history store
 
-`src/history.rs` wraps a single `rusqlite::Connection` in the `History`
-struct. It is the only persistent state owned by the app itself (the
-WhatsApp session lives in a separate database, see
-[Configuration and data](../operations/configuration-and-data.md)).
-[`App`](../architecture/app-state.md) owns the `History` and calls it
-synchronously from the event loop.
+`src/history.rs` wraps a single `rusqlite::Connection` in the `History` struct
+(`repo://src/history.rs#L57-L60`). It is the app's only persistent state of its
+own — the WhatsApp session lives in a separate database owned by
+`whatsapp-rust` (see
+[Configuration and data](../operations/configuration-and-data.md)). `main`
+opens it before building the app and passes it in `AppDeps`
+(`repo://src/main.rs#L51-L68`, `repo://src/app.rs#L193-L199`);
+[`App`](../architecture/app-state.md) keeps it as a private field
+(`repo://src/app.rs#L233`) and calls it synchronously from the event loop, so
+all writes happen on the loop thread. Failures come back as `Err` and end up
+either as a startup abort or in the `Errore storico: …` status line.
 
-## Opening
+## Opening and schema lifecycle
 
-- `History::open(path)` (`src/history.rs:67-76`) creates missing parent
-  directories, opens the file and runs the schema script.
-- `History::open_in_memory()` (`src/history.rs:82-88`) exists only under
+- `History::open(path)` (`repo://src/history.rs#L67-L76`) creates missing parent
+  directories, opens the file and delegates to `from_connection`.
+- `from_connection` (`repo://src/history.rs#L94-L100`) executes the whole schema
+  script, so every construction of a store also ensures the schema exists.
+- `History::open_in_memory()` (`repo://src/history.rs#L82-L88`) exists only under
   `#[cfg(test)]`.
-- The schema is applied with `CREATE TABLE/INDEX IF NOT EXISTS`
-  (`schema_sql`, `src/history.rs:397-436`); there are no migrations, so an
-  existing `history.db` simply gains new tables (such as `settings` and
-  `image_descriptions`) on next start.
+- The script is built by `schema_sql` from the table/index name constants
+  (`repo://src/history.rs#L415-L450`). It uses only `CREATE TABLE/INDEX IF NOT
+  EXISTS` and there are **no migrations**: an older `history.db` gains any
+  *new* table on the next start (for example `settings` and
+  `image_descriptions`), but a new column in an existing table is never added.
+  Resetting the local store therefore means deleting `history.db`
+  (`history_db_path`, see
+  [Configuration and data](../operations/configuration-and-data.md)).
 
 ## Schema
 
 | Table | Columns | Notes |
 |---|---|---|
-| `messages` | `message_id` (PK), `chat_jid`, `sender_jid`, `sender_name`, `text`, `timestamp`, `from_me`, `is_ai` | Index `idx_messages_chat_timestamp` on `(chat_jid, timestamp)` |
-| `chats` | `jid` (PK), `name`, `last_timestamp` (default 0), `autoreply` (default 0) | One row per chat |
+| `messages` | `message_id` (PK), `chat_jid`, `sender_jid`, `sender_name`, `text`, `timestamp`, `from_me`, `is_ai` | Only `sender_name` is nullable; index `idx_messages_chat_timestamp` on `(chat_jid, timestamp)` |
+| `chats` | `jid` (PK), `name`, `last_timestamp` (default 0), `autoreply` (default 0) | One row per chat; `name` is nullable and may stay empty |
 | `contacts` | `jid` (PK), `name` (NOT NULL) | Names from the address book / push names |
 | `settings` | `key` (PK), `value` (INTEGER NOT NULL) | App-wide flags; currently only `global_autoreply` |
-| `image_descriptions` | `message_id` (PK), `chat_jid`, `description` | AI description of an image message, written once per image |
+| `image_descriptions` | `message_id` (PK), `chat_jid`, `description` | AI description of an image message, one row per image |
 
-Rust types (`src/history.rs:28-55`): `StoredMessage`, `ChatSummary`
-(`jid`, `name`, `last_timestamp`, `autoreply`) and `Contact` (`jid`, `name`).
+Booleans are stored as `INTEGER` and read back as `bool` (`from_me`, `is_ai`,
+`chats.autoreply`, `settings.value`); timestamps are unix seconds. The Rust
+counterparts are `StoredMessage`, `ChatSummary` and `Contact`
+(`repo://src/history.rs#L29-L55`), and the default constants
+(`DEFAULT_LAST_TIMESTAMP`, `DEFAULT_AUTOREPLY`, `SETTING_GLOBAL_AUTOREPLY`,
+`DEFAULT_GLOBAL_AUTOREPLY`) sit at `repo://src/history.rs#L16-L23`.
+
+```mermaid
+erDiagram
+    messages {
+        TEXT message_id PK
+        TEXT chat_jid
+        TEXT sender_jid
+        TEXT sender_name
+        TEXT text
+        INTEGER timestamp
+        INTEGER from_me
+        INTEGER is_ai
+    }
+    chats {
+        TEXT jid PK
+        TEXT name
+        INTEGER last_timestamp
+        INTEGER autoreply
+    }
+    contacts {
+        TEXT jid PK
+        TEXT name
+    }
+    settings {
+        TEXT key PK
+        INTEGER value
+    }
+    image_descriptions {
+        TEXT message_id PK
+        TEXT chat_jid
+        TEXT description
+    }
+    chats ||--o{ messages : "chat_jid"
+    chats ||--o{ image_descriptions : "chat_jid"
+    messages ||--o| image_descriptions : "message_id"
+```
+
+The five tables and the join keys the code uses; `contacts` is matched to
+`chats` by the same `jid` value when a display name is resolved.
+
+No table declares a `FOREIGN KEY` and no `ON DELETE` cascade exists
+(`repo://src/history.rs#L415-L450`): referential integrity between `messages`,
+`image_descriptions` and `chats` is maintained entirely by the methods below,
+and `delete_chat` deletes the child rows explicitly.
 
 ## Saving messages and deduplication
 
-`save_message` (`src/history.rs:107-118`) runs in one transaction:
+`save_message` (`repo://src/history.rs#L107-L118`) runs in one
+`unchecked_transaction` (the method takes `&self`, so the transaction is taken
+from the shared connection):
 
-1. `INSERT OR IGNORE` into `messages` — the WhatsApp message id is the
-   primary key, so redelivered messages are ignored (`src/history.rs:438-463`).
-2. Only if the row was new, upsert the `chats` row
-   (`upsert_chat_for_message`, `src/history.rs:465-483`): `last_timestamp`
-   keeps the maximum seen and the name is set only when the chat has no
-   non-empty name yet.
+1. `INSERT OR IGNORE` into `messages` (`insert_message`,
+   `repo://src/history.rs#L456-L477`) — the WhatsApp message id is the primary
+   key, so the deduplication key space is global across chats.
+2. Only when a row was inserted, upsert the `chats` row
+   (`upsert_chat_for_message`, `repo://src/history.rs#L484-L497`):
+   `last_timestamp = MAX(last_timestamp, excluded.last_timestamp)` keeps the
+   maximum seen, and `name = COALESCE(NULLIF(name, ''), excluded.name)` sets the
+   name only while the chat has no non-empty one.
 3. Return `true` for a new message, `false` for a duplicate.
 
+A duplicate save changes **nothing**: not the text, not the flags, and not the
+chat row (the upsert is skipped entirely). The unit test
+`duplicate_message_id_is_ignored` asserts that a re-delivered message with a
+different body leaves the stored one intact
+(`repo://src/history.rs#L587-L599`).
+
 The chat name candidate is the trimmed sender push name of an **incoming**
 message; own messages never name a chat (`chat_name_candidate`,
-`src/history.rs:485-499`).
+`repo://src/history.rs#L503-L513`).
 
-The boolean return value drives the app: duplicates do not refresh the UI or
-trigger the [AI autoreply](../workflows/ai-autoreply.md).
+The boolean return value drives the app: `on_message` returns immediately on
+`false`, so history-sync replays and redeliveries neither refresh the UI nor
+trigger the [AI autoreply](../workflows/ai-autoreply.md)
+(`repo://src/app.rs#L485-L499`).
 
 ## Reading
 
-- `recent_messages(chat_jid, limit)` (`src/history.rs:124-142`): the latest
-  `limit` messages, queried newest first (`timestamp DESC, rowid DESC`) and
-  reversed to ascending order for display (the open chat view) and for the
-  last-message lookup of the startup catch-up.
-- `messages_since(chat_jid, since, limit)` (`src/history.rs:150-168`): the
-  messages with `timestamp >= since` (inclusive), capped to the `limit` most
-  recent ones (`timestamp DESC, rowid DESC LIMIT ?3`) and reversed to
-  ascending order. The [AI autoreply](../workflows/ai-autoreply.md) uses it to
-  load the context from local midnight of yesterday, at most
-  `CONTEXT_MESSAGES` messages.
-- `list_chats()` (`src/history.rs:175-191`): all chats ordered by
-  `last_timestamp DESC`; the name is the contact name, else the chat name,
-  else the user part of the JID (text before `@`).
-- `list_contacts()` (`src/history.rs:217-236`): ordered by name,
-  case-insensitive.
-- `display_name(jid)` (`src/history.rs:242-253`): same contact → chat → JID
-  user part fallback, used for the open chat title.
+- `recent_messages(chat_jid, limit)` (`repo://src/history.rs#L124-L142`): the
+  latest `limit` messages, queried newest first (`timestamp DESC, rowid DESC`)
+  and reversed to ascending order. Used for the open chat view
+  (`MESSAGES_SHOWN`), the last-message lookup of the startup catch-up, the
+  "owner answered meanwhile" check and the AI context cap.
+- `messages_since(chat_jid, since, limit)` (`repo://src/history.rs#L150-L168`):
+  the messages with `timestamp >= since` (inclusive), capped to the `limit` most
+  recent ones (`timestamp DESC, rowid DESC LIMIT ?3`) and reversed to ascending
+  order. The [AI autoreply](../workflows/ai-autoreply.md) uses it to load the
+  context from local midnight of yesterday, at most `CONTEXT_MESSAGES` messages
+  (`repo://src/app.rs#L991-L998`).
+- `list_chats()` (`repo://src/history.rs#L175-L191`): every `chats` row ordered
+  by `last_timestamp DESC, jid`; the name is the contact name, else the chat
+  name, else the user part of the JID, resolved in `chat_summary_from_row`
+  (`repo://src/history.rs#L544-L555`).
+- `list_contacts()` (`repo://src/history.rs#L217-L236`): ordered by
+  `name COLLATE NOCASE, jid`.
+- `display_name(jid)` (`repo://src/history.rs#L242-L253`): the same contact →
+  chat → JID-user-part fallback (`jid_user_part`,
+  `repo://src/history.rs#L519-L521`), used for the open chat title, status
+  lines and the AI chat description.
+
+The list-style read queries are prepared with `prepare_cached` and the point
+lookups (`display_name`, `is_autoreply`, `is_global_autoreply`,
+`message_exists`) use `query_row`, all on the single shared connection.
 
 ## Contacts
 
-`upsert_contact` (`src/history.rs:197-211`) inserts or renames a contact and
-ignores blank names, so an empty push name never erases a known name.
+`upsert_contact` (`repo://src/history.rs#L197-L211`) inserts or renames a
+contact and ignores blank names, so an empty push name never erases a known
+name. `on_contact` stores the contact and reloads the lists
+(`repo://src/app.rs#L465-L477`).
 
 ## Autoreply flags
 
-- `set_autoreply(chat_jid, enabled)` (`src/history.rs:259-270`) upserts the
-  `chats` row, creating it with defaults if the chat has no messages yet.
-- `is_autoreply(chat_jid)` (`src/history.rs:276-285`) returns the stored
-  flag or `false` when the chat is unknown.
+- `set_autoreply(chat_jid, enabled)` (`repo://src/history.rs#L259-L270`) upserts
+  the `chats` row, creating it with defaults when the chat has no messages yet.
+- `is_autoreply(chat_jid)` (`repo://src/history.rs#L276-L285`) returns the stored
+  flag, or `false` for an unknown chat.
 - `set_global_autoreply(enabled)` / `is_global_autoreply()`
-  (`src/history.rs:291-317`) upsert and read the `global_autoreply` row of
-  `settings`; the default is `false` when it was never stored. The global
+  (`repo://src/history.rs#L291-L317`) upsert and read the `global_autoreply` row
+  of `settings`; the default is `false` when it was never stored. The global
   flag is independent of the per-chat flags: toggling it never changes a
-  `chats.autoreply` value.
+  `chats.autoreply` value, and a chat is answered when either is on
+  (`repo://src/app.rs#L854-L860`).
+
+Two consequences of `set_autoreply` upserting the row: a chat toggled on from
+the contacts list exists in `chats` with `last_timestamp = 0` and no messages,
+so it is listed (sorted last, named by the JID user part until a contact or a
+message names it), and the startup catch-up skips it because it has no last
+message to inspect (`repo://src/app.rs#L964-L981`). New messages never reset the
+per-chat flag, because the message upsert only touches `last_timestamp` and
+`name`.
+
+## Stored message bodies: write-once, except transcriptions
+
+A message body is written once, by `save_message`. The insert is
+`INSERT OR IGNORE`, so a second save of the same id cannot change `text`, and no
+other method writes the column — `upsert_chat_for_message` touches only
+`chats`. The single exception is `update_message_text`:
+
+```rust
+UPDATE messages SET text = ?1 WHERE message_id = ?2
+```
+
+`update_message_text(message_id, new_text)`
+(`repo://src/history.rs#L338-L346`) replaces the body of an existing message and
+returns `true` when a row matched the id, `false` when the id is not stored (in
+which case nothing is written). It is not scoped by chat or sender and it
+touches no other column, so `timestamp`, `from_me`, `is_ai` and the chat row are
+preserved.
 
-New messages never reset the per-chat flag, because the message upsert only
-touches `last_timestamp` and `name`.
+The only caller is the transcription step of the
+[AI autoreply](../workflows/ai-autoreply.md) workflow
+(`repo://src/app.rs#L784-L801`). For each transcribed audio whose message still
+exists it stores
+
+```
+[messaggio vocale] <transcription>
+```
+
+i.e. the constant `VOICE_NOTE_TEXT` (`repo://src/app.rs#L38-L41`, a deliberate
+duplicate of the private `VOICE_PLACEHOLDER` in
+`repo://src/whatsapp.rs#L45-L46`), a space, and the transcript. The previous
+body was exactly `[messaggio vocale]`, so this is an **overwrite** with the
+placeholder re-created as a prefix, not an append to arbitrary text. The write is
+guarded by `message_exists`, and the `bool` returned by `update_message_text` is
+discarded because of that guard.
+
+There is no transcription column and no transcription table: the transcript
+lives only inside `messages.text`. Deleting the chat (or its messages) therefore
+discards the transcripts too, and the text after the placeholder is the only
+record that an audio was transcribed.
+
+### Why the placeholder stops matching
+
+`is_voice_note` returns `true` only when the trimmed text equals
+`[messaggio vocale]` exactly (`repo://src/whatsapp.rs#L1174-L1181`). Once a
+transcription is stored the body is `[messaggio vocale] <transcription>`, which
+no longer matches, and two consumers depend on that:
+
+- `audios_to_transcribe` treats a message as already transcribed when an audio
+  is in progress **or** its stored text is no longer the plain placeholder
+  (`repo://src/app.rs#L900-L918`), so each audio is transcribed once even though
+  nothing else records it — and an audio whose transcription failed keeps the
+  bare placeholder.
+- `user_content` in `src/autoreply.rs` replaces the body with the
+  "the contact sent a voice message you cannot listen to" sentinel only while it
+  still matches, otherwise it passes the stored text through
+  (`repo://src/autoreply.rs#L364-L379`). A transcribed voice note therefore
+  reaches the model as `[messaggio vocale] <transcription>` in that reply and in
+  every later context built from the store.
+
+```mermaid
+flowchart TD
+    A["audio message saved with text [messaggio vocale]"] --> B["is_voice_note true, AI sees the cannot-listen sentinel"]
+    B --> C["transcription succeeds, message_exists true"]
+    C --> D["update_message_text stores [messaggio vocale] plus transcript"]
+    D --> E["is_voice_note false, AI sees the transcript"]
+    E --> F["audios_to_transcribe skips it as done"]
+    C -->|"chat deleted meanwhile"| G["nothing written, no row"]
+```
+
+The body-mutation lifecycle that makes a transcription a one-time, permanent
+part of the stored message.
 
 ## Image descriptions
 
 The [AI autoreply](../workflows/ai-autoreply.md) asks the vision model to
-describe each image once and stores the text, so later replies reuse it
-instead of downloading and describing the image again:
+describe each image once and stores the text, so later replies reuse it instead
+of downloading and describing the image again:
 
-- `message_exists(message_id)` (`src/history.rs:319-332`) tells whether a
-  message is still stored; the app checks it before saving a description, so
-  a chat deleted while its images were being described gets no orphan rows.
+- `message_exists(message_id)` (`repo://src/history.rs#L323-L332`) tells whether
+  a message is still stored. The app checks it before saving a description
+  (`repo://src/app.rs#L747-L759`), so a chat deleted while its images were being
+  described gets no orphan rows.
 - `save_image_description(message_id, chat_jid, description)`
-  (`src/history.rs:334-349`) upserts the row, replacing a previous
+  (`repo://src/history.rs#L352-L363`) upserts the row, replacing a previous
   description of the same message.
-- `image_descriptions(chat_jid)` (`src/history.rs:351-367`) returns the
+- `image_descriptions(chat_jid)` (`repo://src/history.rs#L369-L381`) returns the
   chat's descriptions keyed by message id; the app uses it to skip images
-  already described and to turn image messages into text when building the
-  AI context.
+  already described (`repo://src/app.rs#L882-L898`) and to turn image messages
+  into text when building the AI context
+  (`repo://src/app.rs#L1009-L1028`).
+
+Descriptions are separate from `messages.text`, which keeps only the image
+placeholder plus optional caption rendered by `src/whatsapp.rs`
+(`repo://src/whatsapp.rs#L1085-L1103`); editing a body would never touch them,
+and only `delete_chat` removes them.
 
 ## Deleting a chat
 
-`delete_chat(chat_jid)` (`src/history.rs:369-394`) removes, in one
-transaction, all messages of the chat, their image descriptions and its
-`chats` row (and therefore its per-chat autoreply flag). The contact row, other chats and the WhatsApp
-session are untouched; deleting an unknown chat is not an error, and a later
-message simply recreates the chat. The app calls it after a double Delete
-press in the chat list (see [Application state](../architecture/app-state.md)).
+`delete_chat(chat_jid)` (`repo://src/history.rs#L389-L408`) removes, in one
+transaction, all messages of the chat, all their image descriptions and the
+`chats` row (and therefore its per-chat autoreply flag). Because the
+deletion is by `chat_jid`, it covers the transcripts and image descriptions
+stored for that chat as well. The contact row, other chats, `settings` and the
+WhatsApp session are untouched; deleting an unknown chat is not an error, and a
+later message recreates the chat row through the message upsert.
+
+The app calls it after a double Delete press in the chat list; `confirm_delete`
+also drops the scheduled reply and the kept images/audios of that chat and
+closes it if open (`repo://src/app.rs#L1249-L1272`).
 
 ## Tests
 
-`src/history.rs:543` onward covers duplicate ids, limit and ordering,
-`messages_since` (boundary inclusion, limit keeping the most recent,
-ascending order), flag and sender-name round trips, chat ordering, name
-fallbacks, chat naming rules, per-chat autoreply defaults and persistence,
-the global autoreply default, toggle and independence from chat flags,
-`message_exists`, image descriptions (per chat, replaced, deleted with the
-chat), `delete_chat` (messages and row gone, contact and other chats kept), contact
-ordering and blank names, and on-disk persistence with parent directory
-creation. See [Unit tests](../testing/unit-tests.md).
+The inline module (`repo://src/history.rs#L557-L879`) covers duplicate ids and
+the untouched stored body, limit and ordering of `recent_messages`,
+`messages_since` (boundary inclusion, limit keeping the most recent, ascending
+order), flag and sender-name round trips, chat ordering, name fallbacks, chat
+naming rules (set once, never from own messages), per-chat autoreply defaults,
+persistence and survival of incoming messages, the global autoreply default,
+toggle and independence from chat flags, `message_exists` across a chat
+deletion, `update_message_text` (body changed, missing id reported as `false`),
+image descriptions (per chat, replaced, deleted with the chat), `delete_chat`
+(messages and row gone, contact and other chats kept), contact ordering and
+blank names, and on-disk persistence with parent directory creation. See
+[Unit tests](../testing/unit-tests.md).

+ 1 - 1
openwiki/concepts/index.md

@@ -1,3 +1,3 @@
 # Files
 
-- [Message history store](history-store.md) - The local SQLite database in src/history.rs that stores messages, chat summaries, contacts, the per-chat AI autoreply flag, the global autoreply setting and the AI descriptions of images, and supports local chat deletion.
+- [Message history store](history-store.md) - The local SQLite database in src/history.rs — its schema and no-migration lifecycle, the deduplicating save, the read queries, contacts, the per-chat and global autoreply flags, image descriptions, chat deletion, and update_message_text as the only in-place rewrite of a stored message body (voice-note transcriptions stored inside messages.text).

+ 1 - 1
openwiki/index.md

@@ -4,7 +4,7 @@ okf_version: "0.2"
 
 # Files
 
-- [Quickstart](quickstart.md) - What the wh WhatsApp terminal client with per-chat and global AI autoreply (system prompt in SOUL.md, reply delay, image descriptions), live configuration reload, re-pairing with F8 and optional web control panel does, how to build and run it, and where to find each topic in this wiki.
+- [Quickstart](quickstart.md) - What the wh WhatsApp terminal client with per-chat and global AI autoreply (system prompt in SOUL.md, reply delay, image descriptions, voice-note transcriptions) does, how to build and run it, and where to find each topic in this wiki.
 
 # Directories
 

+ 102 - 49
openwiki/integrations/ai-client.md

@@ -1,8 +1,8 @@
 ---
 type: integration
 title: AI chat-completions client
-description: The OpenAI-compatible HTTP client in src/ai.rs — text completions offering the no_reply tool, image descriptions with the vision model (request text from VISION.md), image validation, timeouts, retries with linear backoff, error handling, the optional request log and reply parsing.
-tags: [ai, openai, reqwest, vision, http, retry, tools]
+description: The OpenAI-compatible HTTP client in src/ai.rs — text completions offering the no_reply tool, image descriptions with the vision model (request text from VISION.md), image validation, timeouts, retries with linear backoff, error handling, the optional request log and reply parsing — plus the OpenAI-compatible audio transcription client in src/transcribe.rs.
+tags: [ai, openai, reqwest, vision, http, retry, tools, transcription, whisper]
 sources:
   - id: openwiki-source-651d1fb6c9e49916a916ab51
     resource: repo://Cargo.toml
@@ -10,7 +10,7 @@ sources:
     resource: repo://src/ai.rs
   - id: openwiki-source-2a737474d86fc75cc9d9694f
     resource: repo://src/config.rs
-generated: { by: "claude-code", at: "2026-10-23T21:26:24.156Z" }
+generated: { by: "openwiki/0.7.2", at: "2026-10-24T16:42:08.550Z" }
 verified:
   - by: openwiki/0.7.2
     at: 2026-10-23T21:26:24.156Z
@@ -20,23 +20,24 @@ verified:
 
 `src/ai.rs` implements `AiClient`, a small client for any
 OpenAI-compatible `POST <base_url>/chat/completions` endpoint. It is used by
-the [AI autoreply workflow](../workflows/ai-autoreply.md); the conversation
-turns themselves are built elsewhere.
+the [AI autoreply](../workflows/ai-autoreply.md). Voice notes go through a
+separate OpenAI-compatible `audio/transcriptions` endpoint, implemented in
+`src/transcribe.rs` (see below).
 
 ## Types
 
-- `Reply` (`src/ai.rs:51-58`): the successful outcome of a completion —
+- `Reply` (`src/ai.rs:53-58`): the successful outcome of a completion —
   `Text(String)` to send, or `NoReply` when the model called the `no_reply`
   tool.
-- `Role` (`src/ai.rs:60-80`): `System`, `User`, `Assistant`, serialised as
+- `Role` (`src/ai.rs:62-81`): `System`, `User`, `Assistant`, serialised as
   `"system"`, `"user"`, `"assistant"`.
-- `ChatTurn { role, content }` (`src/ai.rs:82-87`): one message of the
+- `ChatTurn { role, content }` (`src/ai.rs:84-87`): one message of the
   conversation.
-- `ImageInput { mime, bytes }` (`src/ai.rs:141-146`): an image to describe.
-- `AiClient` (`src/ai.rs:89-103`): `reqwest::Client`, endpoint, API key,
+- `ImageInput { mime, bytes }` (`src/ai.rs:143-146`): an image to describe.
+- `AiClient` (`src/ai.rs:91-103`): `reqwest::Client`, endpoint, API key,
   model, optional vision model, the image description request, optional log
   path and the retry count.
-- `StatusError { status, body }` (`src/ai.rs:105-122`): the typed error of a
+- `StatusError { status, body }` (`src/ai.rs:107-122`): the typed error of a
   non-2xx response, kept typed so the retry logic can inspect the status.
 
 Both `AiClient` and `ImageInput` have hand-written `Debug` impls that omit the
@@ -61,7 +62,7 @@ API key and the image bytes (`src/ai.rs:124-160`).
 - copies `openai_model`, `openai_vision_model` and `ai_retries`;
 - takes the image description request from `VISION.md`
   (`Config::image_description_prompt`), falling back to the built-in
-  `DEFAULT_IMAGE_DESCRIPTION_REQUEST` (`src/ai.rs:22-23`), an Italian request
+  `DEFAULT_IMAGE_DESCRIPTION_REQUEST` (`src/ai.rs:23`), an Italian request
   for an objective, concise description of subjects, actions, readable text
   and context;
 - sets the log path to `Config::ai_log_path()` only when `ai_log` is true.
@@ -73,11 +74,11 @@ provider installation runs again and is a no-op after the first time. Defaults a
 are described in
 [Configuration and data](../operations/configuration-and-data.md)
 (default base URL `https://ollama.com/v1`, default model
-`deepseek-v4.1-flash`, default `ai_retries` 2, `src/config.rs:34-43`).
+`deepseek-v4.1-flash`, default `ai_retries` 2, `src/config.rs:39-47`).
 
 ## Text completions and the `no_reply` tool
 
-`complete(&turns)` (`src/ai.rs:204-213`) sends
+`complete(&turns)` (`src/ai.rs:211-213`) sends
 
 ```json
 { "model": "<model>",
@@ -88,49 +89,49 @@ are described in
 ```
 
 built by `build_request_body`, `build_tools` and `build_messages`
-(`src/ai.rs:348-384`). The only tool offered is the parameterless `no_reply`
+(`src/ai.rs:353-389`). The only tool offered is the parameterless `no_reply`
 function; its description is neutral ("does not send any message in the
 chat: call it instead of writing a reply") and never says *when* to use it —
 that decision belongs to the system prompt in `SOUL.md`
-(`src/ai.rs:42-49`). The client never executes the tool: calling it simply
+(`src/ai.rs:44-49`). The client never executes the tool: calling it simply
 means "send nothing".
 
 ## Image descriptions
 
-`has_vision()` (`src/ai.rs:215-221`) is true when a vision model is
-configured. `describe_image(&image)` (`src/ai.rs:223-241`) asks the vision
+`has_vision()` (`src/ai.rs:219-221`) is true when a vision model is
+configured. `describe_image(&image)` (`src/ai.rs:230-241`) asks the vision
 model for a description of **one** image, independently of any
 conversation:
 
 1. fails with `modello vision non configurato` without a vision model;
-2. validates the image (`validate_image`, `src/ai.rs:413-432`): empty →
+2. validates the image (`validate_image`, `src/ai.rs:418-432`): empty →
    `immagine vuota`, larger than `MAX_IMAGE_BYTES` (5 MiB) →
    `immagine troppo grande: N byte` (errors mention the size only, never the
    data);
 3. sends a single user message `[{"type":"text", <description request>},
    {"type":"image_url", ...}]` to the vision model, **without tools**
-   (`build_description_body`, `src/ai.rs:386-397`);
+   (`build_description_body`, `src/ai.rs:391-402`);
 4. returns the description text; a `NoReply` outcome is turned into the
    `risposta del servizio AI vuota` error.
 
 The image is embedded as a `data:<mime>;base64,<...>` URL
-(`build_image_part`, `src/ai.rs:399-411`). Validation errors happen before any
+(`build_image_part`, `src/ai.rs:404-416`). Validation errors happen before any
 request and are not retried. The caller stores the description in the
 [history store](../concepts/history-store.md), so each image is described
 once and later replies see only the text.
 
 ## Retries
 
-Both request kinds go through `send_with_retry` (`src/ai.rs:243-264`),
+Both request kinds go through `send_with_retry` (`src/ai.rs:250-264`),
 which performs up to `1 + ai_retries` attempts with the same body:
 
 - after a failed attempt it retries only while attempts remain and
-  `is_retryable` (`src/ai.rs:322-337`) accepts the error: HTTP 429 and 5xx
-  responses and every non-status failure (transport errors, timeouts, body
-  read errors, invalid JSON, missing or empty reply) are retried; any other
-  4xx response (e.g. 401, 400) fails immediately;
+  `is_retryable` (`src/ai.rs:329-342`) says so: non-2xx statuses other than
+  429 fail immediately (401, 400, ...); 429, 5xx and any non-status failure
+  (transport errors, timeouts, body read errors, invalid JSON, missing or
+  empty reply) are retried;
 - before retry number *n* it sleeps `retry_delay(n)` = 2 s × n
-  (`src/ai.rs:339-346`), i.e. 2 s, 4 s, … (linear backoff);
+  (`src/ai.rs:344-351`), i.e. 2 s, 4 s, … (linear backoff);
 - the returned error is the one of the last attempt.
 
 `ai_retries = 0` disables retrying. Each attempt is a separate HTTP request
@@ -138,8 +139,8 @@ and, when enabled, a separate log record.
 
 ## Sending and error handling
 
-`send_body` (`src/ai.rs:266-285`) delegates to `request_reply`
-(`src/ai.rs:287-319`), which posts JSON with bearer auth and returns
+`send_body` (`src/ai.rs:276-285`) delegates to `request_reply`
+(`src/ai.rs:293-319`), which posts JSON with bearer auth and returns
 Italian `anyhow` errors:
 
 | Failure | Message |
@@ -150,53 +151,105 @@ Italian `anyhow` errors:
 | Invalid JSON | `risposta del servizio AI non valida: <body>` |
 
 Response bodies in errors are truncated to 300 characters on a character
-boundary (`truncate_chars`, `src/ai.rs:573-579`); the API key is never
+boundary (`truncate_chars`, `src/ai.rs:577-583`); the API key is never
 included.
 
 ## Optional request log
 
 When `ai_log = true` in `config.toml` (default `false`), `send_body` appends
 one record per attempt to `ai_requests.log` in the data directory
-(`Config::ai_log_path`, `src/config.rs:333-339`) after the attempt completes.
-`format_log_record` (`src/ai.rs:526-552`) writes a header
+(`Config::ai_log_path`, `src/config.rs:405-409`) after the attempt completes.
+`format_log_record` (`src/ai.rs:534-556`) writes a header
 `===== <local timestamp> | <endpoint> | modello: <model> =====`, the
 pretty-printed request body and the outcome labelled `RISPOSTA` or `ERRORE`;
 a `NoReply` outcome is logged as `RISPOSTA` with the text
 `(tool no_reply: nessun messaggio inviato)`.
 
 - Image data URLs are replaced by `data:<mime>;base64,<omessi N byte>`
-  (`redact_images` / `redact_data_url`, `src/ai.rs:478-524`); text parts and
+  (`redact_images` / `redact_data_url`, `src/ai.rs:484-524`); text parts and
   remote URLs are kept.
 - The API key is never written (it is only in the bearer header, not in the
   body).
 - The file is opened in append mode and created if missing
-  (`append_to_file`, `src/ai.rs:561-571`); write failures are ignored so
+  (`append_to_file`, `src/ai.rs:565-575`); write failures are ignored so
   logging never makes a reply fail and never writes over the TUI.
 
 ## Reply parsing
 
-`parse_reply` (`src/ai.rs:434-482`) looks at `choices[0]`:
+`parse_reply` (`src/ai.rs:440-465`) looks at `choices[0]`:
 
 - if any of its `message.tool_calls` is a function call named `no_reply`
-  (`calls_no_reply`), the result is `Reply::NoReply`, even when the message
-  also carries text;
+  (`calls_no_reply`, `src/ai.rs:467-479`), the result is `Reply::NoReply`,
+  even when the message also carries text;
 - otherwise it takes `message.content`, trims it and returns `Reply::Text`;
   calls to other tool names are ignored;
 - it fails with `risposta del servizio AI senza choices` when there are no
   choices, or `risposta del servizio AI vuota` when the content is missing
   or blank.
 
+## Audio transcription
+
+`src/transcribe.rs` implements the separate transcription of voice notes by
+any OpenAI-compatible `audio/transcriptions` endpoint. The
+[AI autoreply](../workflows/ai-autoreply.md) downloads each kept audio and
+calls `transcribe`, then appends the text to the stored message.
+
+- `TranscriptionJob` (`src/transcribe.rs:59-71`): endpoint URL, API key,
+  model, language and the MIME type of the audio bytes. It is built by the
+  app from `transcribe_url`, `transcribe_key`, `transcribe_model` and
+  `transcribe_language` (defaults: Groq `audio/transcriptions`,
+  `whisper-large-v3-turbo`, `auto`; see
+  [Configuration and data](../operations/configuration-and-data.md),
+  `src/config.rs:49-53`).
+- `transcribe(job, bytes)` (`src/transcribe.rs:81-109`) validates the audio
+  (`validate_audio`, `src/transcribe.rs:270-281`: empty →
+  `audio vuota: impossibile trascrivere`, larger than `MAX_AUDIO_BYTES`
+  = 25 MiB → `audio troppo grande: N byte`) **before any request**, then
+  POSTs a `multipart/form-data` body to `job.url` with bearer auth. It
+  performs a single request — no retries.
+- The multipart body (`multipart_body`, `src/transcribe.rs:201-231`) always
+  contains the `model` field; the `language` field is sent only when it is
+  explicit — blank or `auto` (any case) makes the endpoint detect the
+  language by itself (`request_fields`, `is_explicit_language`,
+  `src/transcribe.rs:177-193`). The file part is named
+  `audio.<extension>`, where the extension comes from the MIME type
+  (`audio_suffix`, `src/transcribe.rs:151-170`: `ogg`, `mp3`, `mp4`, `m4a`,
+  `aac`, `wav`, otherwise `bin`). The MIME part header is sanitised to safe
+  single-line printable ASCII (`header_mime`, `src/transcribe.rs:239-248`),
+  so a hostile MIME cannot inject extra multipart headers. The boundary is
+  unique per request (process id plus an atomic counter,
+  `src/transcribe.rs:284-287`).
+- The HTTP client is a process-wide `OnceLock<reqwest::Client>` with a 60 s
+  request timeout (`client`, `build_client`,
+  `src/transcribe.rs:111-129`); it relies on the rustls provider already
+  installed by `AiClient::new`, so it installs none itself.
+- `read_response` (`src/transcribe.rs:134-148`) fails non-2xx replies with
+  `servizio di trascrizione ha risposto <status>: <body>` (body truncated to
+  300 characters, key never included); `parse_transcription`
+  (`src/transcribe.rs:251-267`) extracts the trimmed `text` field and fails
+  with `risposta del servizio di trascrizione non valida: <body>` or
+  `...senza testo` on malformed or empty replies. The audio bytes are never
+  logged or stored.
+
 ## Tests
 
-`src/ai.rs:581` onward covers the description body (request text plus image,
-no tools), the `VISION.md` request and its default, missing vision model,
-oversize/empty images (without leaking data), the exact size limit,
-`has_vision`, `Debug` redaction, request body shape including `tools`, the
-`no_reply` tool definition, reply parsing (trimmed text, `no_reply` with or
-without text, other tool calls ignored, missing choices/content), the
-missing-key error, endpoint construction, truncation, the request log (image
-redaction, no key or base64 in records, `no_reply` records, logging only when
-enabled, append-only writes) and the retry policy (`is_retryable` refuses 4xx
-except 429 and accepts generic errors, the `StatusError` message is
-unchanged, `retry_delay` grows linearly). See
-[Unit tests](../testing/unit-tests.md).
+- `src/ai.rs:589` onward covers the description body (request text plus
+  image, no tools), the `VISION.md` request and its default, missing vision
+  model, oversize/empty images (without leaking data), the exact size limit,
+  `has_vision`, `Debug` redaction, request body shape including `tools`, the
+  `no_reply` tool definition, reply parsing (trimmed text, `no_reply` with
+  or without text, other tool calls ignored, missing choices/content), the
+  missing-key error, endpoint construction, truncation, the request log
+  (image redaction, no key or base64 in records, `no_reply` records, logging
+  only when enabled, append-only writes) and the retry policy (`is_retryable`
+  refuses 4xx except 429 and accepts generic errors, the `StatusError`
+  message is unchanged, `retry_delay` grows linearly).
+- `src/transcribe.rs:299` onward covers the multipart body (fields before
+  the file part, file headers and terminator, bytes appended unchanged),
+  the language rules (`auto`/blank/whitespace omitted, explicit sent once),
+  `audio_suffix` mappings (including the codecs parameter), MIME
+  sanitisation against header injection, boundary uniqueness, audio
+  validation (empty, oversize) and transcription parsing (trimmed, blank or
+  missing text).
+
+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.
+- [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, image download and sending.
+- [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.

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

@@ -12,7 +12,7 @@ sources:
     resource: repo://src/web_page.html
   - id: openwiki-source-7b75006986d8b0337adc6ca0
     resource: repo://src/web.rs
-generated: { by: "claude-code", at: "2026-10-23T21:26:24.156Z" }
+generated: { by: "openwiki/0.7.2", at: "2026-10-24T16:42:08.550Z" }
 verified:
   - by: openwiki/0.7.2
     at: 2026-10-23T21:26:24.156Z
@@ -26,19 +26,19 @@ 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:1-2`, `src/web.rs:16-19`).
+`src/web.rs:17-19`).
 
 ## Startup and threading
 
 `main` calls `start_web_panel`, which calls `web::start(port, app_tx)` only
 when `web_port` is set ([Runtime and event loop](../architecture/runtime-and-event-loop.md)).
-`start` (`src/web.rs:185-199`) binds a `tiny_http::Server` on
+`start` (`src/web.rs:191-203`) 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:201-212`) handles requests **one at a time** on that
+`serve` (`src/web.rs:205-217`) 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 +46,8 @@ QR (`Cargo.toml`).
 
 ## Routes
 
-`resolve_route` (`src/web.rs:348-362`) matches method and path; the query
-string is ignored (`path_without_query`):
+`resolve_route` (`src/web.rs:352-367`) matches method and path; the query
+string is ignored (`path_without_query`, `src/web.rs:344-348`):
 
 | Method | Path | Effect | Response |
 |---|---|---|---|
@@ -59,7 +59,7 @@ string is ignored (`path_without_query`):
 | POST | `/api/quit` | `WebCommand::Quit` | 200 `{"status":"Bot in chiusura"}` |
 | other | other | — | 404 `non trovato` |
 
-The JSON state is `WebState` (`src/web.rs:123-136`):
+The JSON state is `WebState` (`src/web.rs:125-136`):
 
 ```json
 { "global_autoreply": false,
@@ -70,8 +70,8 @@ The JSON state is `WebState` (`src/web.rs:123-136`):
 ```
 
 `screen` mirrors the TUI screen (`SCREEN_CONNECTING`, `SCREEN_PAIRING`,
-`SCREEN_MAIN`, `src/web.rs:34-39`). `qr_svg` is `null` unless the app holds
-a pairing QR payload; then `qr_svg` (`src/web.rs:364-379`) renders it as an
+`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
 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).
@@ -82,19 +82,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:166-174`, `src/web.rs:214-231`,
-  `src/web.rs:381-390`). A cross-site page cannot add a custom header
+  `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
   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:392-409`).
+  (`read_limited_body`, `src/web.rs:397-414`).
 
 ## Bridge to the application
 
-The server never touches application state. `forward` (`src/web.rs:275-288`)
+The server never touches application state. `forward` (`src/web.rs:279-292`)
 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
@@ -124,7 +124,7 @@ If the event channel is closed or the reply is dropped, the server answers
 
 ### Quit
 
-`handle_quit` (`src/web.rs:256-273`) first writes the 200 response, then
+`handle_quit` (`src/web.rs:261-277`) 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.
@@ -138,28 +138,28 @@ button, a pairing card, one switch per chat 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:619-628`,
-  `src/web_page.html:739-763`);
-- the pairing card (`renderPairing`, `src/web_page.html:600-617`) is hidden
+  (`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
   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;
 - every POST carries `X-Wh-Request: 1`; the chat toggle sends the JID as a
-  `text/plain` body (`buildHeaders`, `src/web_page.html:441-450`);
+  `text/plain` body (`buildHeaders`, `src/web_page.html:445-450`);
 - while a toggle is in flight its switch is disabled and keeps the user's
   position; polling is paused, and responses are applied in request order
   through a ticket counter so a stale poll cannot overwrite a newer toggle
-  result (`src/web_page.html:643-700`);
+  result (`applyState`, `runToggle`, `src/web_page.html:647-701`);
 - names are inserted with `textContent` only;
-- "Nuovo QR" (`relinkBot`, `src/web_page.html:703-718`) asks for
+- "Nuovo QR" (`relinkBot`, `src/web_page.html:707-718`) 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-510`, `src/web_page.html:720-737`).
+  (`src/web_page.html:485-511`, `src/web_page.html:724-737`).
 
 ## Security and operations
 
@@ -178,7 +178,7 @@ button, a pairing card, one switch per chat and a status bar:
 
 ## Tests
 
-`src/web.rs:411-503` covers query stripping, route resolution (known routes
+`src/web.rs:415-502` 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`

+ 58 - 53
openwiki/integrations/whatsapp-client.md

@@ -1,17 +1,14 @@
 ---
 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, image download and sending.
-tags: [whatsapp, whatsapp-rust, events, jid, history-sync, relink, session]
+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]
 sources:
   - id: openwiki-source-651d1fb6c9e49916a916ab51
     resource: repo://Cargo.toml
   - id: openwiki-source-7ea5eac45e5030178a6ea05b
     resource: repo://src/whatsapp.rs
-generated: { by: "claude-code", at: "2026-10-23T21:26:24.156Z" }
-verified:
-  - by: openwiki/0.7.2
-    at: 2026-10-23T21:26:24.156Z
+generated: { by: "opencode", at: "2026-10-24T16:39:50.206Z" }
 ---
 
 # WhatsApp client integration
@@ -24,8 +21,8 @@ verified:
 - 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, query its own JIDs and replace the session
-  with a new unpaired one (`relink`).
+  send text, download images and audios, 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
@@ -33,23 +30,23 @@ also uses it directly to empty the session database during a relink.
 
 ## Session and handle
 
-`WaHandle` (`src/whatsapp.rs:160-170`) holds the running `Session` in an
+`WaHandle` (`src/whatsapp.rs:196-203`) 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:172-180`)
+started from one clone is seen by all. A `Session` (`src/whatsapp.rs:206-213`)
 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:200-210`), which fails with
+(`src/whatsapp.rs:238-243`), 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:297-312`) calls
+`start(session_db, events)` (`src/whatsapp.rs:358-370`) calls
 `open_session` and wraps the result in a new `WaHandle`. `open_session`
-(`src/whatsapp.rs:369-416`):
+(`src/whatsapp.rs:430-471`):
 
 1. requires a UTF-8 session path and creates its parent directory;
 2. opens the session store with `SqliteStore::new` (`session.db`, see
@@ -59,9 +56,9 @@ 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:417-439`) only pushes raw events, in
+`EventForwarder` (`src/whatsapp.rs:472-495`) only pushes raw events, in
 order, into an internal unbounded channel; its interest set is
-`SUBSCRIBED_KINDS` (`src/whatsapp.rs:54-74`), which includes
+`SUBSCRIBED_KINDS` (`src/whatsapp.rs:58-75`), which includes
 `OfflineSyncCompleted` and `AppStateSyncFailed`. All translation happens in
 `Translator`, which also owns a name cache.
 
@@ -71,19 +68,19 @@ whatsapp-rust Bot --raw events--> EventForwarder --chan--> Translator --WaEvent-
 
 ## Relink
 
-`WaHandle::relink` (`src/whatsapp.rs:221-237`) replaces the session with a
+`WaHandle::relink` (`src/whatsapp.rs:261-270`) 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:182-198`): `logout` (deregisters
+   calls `Session::stop` (`src/whatsapp.rs:223-231`): `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:314-329`) deletes `session.db`
+2. `reset_session_files` (`src/whatsapp.rs:374-386`) 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:349-367`)
+   process exits — `clear_session_tables` (`src/whatsapp.rs:407-428`)
    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;
@@ -96,7 +93,7 @@ successful relink (sends then fail with `WhatsApp session not running`).
 
 ## WaEvent
 
-`WaEvent` (`src/whatsapp.rs:104-130`):
+`WaEvent` (`src/whatsapp.rs:109-135`):
 
 | Variant | Meaning |
 |---|---|
@@ -105,11 +102,11 @@ successful relink (sends then fail with `WhatsApp session not running`).
 | `Disconnected(String)` | Connection lost, with reason |
 | `LoggedOut` | Device unlinked; pairing must restart |
 | `OfflineSyncCompleted` | All messages received while the app was offline have been delivered (and, being sequential, already emitted as `Message`); triggers the startup [catch-up](../workflows/ai-autoreply.md) |
-| `Message { message, image }` | A `StoredMessage` (live or history sync); `image` is `Some` only for live image messages |
+| `Message { message, image, audio }` | A `StoredMessage` (live, history sync or phone-sent); `image` / `audio` are `Some` only for live media messages (never history sync) |
 | `Contact(Contact)` | A new or better contact name |
 | `Error(String)` | Non-fatal problem shown in the status line |
 
-`Translator::handle_event` (`src/whatsapp.rs:585-634`) maps raw events to
+`Translator::handle_event` (`src/whatsapp.rs:641-690`) 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
@@ -117,12 +114,12 @@ 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:971-976`), `stream error: ...` and
+`src/whatsapp.rs:1031-1041`), `stream error: ...` and
 `could not decrypt a message from ...`; `StreamReplaced` becomes
 `Disconnected("stream replaced by another session")`.
 
 `AppStateSyncFailed` is reported only when at least one collection failed
-**fatally** (`is_fatal_sync_failure`, `src/whatsapp.rs:983-985`), as
+**fatally** (`is_fatal_sync_failure`, `src/whatsapp.rs:1043-1050`), as
 `app state sync incomplete (fatal: [...], retryable: [...])`; retryable or
 skipped collections are retried by the library on a forced reconnect and are
 not shown.
@@ -130,25 +127,27 @@ not shown.
 ## JIDs
 
 - JIDs are stored as `user@server` without device suffix
-  (`jid_to_string` uses `to_non_ad_string`, `src/whatsapp.rs:1000-1002`).
-- `canonical_jid` (`src/whatsapp.rs:541-556`) prefers the phone-number (PN)
+  (`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)
   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:279-295`) returns this account's PN
+- `WaHandle::own_jids` (`src/whatsapp.rs:339-348`) 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:650-696`):
+`handle_live_message` (`src/whatsapp.rs:703-755`):
 
 - 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:992-995`);
-- attaches an `ImageRef` only when the rendered text is an image text;
+  and empty names, `src/whatsapp.rs:1052-1058`);
+- 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;
 - emits `StoredMessage` with the WhatsApp timestamp and `is_ai = false`.
 
 Mentions and quotes are not extracted: the message text is passed on as
@@ -158,64 +157,70 @@ 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:442-450`): push names from messages and `PushNameUpdate`,
-saved names from `ContactUpdate` (full name, else first name,
-`src/whatsapp.rs:701-715`) and from history sync. `note_name`
-(`src/whatsapp.rs:499-522`) ignores empty names, never lets a push name
-override a saved one, skips unchanged names and otherwise emits
+`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
+push name override a saved one, skips unchanged names and otherwise emits
 `WaEvent::Contact`. The [history store](../concepts/history-store.md)
 persists them.
 
 ## History sync
 
 Each `HistorySync` chunk is processed in its own task
-(`handle_history`, `src/whatsapp.rs:730-750`). Decoding (inflate + protobuf)
-runs in `spawn_blocking` via `extract_history` (`src/whatsapp.rs:879-920`).
-`emit_history` (`src/whatsapp.rs:756-822`) then emits push names, inline
-address-book contacts, conversation names and every renderable message.
-History messages never carry an image reference (so their images are never
-described); the sender is the
+(`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
+address-book contacts, conversation names (`emit_history_conversation`,
+`src/whatsapp.rs:843-884`) 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.
 Undecodable conversations are reported as
 `history sync skipped N undecodable conversations`.
 
 ## Message rendering
 
-`describe_message` (`src/whatsapp.rs:1011-1023`) turns a protobuf message into
+`describe_message` (`src/whatsapp.rs:1071-1083`) 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:1031-1043`);
+   `src/whatsapp.rs:1091-1103`);
 3. caption of other media;
-4. `describe_other` (`src/whatsapp.rs:1051-1069`): protocol, reaction,
+4. `describe_other` (`src/whatsapp.rs:1111-1129`): 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:1090-1138`): `is_placeholder` (only `[media]` and
-`[messaggio non supportato]`), `is_voice_note`, `is_image` and
+(`src/whatsapp.rs:1169-1203`): `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:239-257`) rejects
+- `WaHandle::send_text(chat_jid, text)` (`src/whatsapp.rs:277-290`) 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:259-277`) downloads
+- `WaHandle::download_image(&ImageRef)` (`src/whatsapp.rs:297-310`) downloads
   and decrypts the image into memory (`DownloadedImage`), falling back to
-  `image/jpeg` when the MIME type is blank. Images are never written to disk,
-  and `ImageRef`'s `Debug` omits key material (`src/whatsapp.rs:132-151`).
+  `image/jpeg` when the MIME type is blank. `WaHandle::download_audio`
+  (`src/whatsapp.rs:317-330`) 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`).
 
 ## Tests
 
-`src/whatsapp.rs:1140+` covers `is_fatal_sync_failure`, text and extended
-text, captions and placeholders, image and voice rendering, image
+`src/whatsapp.rs:1216` 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:1163-1183`), connects to the WhatsApp servers, waits for a
+(`src/whatsapp.rs:1241-1259`), 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).

+ 58 - 43
openwiki/operations/configuration-and-data.md

@@ -1,8 +1,8 @@
 ---
 type: operations
 title: Configuration and data files
-description: How src/config.rs loads config.toml, the system prompt from SOUL.md and the optional image description request from VISION.md, how src/config_watch.rs reloads them live every 2 seconds, the supported keys and defaults (reply_delay_seconds, catch_up_seconds, ai_retries, web_port), the template files, where the data directory and databases live, the build version and release profile, and operational and security caveats.
-tags: [configuration, toml, data-directory, sqlite, security, soul-md, live-reload]
+description: How src/config.rs loads config.toml, the system prompt from SOUL.md and the optional image description request from VISION.md, how src/config_watch.rs reloads them live every 2 seconds, the supported keys and defaults (reply_delay_seconds, catch_up_seconds, ai_retries, web_port, transcribe_key/url/model/language), the template files, where the data directory and databases live, the build version and release profile, and operational and security caveats.
+tags: [configuration, toml, data-directory, sqlite, security, soul-md, live-reload, transcription]
 sources:
   - id: openwiki-source-ea70eb6c045047448e446296
     resource: repo://.gitignore
@@ -20,38 +20,35 @@ sources:
     resource: repo://src/config.rs
   - id: openwiki-source-b55a21a31ede1b56cd31a6a6
     resource: repo://src/main.rs
-generated: { by: "claude-code", at: "2026-10-23T21:26:24.156Z" }
-verified:
-  - by: openwiki/0.7.2
-    at: 2026-10-23T21:26:24.156Z
+generated: { by: "opencode", at: "2026-10-24T16:39:50.206Z" }
 ---
 
 # Configuration and data files
 
 All runtime settings come from files in the data directory, loaded by
-`Config::load` (`src/config.rs:265-281`): `config.toml` for the settings,
+`Config::load` (`src/config.rs:325-335`): `config.toml` for the settings,
 `SOUL.md` for the system prompt and the optional `VISION.md` for the image
 description request. No environment variable is ever read
-(`src/config.rs:1-25`). The files are loaded at startup and reloaded
+(`src/config.rs:1-29`). The files are loaded at startup and reloaded
 automatically when they change (see [Live reload](#live-reload)); only
 `web_port` needs a restart.
 
 ## Data directory
 
 The data directory is fixed: `<platform config dir>/wh`, resolved with
-`dirs::config_dir()` (`src/config.rs:160-168`). On Windows this is
+`dirs::config_dir()` (`src/config.rs:206-210`). On Windows this is
 `%APPDATA%\wh`, on Linux `~/.config/wh` (file names are case-sensitive
 there). It cannot be changed from the config file. At startup
-`ensure_data_dir` creates it if missing (`src/config.rs:308-315`).
+`ensure_data_dir` creates it if missing (`src/config.rs:372-375`).
 
 | File | Purpose | Defined by |
 |---|---|---|
-| `config.toml` | Optional settings | `CONFIG_FILE`, `src/config.rs:54` |
-| `SOUL.md` | System prompt: the responder's identity and rules; required for the AI | `SOUL_FILE`, `src/config.rs:56` |
-| `VISION.md` | Optional request sent with each image to obtain its description | `VISION_FILE`, `src/config.rs:58` |
-| `session.db` | WhatsApp session store used by `whatsapp-rust` ([WhatsApp client](../integrations/whatsapp-client.md)) | `session_db_path`, `src/config.rs:325-331` |
-| `history.db` | Local messages, chats, contacts, per-chat autoreply flags, the global autoreply setting and image descriptions ([History store](../concepts/history-store.md)) | `history_db_path`, `src/config.rs:333-339` |
-| `ai_requests.log` | Optional AI request log, written only when `ai_log = true` ([AI client](../integrations/ai-client.md)) | `ai_log_path`, `src/config.rs:341-347` |
+| `config.toml` | Optional settings | `CONFIG_FILE`, `src/config.rs:64` |
+| `SOUL.md` | System prompt: the responder's identity and rules; required for the AI | `SOUL_FILE`, `src/config.rs:66` |
+| `VISION.md` | Optional request sent with each image to obtain its description | `VISION_FILE`, `src/config.rs:68` |
+| `session.db` | WhatsApp session store used by `whatsapp-rust` ([WhatsApp client](../integrations/whatsapp-client.md)) | `session_db_path`, `src/config.rs:389-391` |
+| `history.db` | Local messages, chats, contacts, per-chat autoreply flags, the global autoreply setting and image descriptions ([History store](../concepts/history-store.md)) | `history_db_path`, `src/config.rs:397-399` |
+| `ai_requests.log` | Optional AI request log, written only when `ai_log = true` ([AI client](../integrations/ai-client.md)) | `ai_log_path`, `src/config.rs:405-407` |
 
 The two databases are separate by design: `session.db` belongs to the
 WhatsApp library (linked-device session and keys, schema owned by
@@ -70,31 +67,37 @@ WhatsApp library (linked-device session and keys, schema owned by
 | `catch_up_seconds` | After a restart, the last unanswered message of each private chat is answered once if it is at most this old; `0` disables the catch-up | `7200` |
 | `ai_retries` | How many times a failed AI request is retried, waiting 2 s, 4 s, …; HTTP 4xx other than 429 is never retried; `0` disables retrying | `2` |
 | `web_port` | TCP port of the [web control panel](../integrations/web-control-panel.md) on `0.0.0.0`, no authentication; valid range 1–65535 | unset — panel disabled |
+| `transcribe_key` | API key of a cloud, OpenAI-compatible `audio/transcriptions` endpoint (trimmed); audio transcription is enabled only when set | unset — voice notes stay untranscribed |
+| `transcribe_url` | URL of the transcription endpoint (trimmed, trailing `/` removed) | `https://api.groq.com/openai/v1/audio/transcriptions` |
+| `transcribe_model` | Model of the transcription endpoint (trimmed) | `whisper-large-v3-turbo` |
+| `transcribe_language` | Language passed to the transcriber (trimmed); `auto` detects the language | `auto` |
 
-Mapping and defaults: `Config::from_file` (`src/config.rs:284-306`) and the
-constants at `src/config.rs:34-43`. `reply_delay_seconds`,
+Mapping and defaults: `Config::from_file` (`src/config.rs:342-366`) and the
+constants at `src/config.rs:38-53`. `reply_delay_seconds`,
 `catch_up_seconds` and `ai_retries` are unsigned integers in the file
-(`Option<u32>`, `src/config.rs:121-127`), so a negative value is rejected as
-an invalid value type. `web_port` is `Option<u16>` (`src/config.rs:128-129`),
+(`Option<u32>`, `src/config.rs:143-148`), so a negative value is rejected as
+an invalid value type. `web_port` is `Option<u16>` (`src/config.rs:149-151`),
 so negatives and values above 65535 fail TOML parsing, and
-`resolve_web_port` (`src/config.rs:254-263`) additionally rejects `0`. Their
+`resolve_web_port` (`src/config.rs:312-317`) additionally rejects `0`. Their
 use is described in [AI autoreply](../workflows/ai-autoreply.md),
 [AI client](../integrations/ai-client.md) and
 [Web control panel](../integrations/web-control-panel.md).
 
 Unknown keys are ignored. In particular the keys of older versions are no
-longer read: `system_prompt` (move the prompt to `SOUL.md`) and
+longer read: `system_prompt` (move the prompt to `SOUL.md`),
 `cooldown_seconds` / `group_cooldown_seconds` (replaced by
-`reply_delay_seconds`); a leftover value is silently ignored, not an error.
+`reply_delay_seconds`) and `transcribe_command` (removed local transcription
+command, replaced by the cloud endpoint keys); a leftover value is silently
+ignored, not an error.
 
 `config.toml.global.template` in the repository root lists every key with
-its default and a comment (with `web_port` commented out) and points to
-`SOUL.md` for the prompt; copy it to the data directory as `config.toml` and
-fill in `openai_api_key`.
+its default and a comment (with `web_port` and the `transcribe_*` keys
+commented out) and points to `SOUL.md` for the prompt; copy it to the data
+directory as `config.toml` and fill in `openai_api_key`.
 
 ## Prompt files: `SOUL.md` and `VISION.md`
 
-`read_prompt_file` (`src/config.rs:212-223`) reads both files:
+`read_prompt_file` (`src/config.rs:259-265`) reads both files:
 
 - the content is kept **as written** (not trimmed, multiline, Markdown
   allowed); only a leading UTF-8 BOM is removed;
@@ -122,11 +125,11 @@ case-sensitive, an old lowercase file is no longer read and must be renamed.
 
 ## Loading rules for `config.toml`
 
-- Missing file → all defaults (`read_config_file`, `src/config.rs:196-210`).
+- Missing file → all defaults (`read_config_file`, `src/config.rs:242-252`).
 - Unreadable file → `cannot read config file <path>`.
 - Invalid TOML or wrong value type → `invalid config file <path>` with the
   cause `invalid TOML syntax or value type at line L, column C`; the error
-  never echoes file content (`parse_config_file`, `src/config.rs:181-194`).
+  never echoes file content (`parse_config_file`, `src/config.rs:227-236`).
   This also covers negative or non-integer `reply_delay_seconds`,
   `catch_up_seconds` and `ai_retries`, a non-boolean `ai_log` and an
   out-of-range `web_port`.
@@ -134,7 +137,8 @@ case-sensitive, an old lowercase file is no longer read and must be renamed.
   are tolerated.
 - `openai_base_url` consisting only of slashes is rejected with
   `openai_base_url in config.toml must not be empty or only slashes`
-  (`src/config.rs:241-252`).
+  (`src/config.rs:287-294`); a `transcribe_url` that is blank or only
+  slashes instead falls back to the default (`src/config.rs:300-306`).
 - `web_port = 0` is rejected with
   `web_port in config.toml must be between 1 and 65535`.
 
@@ -145,9 +149,9 @@ the same errors only keep the current configuration.
 ## Live reload
 
 `main` starts `config_watch::spawn(config.source_files(), app_tx)`.
-`Config::source_files` (`src/config.rs:317-323`) lists `config.toml`,
+`Config::source_files` (`src/config.rs:381-383`) lists `config.toml`,
 `SOUL.md` and `VISION.md` inside the data directory. The watcher
-(`src/config_watch.rs:15-52`) polls their modification times every 2 seconds
+(`src/config_watch.rs:24-40`) polls their modification times every 2 seconds
 (`POLL_INTERVAL_SECONDS`) instead of using file notifications, so it needs
 no extra dependency and behaves the same on Windows and Linux. A file whose
 metadata cannot be read counts as `None`, so creating or deleting a file is
@@ -165,10 +169,12 @@ ends when the application receiver is dropped.
   `Configurazione non ricaricata: <error>`.
 
 New values of the AI keys, `SOUL.md`, `VISION.md`, `ai_log`,
-`reply_delay_seconds`, `catch_up_seconds` and `ai_retries` therefore apply
-to the next replies without restarting. `web_port` does not: the web panel is
-bound once at startup. Saving a file in several steps can trigger more than
-one reload; each one reloads the full set of files.
+`reply_delay_seconds`, `catch_up_seconds`, `ai_retries` and the
+`transcribe_*` keys therefore apply to the next replies without restarting.
+Note that audios arriving while transcription was disabled are not
+transcribed later; only new audios are. `web_port` does not apply: the web
+panel is bound once at startup. Saving a file in several steps can trigger
+more than one reload; each one reloads the full set of files.
 
 ## Build version and profile
 
@@ -188,14 +194,21 @@ C build is not needed, which matters for the Linux musl cross-compile with
 
 ## Security and operational notes
 
-- The API key is never printed: `Config`'s `Debug` shows `***` or `<unset>`
-  (`src/config.rs:132-158`), and the raw `FileConfig` has no `Debug` impl at
-  all (`src/config.rs:107-130`). The AI client redacts it too.
+- API keys are never printed: `Config`'s `Debug` shows `***` or `<unset>`
+  for both `openai_api_key` and `transcribe_key`
+  (`src/config.rs:174-200`), and the raw `FileConfig` has no `Debug` impl at
+  all (`src/config.rs:129-160`). The AI client redacts its key too, and the
+  transcription client never logs or echoes its key.
 - `ai_requests.log` never contains the API key and replaces image data
   with a size placeholder, but it does store the full request (system
   prompt and chat context) and the reply in clear text, one record per
   attempt including retries, and it is only appended to, never rotated.
   Enable `ai_log` only for diagnosis and delete the file afterwards.
+- Voice-note transcription sends the audio bytes to the configured
+  third-party endpoint (Groq by default): keep `transcribe_key` unset if
+  that is not acceptable. Audio bytes are kept in memory only and never
+  written to the data directory; only the transcribed text ends up in
+  `history.db` inside the voice-note message.
 - `web_port` exposes an unauthenticated HTTP panel on every network
   interface: anyone who can reach the port can see chat names, toggle
   autoreplies, quit the bot, unlink the WhatsApp device and, while pairing,
@@ -222,12 +235,14 @@ C build is not needed, which matters for the Linux musl cross-compile with
 
 ## Tests
 
-`src/config.rs:350+` covers defaults, full files, the ignored legacy
-`system_prompt` key, blank values, trimming, base URL normalisation and
+`src/config.rs:413` onward covers defaults, full files, the ignored legacy
+keys (`system_prompt`, `cooldown_seconds` / `group_cooldown_seconds` and
+`transcribe_command`), blank values, trimming, base URL normalisation and
 errors, `Debug` masking, vision model, DB and AI log paths, `ai_log`,
 `reply_delay_seconds` (default, custom values including `0`, negatives
-rejected, legacy cooldown keys ignored), catch-up and `ai_retries` defaults
-and negatives, `web_port` (unset, valid, `0` and out-of-range rejected),
+rejected), catch-up and `ai_retries` defaults and negatives, `web_port`
+(unset, valid, `0` and out-of-range rejected), the transcription keys
+(unset disables with defaults, parsed and trimmed, blank falls back),
 unknown keys, BOM/CRLF, non-leaking parse errors, missing, existing, invalid
 and unreadable files, and `SOUL.md` reading (missing, blank, kept as written
 without BOM, non-UTF-8 error naming the path). `src/config_watch.rs` checks

+ 1 - 1
openwiki/operations/index.md

@@ -1,3 +1,3 @@
 # Files
 
-- [Configuration and data files](configuration-and-data.md) - How src/config.rs loads config.toml, the system prompt from SOUL.md and the optional image description request from VISION.md, how src/config_watch.rs reloads them live every 2 seconds, the supported keys and defaults (reply_delay_seconds, catch_up_seconds, ai_retries, web_port), the template files, where the data directory and databases live, the build version and release profile, and operational and security caveats.
+- [Configuration and data files](configuration-and-data.md) - How src/config.rs loads config.toml, the system prompt from SOUL.md and the optional image description request from VISION.md, how src/config_watch.rs reloads them live every 2 seconds, the supported keys and defaults (reply_delay_seconds, catch_up_seconds, ai_retries, web_port, transcribe_key/url/model/language), the template files, where the data directory and databases live, the build version and release profile, and operational and security caveats.

+ 15 - 13
openwiki/quickstart.md

@@ -1,8 +1,8 @@
 ---
 type: overview
 title: Quickstart
-description: What the wh WhatsApp terminal client with per-chat and global AI autoreply (system prompt in SOUL.md, reply delay, image descriptions), live configuration reload, re-pairing with F8 and optional web control panel does, how to build and run it, and where to find each topic in this wiki.
-tags: [overview, quickstart, rust, whatsapp, tui, ai, web-panel, pairing]
+description: What the wh WhatsApp terminal client with per-chat and global AI autoreply (system prompt in SOUL.md, reply delay, image descriptions, voice-note transcriptions) does, how to build and run it, and where to find each topic in this wiki.
+tags: [overview, quickstart, rust, whatsapp, tui, ai, web-panel, pairing, transcription]
 sources:
   - id: openwiki-source-651d1fb6c9e49916a916ab51
     resource: repo://Cargo.toml
@@ -14,10 +14,7 @@ sources:
     resource: repo://src/config.rs
   - id: openwiki-source-b55a21a31ede1b56cd31a6a6
     resource: repo://src/main.rs
-generated: { by: "claude-code", at: "2026-10-23T21:26:24.156Z" }
-verified:
-  - by: openwiki/0.7.2
-    at: 2026-10-23T21:26:24.156Z
+generated: { by: "opencode", at: "2026-10-24T16:39:50.206Z" }
 ---
 
 # Quickstart
@@ -53,7 +50,7 @@ because the binary exports symbols.
 
 `wh --version` (or `-V`) prints `wh <version> (<git commit>)` and exits; the
 same version is shown at the bottom right of the main screen
-(`src/main.rs:30-48`, `build.rs`).
+(`src/main.rs:32-48`, `build.rs`).
 
 On first start the app shows a QR code: scan it from WhatsApp → *Linked
 devices*. The session is kept in `session.db`, so later starts connect
@@ -72,7 +69,7 @@ screen ([key bindings](architecture/app-state.md#key-bindings)).
 Settings live in `config.toml` inside the data directory
 `<platform config dir>/wh` (on Windows `%APPDATA%\wh`, on Linux
 `~/.config/wh`), next to `session.db` and `history.db`
-(`src/config.rs:46-58`, `src/config.rs:160-168`); the repository's
+(`src/config.rs:55-68`, `src/config.rs:206-210`); the repository's
 `config.toml.global.template` lists every key. The system prompt is not a
 key: it is the file `SOUL.md` in the same folder (start from
 `soul.md.template`), and the optional `VISION.md` replaces the built-in
@@ -87,7 +84,10 @@ plain WhatsApp client. Optional keys tune the reply delay
 (`reply_delay_seconds`), the startup catch-up (`catch_up_seconds`), AI
 retries (`ai_retries`), the vision model (`openai_vision_model`), an AI
 request log (`ai_log`, written to `ai_requests.log` in the same folder) and
-the web panel port (`web_port`, unauthenticated, all interfaces). The old
+the web panel port (`web_port`, unauthenticated, all interfaces). Four more
+`transcribe_*` keys enable voice-note transcription against any
+OpenAI-compatible `audio/transcriptions` endpoint (off by default; Groq with
+`whisper-large-v3-turbo` as the default endpoint and model). The old
 keys `system_prompt`, `cooldown_seconds` and `group_cooldown_seconds` are
 ignored. Keys and defaults:
 [Configuration and data](operations/configuration-and-data.md).
@@ -97,16 +97,18 @@ ignored. Keys and defaults:
 The first unanswered message of an enabled chat starts a
 `reply_delay_seconds` wait; messages arriving meanwhile join the same reply.
 Images are described once by the vision model and only their descriptions
-reach the AI. The reply is dropped if the owner answered by hand, and the
-model can stay silent by calling the `no_reply` tool, whose use is decided
-by `SOUL.md`. Details: [AI auto-reply workflow](workflows/ai-autoreply.md).
+reach the AI; voice notes are transcribed once (when `transcribe_key` is
+set, Groq by default) and their text enters the reply context. The reply is
+dropped if the owner answered by hand, and the model can stay silent by
+calling the `no_reply` tool, whose use is decided by `SOUL.md`. Details:
+[AI auto-reply workflow](workflows/ai-autoreply.md).
 
 ## Startup in one line
 
 `main` prints the version and exits for `--version`; otherwise it loads the
 config, opens the history, starts the WhatsApp client, the optional web
 panel, the configuration watcher and an input thread, then runs the
-draw/receive loop until quit (`src/main.rs:35-71`, see
+draw/receive loop until quit (`src/main.rs:44-72`, see
 [Runtime and event loop](architecture/runtime-and-event-loop.md)).
 
 ## Where to look

+ 1 - 1
openwiki/testing/index.md

@@ -1,3 +1,3 @@
 # Files
 
-- [Unit tests](unit-tests.md) - Overview of the in-module unit test suites in src/*.rs (including the web panel, the version flag and the configuration watcher), the ignored live relink test, what each one covers, how they isolate files, and how to run them.
+- [Unit tests](unit-tests.md) - Overview of the in-module unit test suites in src/*.rs (including the web panel, the version flag, the configuration watcher and the audio transcription client), the ignored live relink test, what each one covers, how they isolate files, and how to run them.

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

@@ -1,7 +1,7 @@
 ---
 type: testing
 title: Unit tests
-description: Overview of the in-module unit test suites in src/*.rs (including the web panel, the version flag and the configuration watcher), the ignored live relink test, what each one covers, how they isolate files, and how to run them.
+description: Overview of the in-module unit test suites in src/*.rs (including the web panel, the version flag, the configuration watcher and the audio transcription client), the ignored live relink test, what each one covers, how they isolate files, and how to run them.
 tags: [testing, cargo, unit-tests]
 sources:
   - id: openwiki-source-651d1fb6c9e49916a916ab51
@@ -20,16 +20,15 @@ sources:
     resource: repo://src/history.rs
   - id: openwiki-source-b55a21a31ede1b56cd31a6a6
     resource: repo://src/main.rs
+  - id: openwiki-source-feb00b7ad73520627c930a9f
+    resource: repo://src/transcribe.rs
   - id: openwiki-source-6bddea306b9b87f180f1fc02
     resource: repo://src/ui.rs
   - id: openwiki-source-7b75006986d8b0337adc6ca0
     resource: repo://src/web.rs
   - id: openwiki-source-7ea5eac45e5030178a6ea05b
     resource: repo://src/whatsapp.rs
-generated: { by: "claude-code", at: "2026-10-23T21:26:24.156Z" }
-verified:
-  - by: openwiki/0.7.2
-    at: 2026-10-23T21:26:24.156Z
+generated: { by: "opencode", at: "2026-10-24T16:39:50.206Z" }
 ---
 
 # Unit tests
@@ -50,7 +49,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:1163-1183`) is skipped by a plain `cargo test`. It starts a
+(`src/whatsapp.rs:1238-1258`) 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
@@ -62,33 +61,35 @@ exits. See [WhatsApp relink and pairing](../workflows/relink-and-pairing.md).
 
 | Module | Tests | Location | Covers |
 |---|---|---|---|
-| `autoreply` | 55 | `src/autoreply.rs:505+` | private/group/self-chat detection, `supports_autoreply` (groups yes, broadcasts and newsletters no), `ReplyGate::should_schedule` rules (disabled, own messages, self-chat, backlog, blank/placeholder text, voice notes and images trigger, pending chat), pending tracking and `take_pending`, `answered_by_owner`, `catch_up_due` rules, `build_turns` (system turn naming private chat, group or self-chat, roles, `is_ai` in the self-chat, context cap, blank texts skipped, voice notes, stored image descriptions vs. unseen images, assistant turns untouched, group sender prefixes), `context_start`, `mark_own_mentions` |
-| `config` | 37 | `src/config.rs:350+` | defaults, full files, ignored legacy `system_prompt` key, blank values, trimming, base URL normalisation, `Debug` masking, DB and AI log paths, `ai_log`, `reply_delay_seconds` (default, custom and `0`, negatives rejected, legacy cooldown keys ignored), catch-up and `ai_retries` defaults/custom values/negative rejection, `web_port` (unset, valid, `0` and out of range rejected), BOM/CRLF, non-leaking parse errors, missing/invalid/unreadable files, `SOUL.md` reading (missing, blank, as written without BOM, non-UTF-8 error naming the path) |
-| `ai` | 27 | `src/ai.rs:581+` | text request body with the `no_reply` tool, image description body (no tools) and its `VISION.md`/default request, image validation limits, `describe_image` errors, `Debug` redaction, reply parsing (`no_reply` with or without text, other tool calls ignored, missing choices/content), endpoint construction, AI log records (no key, images redacted, `no_reply` record), logging only when enabled, append-only log writes, retry policy (`is_retryable`, `StatusError` message, linear `retry_delay`), truncation |
-| `whatsapp` | 22 + 1 ignored | `src/whatsapp.rs:1140+` | ignored live relink test (new QR with new device keys), fatal-only app state sync reporting, message rendering and placeholders, captions, image references, voice/image detection, silent messages, JID device suffix, push-name filtering |
-| `app` | 20 | `src/app.rs:1596+` | autoreply enable rules (private, self-chat LID, groups; broadcasts, newsletters and missing AI refused), `web_chats` filtering and order, vision applicability, `select_images_to_describe` (last five in arrival order, known and out-of-context images skipped, nothing without vision), `arrived_after`, `manual_reply_after`, `description_failure_status`, list filter matching, selection, page steps and toggle helpers, AltGr handling, fixed function-key codes |
-| `history` | 18 | `src/history.rs:543+` | dedupe by message id, ordering and limits, `messages_since` (boundary, limit keeps most recent, ascending order), chat name fallbacks, per-chat and global autoreply flags, `message_exists`, image descriptions (per chat, replaced, deleted with the chat), `delete_chat`, contacts, on-disk open |
+| `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` |
+| `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 |
+| `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:207+` | only `--version` / `-V` request the version; `BUILD_VERSION` starts with the package version and names a commit |
+| `main` | 2 | `src/main.rs:208+` | only `--version` / `-V` request the version; `BUILD_VERSION` starts with the package version and names a commit |
 | `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: 209 test functions — 208 `#[test]` run by `cargo test` plus the
+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.
 
 ## Test isolation
 
 - History tests use `History::open_in_memory()` (only compiled for tests,
-  `src/history.rs:78-89`); the on-disk test writes under
-  `std::env::temp_dir()` in a per-process directory (`src/history.rs:837-852`).
-- AI tests check request bodies, parsing, log records and the retry policy
-  without sending requests; the append test writes a per-process file under
-  `std::env::temp_dir()` and removes it (`src/ai.rs:861-873`).
+  `src/history.rs:83-90`); the on-disk test writes under
+  `std::env::temp_dir()` in a per-process directory (`src/history.rs:864-872`).
+- AI and transcription tests check request bodies, parsing, log records and,
+  for the AI client, the retry policy, without sending requests; the append
+  test writes a per-process file under `std::env::temp_dir()` and removes it
+  (`src/ai.rs:865-877`).
 - Config tests that read `SOUL.md`-style prompt files write per-process
   scratch files under the `target` directory and remove them
-  (`scratch_path`, `src/config.rs:357-383`). The `config_watch` test does the
-  same with a per-process file under `target` (`src/config_watch.rs:54-73`).
+  (`scratch_path`, `src/config.rs:439-444`). The `config_watch` test does the
+  same with a per-process file under `target` (`src/config_watch.rs:61-74`).
 - Web tests exercise the pure routing, parsing and QR rendering helpers; the
   HTTP server itself is never started.
 - The live relink test writes its session database under `target` and is

+ 149 - 122
openwiki/workflows/ai-autoreply.md

@@ -1,8 +1,8 @@
 ---
 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, image descriptions stored once and reused, the startup catch-up, building the AI context from SOUL.md, the no_reply tool, retries and sending the reply.
-tags: [ai, autoreply, workflow, vision, reply-gate, reply-delay, catch-up, image-description]
+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.
+tags: [ai, autoreply, workflow, vision, transcription, reply-gate, reply-delay, catch-up, image-description]
 sources:
   - id: openwiki-source-9fec9d1af408501c9c455161
     resource: repo://src/app.rs
@@ -14,10 +14,7 @@ sources:
     resource: repo://src/config.rs
   - id: openwiki-source-ce0c4ea8cd3b15431fa691ce
     resource: repo://src/history.rs
-generated: { by: "claude-code", at: "2026-10-23T21:26:24.156Z" }
-verified:
-  - by: openwiki/0.7.2
-    at: 2026-10-23T21:26:24.156Z
+generated: { by: "opencode", at: "2026-10-24T16:39:50.206Z" }
 ---
 
 # AI auto-reply workflow
@@ -25,21 +22,24 @@ verified:
 The autoreply answers incoming WhatsApp messages with an AI-generated text,
 only for chats where the user turned it on — one by one, or all at once with
 the global switch. Decision rules and context building are pure functions in
-`src/autoreply.rs` (no I/O, `src/autoreply.rs:1-4`); orchestration lives in
+`src/autoreply.rs` (no I/O); orchestration lives in
 [`App`](../architecture/app-state.md); the HTTP calls are done by the
-[AI client](../integrations/ai-client.md).
+[AI client](../integrations/ai-client.md) (text and vision) and by the
+transcription endpoint (`src/transcribe.rs`).
 
 ```
-WaEvent::Message { message, image }
+WaEvent::Message { message, image, audio }
   -> History::save_message (new only)
   -> App::maybe_autoreply   (enabled = global OR per-chat flag)
        ReplyGate::should_schedule? --yes--> mark_pending + timer(reply_delay_seconds)
-       image from an allowed sender?  ----> keep_image (pending_images / backlog_images)
+       media from an allowed sender?  ----> keep_image / keep_audio
   ... AppEvent::ReplyDue
   -> prepare_reply: reply_context (still enabled? AI? owner not answered last?)
        --no--> finish_reply (drop)
-       images without description? --yes--> download + describe_image (parallel, max 5)
-                                             -> AppEvent::ImagesDescribed -> store descriptions
+       audios without transcription? --yes--> download + transcribe (parallel, max 3)
+                                               -> 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
   -> AiClient::complete (tools: [no_reply], up to ai_retries retries)
   -> AppEvent::AiReply
@@ -53,23 +53,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:638-644`):
+on (`autoreply_enabled`, `src/app.rs:858-860`):
 
 - **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:1101-1137`), so a chat can be enabled without opening it; the
+  `src/app.rs:1348-1378`), 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:1544-1557`), otherwise the status shows
+  `src/app.rs:1894-1902`), 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:1186-1203`);
+  `global_autoreply` (`toggle_global_autoreply`, `src/app.rs:1433-1444`);
   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,88 +82,90 @@ 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:383-400`, see
+(`apply_reloaded_config`, `src/app.rs:422-434`, 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`,
-`catch_up_seconds` and `ai_retries`. If the new files leave the AI
-unconfigured, every autoreply path stops as described below, but the
-persisted flags are kept.
+`catch_up_seconds`, `ai_retries` and `transcribe_*` keys. If the new files
+leave the AI unconfigured, every autoreply path stops as described below,
+but the persisted flags are kept.
 
-`supports_autoreply` (`src/autoreply.rs:99-107`) accepts private chats and
+`supports_autoreply` (`src/autoreply.rs:105-108`) accepts private chats and
 groups:
 
 - private: JID server `s.whatsapp.net` or `lid` (`is_private_chat`,
-  `src/autoreply.rs:76-86`);
-- group: JID server `g.us` (`is_group_chat`, `src/autoreply.rs:88-97`);
+  `src/autoreply.rs:83-92`);
+- group: JID server `g.us` (`is_group_chat`, `src/autoreply.rs:94-103`);
 - 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:450-459`; `maybe_autoreply`,
-`src/app.rs:461-483`), so duplicates and history-sync re-deliveries of
+`maybe_autoreply` (`on_message`, `src/app.rs:485-499`; `maybe_autoreply`,
+`src/app.rs:509-532`), 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:745-751`), refreshes the own JIDs if
+system prompt (`ai_setup`, `src/app.rs:987-989`), refreshes the own JIDs if
 unknown and computes `enabled` (global or per chat).
 
-`ReplyGate::should_schedule(message, enabled)` (`src/autoreply.rs:182-191`)
-returns `true` only when **all** hold (`is_reply_candidate` and
-`meets_content_rules`, `src/autoreply.rs:241-265`):
+`ReplyGate::should_schedule(message, enabled)`
+(`src/autoreply.rs:189-191`) returns `true` only when **all** hold
+(`is_reply_candidate` and `meets_content_rules`,
+`src/autoreply.rs:247-265`):
 
 | Rule | Detail |
 |---|---|
 | Not pending | the chat has no reply already scheduled or in progress |
 | Enabled | global autoreply on, or the per-chat flag on |
-| Sender allowed | not sent by the user — except human-written messages in the self-chat ("Messaggi a te stesso"); AI messages never trigger (`sender_allowed`, `src/autoreply.rs:172-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:178-180`) |
 | Supported chat | private chat or group (`supports_autoreply`) |
-| No backlog | `timestamp >= started_at`, the process start time |
+| No backlog | `timestamp >= started_at`, the process start time (`is_backlog`, `src/autoreply.rs:198-200`) |
 | 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:148-157`; `strip_device_suffix`,
-`src/autoreply.rs:293-309`). Groups follow exactly the same rules as private
+`src/autoreply.rs:154-157`; `strip_device_suffix`,
+`src/autoreply.rs:299-309`). Groups follow exactly the same rules as private
 chats; mentions and quotes no longer change anything.
 
 ## 3. Reply delay and batching
 
-When `should_schedule` passes, `schedule_reply` (`src/app.rs:485-493`) marks
-the chat pending in the gate (`mark_pending`, `src/autoreply.rs:202-210`)
-and `spawn_reply_timer` (`src/app.rs:1340-1350`) emits
+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
 `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:534-545`) takes the images kept for the chat
-and, if the chat is still pending (a local deletion removes the pending
-mark), calls `prepare_reply` (`src/app.rs:547-568`). Its `reply_context`
-(`src/app.rs:646-664`) drops the reply — and `finish_reply` ends the pending
-state — when:
+`handle_reply_due` (`src/app.rs:634-641`) 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`
+ends the pending state — when:
 
 - the autoreply was turned off (both switches) during the wait;
 - the AI is not configured;
 - the chat has no message in the context window;
 - the latest message was written by hand by the owner outside the self-chat
-  (`answered_by_owner`, `src/autoreply.rs:231-239`): the owner already
+  (`answered_by_owner`, `src/autoreply.rs:237-239`): the owner already
   answered, so the AI stays silent.
 
-Otherwise it describes the new images first (see §5) or calls
-`start_reply` at once.
+Otherwise it transcribes the new audios and describes the new images first
+(see §5) or calls `start_reply` at once.
 
 ### After the reply
 
-`AiReply` is always followed by `finish_reply` (`src/app.rs:616-636`), which
+`AiReply` is always followed by `finish_reply` (`src/app.rs:837-852`), 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:1448-1455`) that itself passes
+(`arrived_after`, `src/app.rs:1696-1698`) that itself passes
 `should_schedule`, a **new** reply is scheduled (with a fresh delay, keeping
-the images received meanwhile); otherwise the kept images are dropped. So
-messages that arrive while a reply is being generated are answered once,
-afterwards, rather than being lost or triggering parallel replies.
+the media received meanwhile); otherwise the kept images and audios are
+dropped. So messages that arrive while a reply is being generated are
+answered once, afterwards, rather than being lost or triggering parallel
+replies.
 
 ## 4. Startup catch-up
 
@@ -171,13 +173,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:684-694`) runs `catch_up` once per process (`catch_up_done`;
+(`src/app.rs:924-930`) runs `catch_up` once per process (`catch_up_done`;
 later reconnections skip it).
 
-`catch_up` / `catch_up_chats` (`src/app.rs:696-743`) look at every enabled
+`catch_up` / `catch_up_chats` (`src/app.rs:940-981`) 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:267-290`) accepts it only when:
+`ReplyGate::catch_up_due` (`src/autoreply.rs:278-290`) accepts it only when:
 
 - `catch_up_seconds` (default 7200) is positive — `0` disables the catch-up;
 - the chat is not a group;
@@ -187,78 +189,102 @@ per-chat flag — and take its last stored message.
   (already answered) never qualifies.
 
 Each due chat is marked pending and goes through `prepare_reply` **without**
-the reply delay, together with the images of its offline messages
-(`backlog_images`); offline images of other chats are dropped. When at least
-one reply went ahead the status shows `Risposte di recupero: N`. Offline
-images that arrive after the catch-up already ran are described on their own,
-without a reply (`describe_late_image`, `src/app.rs:517-532`).
-
-## 5. Image descriptions
-
-Images are never sent to the text model. Each image sent by an allowed
-sender in an enabled chat is kept by `keep_image` (`src/app.rs:495-515`) and,
-just before the reply that covers it, described **once** by the vision model
-(`AiClient::describe_image`, request text from `VISION.md` or the built-in
-one, see [AI client](../integrations/ai-client.md)). The vision model sees
-only that request and the image, never the conversation. The description is
-stored in the `image_descriptions` table and reused in every later context.
-
-- `select_images_to_describe` (`src/app.rs:1406-1433`) keeps only the images
-  whose message is in the reply context and that have neither a stored
-  description nor one in progress (`describing`), and at most the **5 most
-  recent** (`MAX_IMAGES_PER_REPLY`); older images of the same batch are
-  dropped for good, to cap the cost of albums. Without `openai_vision_model`
-  nothing is described (`vision_applies`).
-- `spawn_image_descriptions` (`src/app.rs:1372-1404`) describes all selected
-  images in parallel. Each download (`download_image_with_retry`,
-  `src/app.rs:1477-1503`) makes up to 3 attempts, each abandoned after 60 s,
-  waiting 2 s × attempt between them; the bytes stay in memory.
-- `handle_images_described` (`src/app.rs:582-614`) stores each description
-  whose message still exists (`History::message_exists`), so a chat deleted
-  meanwhile gets no orphan rows. Failed images stay without description, the
-  others are unaffected, and the status shows
-  `Descrizione immagine fallita (N): <first error>`. If the chat is still
-  pending, the reply then starts (re-checking `reply_context`).
+the reply delay, together with the images and audios of its offline messages
+(`backlog_images`, `backlog_audios`); offline media of other chats are
+dropped. When at least one reply went ahead the status shows
+`Risposte di recupero: N`. Offline images that arrive after the catch-up
+already ran are described on their own, without a reply
+(`describe_late_image`, `src/app.rs:571-581`), and offline audios
+transcribed the same way (`transcribe_late_audio`, `src/app.rs:616-626`).
+
+## 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
+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
+  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
+  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
+  starts;
+- images are then described by the vision model (`AiClient::describe_image`,
+  request text from `VISION.md` or the built-in one, see
+  [AI client](../integrations/ai-client.md)). The vision model sees only
+  that request and the image, never the conversation. The description is
+  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
+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
+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`).
+
+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,
+waiting 2 s × attempt between them, and the bytes stay in memory.
+`handle_images_described` (`src/app.rs:747-770`) stores each description
+whose message still exists (`History::message_exists`), so a chat deleted
+meanwhile gets no orphan rows; failed images stay without description, the
+others are unaffected, and the status shows
+`Descrizione immagine fallita (N): <first error>`. If the chat is still
+pending, the reply then starts (re-checking `reply_context`).
 
 ## 6. Building the context
 
-`start_reply` (`src/app.rs:762-790`) loads the context with
+`start_reply` (`src/app.rs:1009-1028`) loads the context with
 `History::messages_since(chat_jid, context_start(now), CONTEXT_MESSAGES)`:
 
-- `context_start` (`src/autoreply.rs:459-468`) is local midnight of
+- `context_start` (`src/autoreply.rs:466-468`) is local midnight of
   **yesterday** (falls back to `now` minus two days if local time cannot be
-  resolved);
-- `CONTEXT_MESSAGES` = 200 (`src/autoreply.rs:13-15`) caps the window,
+  resolved, `src/autoreply.rs:475-480`);
+- `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` /
-`replace_mention`, `src/autoreply.rs:159-170`, `src/autoreply.rs:438-457`;
-a longer number is never matched) and calls
-`build_turns(system_prompt, messages, TurnContext)`
-(`src/autoreply.rs:482-503`) with the chat name and the stored image
+`@<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:420-436`): the `SOUL.md` prompt as written, then — in
-  normal chats and groups — a line `Chat privata con «name».` or
-  `Chat di gruppo «name».` and `ROLE_NOTE_CHAT` (`assistant` messages were
-  written by the account owner it answers for, `user` messages by the
-  contact or, in groups, by the participants named in front); in the
-  self-chat only `ROLE_NOTE_SELF_CHAT` (`user` is the owner, `assistant` its
-  own previous replies). Parts are separated by a blank line;
+- 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
+  `ROLE_NOTE_CHAT` (`assistant` messages were written by the account owner
+  it answers for, `user` messages by the contact or, in groups, by the
+  participants named in front); in the self-chat only `ROLE_NOTE_SELF_CHAT`
+  (`user` is the owner, `assistant` its own previous replies). Parts are
+  separated by a blank line;
 - then the last `CONTEXT_MESSAGES` messages, skipping blank texts;
-- roles (`turn_role`, `src/autoreply.rs:311-321`): in normal chats and
+- roles (`turn_role`, `src/autoreply.rs:318-321`): in normal chats and
   groups own messages are `Assistant`; in the self-chat, where every message
   is `from_me`, only `is_ai` messages are `Assistant`;
 - in groups every `User` turn is prefixed with `"<sender>: "`, where the
   sender is the trimmed push name or else the user part of the sender JID
-  (`sender_label`, `message_to_turn`, `src/autoreply.rs:381-418`);
-- only `User` turns are rewritten (`user_content`,
-  `src/autoreply.rs:364-379`), with plain facts — how to react is left to
-  `SOUL.md`:
-  - voice note → `[Il contatto ha inviato un messaggio vocale che non puoi ascoltare]`;
+  (`sender_label`, `message_to_turn`, `src/autoreply.rs:387-418`);
+- only `User` turns are rewritten (`user_content`, `src/autoreply.rs:370-379`),
+  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]`;
+    a transcribed one instead passes through as its stored text
+    (`[messaggio vocale] <transcription>`);
   - image with a stored description →
     `[Il contatto ha inviato un'immagine. Descrizione: <text>]`, without one →
     `[Il contatto ha inviato un'immagine che non puoi vedere]`; a caption is
@@ -267,7 +293,7 @@ descriptions:
 
 ## 7. Request, `no_reply` and retries
 
-`spawn_ai_request` (`src/app.rs:1352-1370`) calls `AiClient::complete`, which
+`spawn_ai_request` (`src/app.rs:1600-1611`) 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 —
@@ -277,27 +303,27 @@ Every AI call (text completion and image description) is retried by the
 [AI client](../integrations/ai-client.md) up to `ai_retries` times (default
 2, waiting 2 s, 4 s, …) on transient errors — HTTP 429, 5xx, timeouts,
 transport errors, malformed or empty replies — while other 4xx errors fail
-at once. A reply can therefore arrive noticeably later than the trigger;
-the checks in §8 happen after all retries. The request task is awaited by a
-second task, so `AiReply` is always delivered and the chat never stays
-pending. When `ai_log = true`, every attempt is also appended to
-`ai_requests.log`.
+at once; transcription calls perform a single request instead. A reply can
+therefore arrive noticeably later than the trigger; the checks in §8 happen
+after all retries. The request task is awaited by a second task, so
+`AiReply` is always delivered and the chat never stays pending. When
+`ai_log = true`, every attempt is also appended to `ai_requests.log`.
 
 ## 8. Sending
 
 A text reply is handled by `send_ai_reply_if_enabled`
-(`src/app.rs:820-839`), which re-checks two things:
+(`src/app.rs:1064-1077`), 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:841-852`): if a hand-written owner
-  message follows the last message of the reply context
+- `owner_answered_meanwhile` (`src/app.rs:1084-1090`): 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:854-882`) stores it as an own message with
+`handle_sent` (`src/app.rs:1096-1120`) 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: ...`).
@@ -305,8 +331,9 @@ counting as a manual answer. Errors appear in the status line
 ## Tests
 
 The gate rules, pending tracking, catch-up, mention marking and turn building
-are covered by `src/autoreply.rs:505+`; enable rules, `web_chats`, image
-selection, `arrived_after`, `manual_reply_after` and the description failure
-status by `src/app.rs:1596+`; image descriptions and the global flag storage
-by `src/history.rs`; the `no_reply` tool, descriptions and the retry policy by
-`src/ai.rs`. See [Unit tests](../testing/unit-tests.md).
+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
+[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, image descriptions stored once and reused, the startup catch-up, building the AI context from SOUL.md, the no_reply tool, retries and sending the reply.
+- [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.
 - [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.

+ 22 - 22
openwiki/workflows/relink-and-pairing.md

@@ -3,9 +3,6 @@ type: workflow
 title: WhatsApp relink and pairing
 description: 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.
 tags: [whatsapp, pairing, relink, qr-code, session, web-panel, tui]
-verified:
-  - by: openwiki/0.7.2
-    at: 2026-10-23T21:26:24.156Z
 sources:
   - id: openwiki-source-9fec9d1af408501c9c455161
     resource: repo://src/app.rs
@@ -15,7 +12,10 @@ sources:
     resource: repo://src/web.rs
   - id: openwiki-source-7ea5eac45e5030178a6ea05b
     resource: repo://src/whatsapp.rs
-generated: { by: "claude-code", at: "2026-10-23T21:26:24.156Z" }
+generated: { by: "openwiki/0.7.2", at: "2026-10-24T16:42:08.550Z" }
+verified:
+  - by: openwiki/0.7.2
+    at: 2026-10-23T21:26:24.156Z
 ---
 
 # WhatsApp relink and pairing
@@ -64,26 +64,26 @@ 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:884-903`).
-`request_relink` (`src/app.rs:356-369`) cancels a pending chat deletion; the
+`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
 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:371-381`) with the status
+flag through `cancel_pending_relink` (`src/app.rs:409-415`) 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:703-718`) asks
+**Web panel.** "Nuovo QR" (`relinkBot`, `src/web_page.html:707-718`) 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:166-174`, `src/web.rs:238-254`).
+`X-Wh-Request: 1` header (`src/web.rs:171-174`, `src/web.rs:238-259`).
 `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:320-341`) clears `pending_relink` and returns at
+`start_relink` (`src/app.rs:360-375`) 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`,
 drops the stored QR, shows `Cancellazione della sessione in corso...` and
@@ -99,29 +99,29 @@ every 2 s.
 
 ## 3. Replacing the session
 
-`WaHandle::relink` (`src/whatsapp.rs:221-237`, see
+`WaHandle::relink` (`src/whatsapp.rs:261-270`, 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: ...`, image downloads fail, `own_jids` is empty.
-2. **Stop** it (`Session::stop`, `src/whatsapp.rs:182-198`): `logout`
+   `Invio fallito: ...`, media downloads fail, `own_jids` is empty.
+2. **Stop** it (`Session::stop`, `src/whatsapp.rs:223-231`): `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:314-329`): delete `session.db` and its `-wal` / `-shm`
+   `src/whatsapp.rs:374-386`): 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:349-367`) opens it with `rusqlite` (5 s busy timeout)
+   (`src/whatsapp.rs:407-428`) 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:369-416`) and put it in the slot. It sends its events
+   (`src/whatsapp.rs:430-471`) 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.
 
@@ -131,24 +131,24 @@ previously linked account stay visible until deleted locally.
 
 ## 4. Outcome and new QR
 
-`finish_relink` (`src/app.rs:343-354`) clears `relinking` and shows
+`finish_relink` (`src/app.rs:382-388`) 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:406-411`). It is shown
+`Screen::Pairing` and stores the payload (`src/app.rs:440-445`). 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:364-379`) and returns it as `qr_svg` with
+  (`src/web.rs:369-384`) 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:596-628`).
+  (`src/web_page.html:604-629`).
 
 When the library runs out of QR codes the status shows
 `pairing QR codes exhausted; request a new QR to pair again` — another
@@ -174,10 +174,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:422-442`).
+  produces an SVG document (`src/web.rs:423-444`).
 - `whatsapp.rs`: the ignored live test
   `relink_deletes_the_session_and_offers_a_new_qr`
-  (`src/whatsapp.rs:1163-1183`) starts a real client, waits for a QR,
+  (`src/whatsapp.rs:1238-1258`) 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