iobroker.assistant 1.4.6 → 1.4.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -57,7 +57,9 @@ the origin in `assistant.0.text.querySource` (satellite name, `chat`, or empty f
57
57
  or — if it is a URL/path to an audio file (`.mp3`/`.wav`/…) — played back.
58
58
 
59
59
  > **Playing audio files needs [ffmpeg](https://ffmpeg.org/) on the host** (Windows and Linux):
60
- > Linux → `sudo apt install ffmpeg`; Windows → install ffmpeg and add it to the `PATH`.
60
+ > Linux → `sudo apt install ffmpeg`; Windows → install ffmpeg and add it to the `PATH`, or enter its full path
61
+ > under **Path to ffmpeg** on the Voice tab. Without either, **Download ffmpeg** on the Voice tab fetches a static
62
+ > build (ffmpeg-static, 20–30 MB) into the data directory of the instance.
61
63
  > Plain-text TTS does not need it — only audio-file playback (mp3/wav/…) does.
62
64
 
63
65
  ### Start a Hannah satellite pointed at this adapter
@@ -235,6 +237,16 @@ train it twice.
235
237
  Placeholder for the next version (at the beginning of the line):
236
238
  ### **WORK IN PROGRESS**
237
239
  -->
240
+ ### 1.4.7 (2026-11-04)
241
+ * (@GermanBluefox) Voice messages: `ask` also takes a recording (`audio` as base64 plus `format`, e.g. `ogg`) instead of text. It is recognised with the speech recognition of the Voice tab and answered like a text question; the reply carries the recognised text as `heard`. Telegram uses it for voice messages
242
+ * (@GermanBluefox) Path to ffmpeg can be set on the Voice tab (empty = from the search path), with a button that checks it
243
+ * (@GermanBluefox) Hosts without ffmpeg: "Download ffmpeg" on the Voice tab fetches a static build of ffmpeg-static (20-30 MB, checked against a fixed SHA-256) into the data directory of the instance; it is used while no path is entered
244
+ * (@GermanBluefox) Pictures: `ask` takes `images` (base64 JPEG/PNG/GIF/WebP, at most 4) and passes them to the cloud model (OpenAI-compatible, Anthropic, pi-agent); without a text the question is "What is in this picture?". Rules and the local model are skipped for them. Telegram uses it for photos
245
+ * (@GermanBluefox) Videos: `ask` takes a reference `video: { adapter, path }` into the file storage (never the content). Videos are not looked at yet: without a question the answer says so, with one the model is told
246
+ * (@GermanBluefox) ESPHome: after a reply that asked something, every later reply re-opened the microphone. The firmware keeps `continue_conversation` until it is told "0", so every reply now says it, like Home Assistant does
247
+ * (@GermanBluefox) Satellites only listen again after a reply that asks something: a [[LISTEN]] marker (pi-agent's confirmations) or a question mark at the end. A question mark in the middle of a reply no longer opens the microphone
248
+ * (@GermanBluefox) ESPHome satellites with a screen: yes/no buttons (event `confirm_answer`) answer the open question like a spoken "yes"/"no" - the microphone closes, the reply is announced on that satellite. A select `display_language` is set to the voice language when the satellite connects
249
+
238
250
  ### 1.4.6 (2026-11-03)
239
251
  * (@GermanBluefox) Speech recognition: own OpenAI-compatible server (Voice tab, provider OpenAI, "Speech recognition server"), e.g. a local whisper-large-v3-turbo in speaches, faster-whisper-server or LocalAI. It needs no OpenAI key and never gets it; an own key for the server is optional
240
252
 
@@ -226,5 +226,10 @@
226
226
  "Speech recognition server (optional)": "Server für die Spracherkennung (optional)",
227
227
  "OpenAI-compatible server instead of OpenAI, e.g. a local Whisper: http://192.168.1.10:8000/v1 (speaches, faster-whisper-server, LocalAI). Empty = OpenAI.": "OpenAI-kompatibler Server statt OpenAI, z. B. ein lokales Whisper: http://192.168.1.10:8000/v1 (speaches, faster-whisper-server, LocalAI). Leer = OpenAI.",
228
228
  "Key of the speech recognition server": "Schlüssel des Spracherkennungs-Servers",
229
- "Only if the server wants one. The OpenAI key is never sent to it.": "Nur, wenn der Server einen verlangt. Der OpenAI-Schlüssel wird nie dorthin geschickt."
229
+ "Only if the server wants one. The OpenAI key is never sent to it.": "Nur, wenn der Server einen verlangt. Der OpenAI-Schlüssel wird nie dorthin geschickt.",
230
+ "Path to ffmpeg": "Pfad zu ffmpeg",
231
+ "Empty = the downloaded ffmpeg, otherwise ffmpeg from the search path. Needed for timer/alarm sounds, Telegram voice messages and MP3/Opus replies. Example: /usr/bin/ffmpeg or C:\\ffmpeg\\bin\\ffmpeg.exe": "Leer = das heruntergeladene ffmpeg, sonst ffmpeg aus dem Suchpfad. Wird für Timer-/Weckertöne, Telegram-Sprachnachrichten und MP3/Opus-Antworten gebraucht. Beispiel: /usr/bin/ffmpeg oder C:\\ffmpeg\\bin\\ffmpeg.exe",
232
+ "Check ffmpeg": "ffmpeg prüfen",
233
+ "Download ffmpeg": "ffmpeg herunterladen",
234
+ "Downloads a static ffmpeg (ffmpeg-static, 20-30 MB) into the data directory of the instance. It is used when no path is entered.": "Lädt ein statisches ffmpeg (ffmpeg-static, 20–30 MB) in das Datenverzeichnis der Instanz. Es wird genutzt, wenn kein Pfad eingetragen ist."
230
235
  }
@@ -226,5 +226,10 @@
226
226
  "Speech recognition server (optional)": "Speech recognition server (optional)",
227
227
  "OpenAI-compatible server instead of OpenAI, e.g. a local Whisper: http://192.168.1.10:8000/v1 (speaches, faster-whisper-server, LocalAI). Empty = OpenAI.": "OpenAI-compatible server instead of OpenAI, e.g. a local Whisper: http://192.168.1.10:8000/v1 (speaches, faster-whisper-server, LocalAI). Empty = OpenAI.",
228
228
  "Key of the speech recognition server": "Key of the speech recognition server",
229
- "Only if the server wants one. The OpenAI key is never sent to it.": "Only if the server wants one. The OpenAI key is never sent to it."
229
+ "Only if the server wants one. The OpenAI key is never sent to it.": "Only if the server wants one. The OpenAI key is never sent to it.",
230
+ "Path to ffmpeg": "Path to ffmpeg",
231
+ "Empty = the downloaded ffmpeg, otherwise ffmpeg from the search path. Needed for timer/alarm sounds, Telegram voice messages and MP3/Opus replies. Example: /usr/bin/ffmpeg or C:\\ffmpeg\\bin\\ffmpeg.exe": "Empty = the downloaded ffmpeg, otherwise ffmpeg from the search path. Needed for timer/alarm sounds, Telegram voice messages and MP3/Opus replies. Example: /usr/bin/ffmpeg or C:\\ffmpeg\\bin\\ffmpeg.exe",
232
+ "Check ffmpeg": "Check ffmpeg",
233
+ "Download ffmpeg": "Download ffmpeg",
234
+ "Downloads a static ffmpeg (ffmpeg-static, 20-30 MB) into the data directory of the instance. It is used when no path is entered.": "Downloads a static ffmpeg (ffmpeg-static, 20-30 MB) into the data directory of the instance. It is used when no path is entered."
230
235
  }
@@ -226,5 +226,10 @@
226
226
  "Speech recognition server (optional)": "Servidor de reconocimiento de voz (opcional)",
227
227
  "OpenAI-compatible server instead of OpenAI, e.g. a local Whisper: http://192.168.1.10:8000/v1 (speaches, faster-whisper-server, LocalAI). Empty = OpenAI.": "Servidor compatible con OpenAI en lugar de OpenAI, p. ej. un Whisper local: http://192.168.1.10:8000/v1 (speaches, faster-whisper-server, LocalAI). Vacío = OpenAI.",
228
228
  "Key of the speech recognition server": "Clave del servidor de reconocimiento de voz",
229
- "Only if the server wants one. The OpenAI key is never sent to it.": "Solo si el servidor la pide. La clave de OpenAI nunca se envía allí."
229
+ "Only if the server wants one. The OpenAI key is never sent to it.": "Solo si el servidor la pide. La clave de OpenAI nunca se envía allí.",
230
+ "Path to ffmpeg": "Ruta de ffmpeg",
231
+ "Empty = the downloaded ffmpeg, otherwise ffmpeg from the search path. Needed for timer/alarm sounds, Telegram voice messages and MP3/Opus replies. Example: /usr/bin/ffmpeg or C:\\ffmpeg\\bin\\ffmpeg.exe": "Vacío = el ffmpeg descargado, si no, ffmpeg de la ruta de búsqueda. Necesario para los sonidos de temporizador/alarma, los mensajes de voz de Telegram y las respuestas MP3/Opus. Ejemplo: /usr/bin/ffmpeg o C:\\ffmpeg\\bin\\ffmpeg.exe",
232
+ "Check ffmpeg": "Comprobar ffmpeg",
233
+ "Download ffmpeg": "Descargar ffmpeg",
234
+ "Downloads a static ffmpeg (ffmpeg-static, 20-30 MB) into the data directory of the instance. It is used when no path is entered.": "Descarga un ffmpeg estático (ffmpeg-static, 20-30 MB) en el directorio de datos de la instancia. Se usa cuando no se indica ninguna ruta."
230
235
  }
@@ -226,5 +226,10 @@
226
226
  "Speech recognition server (optional)": "Serveur de reconnaissance vocale (facultatif)",
227
227
  "OpenAI-compatible server instead of OpenAI, e.g. a local Whisper: http://192.168.1.10:8000/v1 (speaches, faster-whisper-server, LocalAI). Empty = OpenAI.": "Serveur compatible OpenAI à la place d'OpenAI, p. ex. un Whisper local : http://192.168.1.10:8000/v1 (speaches, faster-whisper-server, LocalAI). Vide = OpenAI.",
228
228
  "Key of the speech recognition server": "Clé du serveur de reconnaissance vocale",
229
- "Only if the server wants one. The OpenAI key is never sent to it.": "Seulement si le serveur en demande une. La clé OpenAI n'y est jamais envoyée."
229
+ "Only if the server wants one. The OpenAI key is never sent to it.": "Seulement si le serveur en demande une. La clé OpenAI n'y est jamais envoyée.",
230
+ "Path to ffmpeg": "Chemin vers ffmpeg",
231
+ "Empty = the downloaded ffmpeg, otherwise ffmpeg from the search path. Needed for timer/alarm sounds, Telegram voice messages and MP3/Opus replies. Example: /usr/bin/ffmpeg or C:\\ffmpeg\\bin\\ffmpeg.exe": "Vide = le ffmpeg téléchargé, sinon ffmpeg du chemin de recherche. Nécessaire pour les sons de minuterie/réveil, les messages vocaux Telegram et les réponses MP3/Opus. Exemple : /usr/bin/ffmpeg ou C:\\ffmpeg\\bin\\ffmpeg.exe",
232
+ "Check ffmpeg": "Vérifier ffmpeg",
233
+ "Download ffmpeg": "Télécharger ffmpeg",
234
+ "Downloads a static ffmpeg (ffmpeg-static, 20-30 MB) into the data directory of the instance. It is used when no path is entered.": "Télécharge un ffmpeg statique (ffmpeg-static, 20 à 30 Mo) dans le répertoire de données de l'instance. Il est utilisé quand aucun chemin n'est indiqué."
230
235
  }
@@ -226,5 +226,10 @@
226
226
  "Speech recognition server (optional)": "Server di riconoscimento vocale (facoltativo)",
227
227
  "OpenAI-compatible server instead of OpenAI, e.g. a local Whisper: http://192.168.1.10:8000/v1 (speaches, faster-whisper-server, LocalAI). Empty = OpenAI.": "Server compatibile con OpenAI al posto di OpenAI, ad es. un Whisper locale: http://192.168.1.10:8000/v1 (speaches, faster-whisper-server, LocalAI). Vuoto = OpenAI.",
228
228
  "Key of the speech recognition server": "Chiave del server di riconoscimento vocale",
229
- "Only if the server wants one. The OpenAI key is never sent to it.": "Solo se il server ne richiede una. La chiave OpenAI non viene mai inviata lì."
229
+ "Only if the server wants one. The OpenAI key is never sent to it.": "Solo se il server ne richiede una. La chiave OpenAI non viene mai inviata lì.",
230
+ "Path to ffmpeg": "Percorso di ffmpeg",
231
+ "Empty = the downloaded ffmpeg, otherwise ffmpeg from the search path. Needed for timer/alarm sounds, Telegram voice messages and MP3/Opus replies. Example: /usr/bin/ffmpeg or C:\\ffmpeg\\bin\\ffmpeg.exe": "Vuoto = l'ffmpeg scaricato, altrimenti ffmpeg dal percorso di ricerca. Necessario per i suoni di timer/sveglia, i messaggi vocali di Telegram e le risposte MP3/Opus. Esempio: /usr/bin/ffmpeg o C:\\ffmpeg\\bin\\ffmpeg.exe",
232
+ "Check ffmpeg": "Verifica ffmpeg",
233
+ "Download ffmpeg": "Scarica ffmpeg",
234
+ "Downloads a static ffmpeg (ffmpeg-static, 20-30 MB) into the data directory of the instance. It is used when no path is entered.": "Scarica un ffmpeg statico (ffmpeg-static, 20-30 MB) nella directory dei dati dell'istanza. Viene usato quando non è indicato alcun percorso."
230
235
  }
@@ -226,5 +226,10 @@
226
226
  "Speech recognition server (optional)": "Server voor spraakherkenning (optioneel)",
227
227
  "OpenAI-compatible server instead of OpenAI, e.g. a local Whisper: http://192.168.1.10:8000/v1 (speaches, faster-whisper-server, LocalAI). Empty = OpenAI.": "OpenAI-compatibele server in plaats van OpenAI, bijv. een lokale Whisper: http://192.168.1.10:8000/v1 (speaches, faster-whisper-server, LocalAI). Leeg = OpenAI.",
228
228
  "Key of the speech recognition server": "Sleutel van de spraakherkenningsserver",
229
- "Only if the server wants one. The OpenAI key is never sent to it.": "Alleen als de server er een vraagt. De OpenAI-sleutel wordt er nooit naartoe gestuurd."
229
+ "Only if the server wants one. The OpenAI key is never sent to it.": "Alleen als de server er een vraagt. De OpenAI-sleutel wordt er nooit naartoe gestuurd.",
230
+ "Path to ffmpeg": "Pad naar ffmpeg",
231
+ "Empty = the downloaded ffmpeg, otherwise ffmpeg from the search path. Needed for timer/alarm sounds, Telegram voice messages and MP3/Opus replies. Example: /usr/bin/ffmpeg or C:\\ffmpeg\\bin\\ffmpeg.exe": "Leeg = de gedownloade ffmpeg, anders ffmpeg uit het zoekpad. Nodig voor timer-/wekkergeluiden, Telegram-spraakberichten en MP3/Opus-antwoorden. Voorbeeld: /usr/bin/ffmpeg of C:\\ffmpeg\\bin\\ffmpeg.exe",
232
+ "Check ffmpeg": "ffmpeg controleren",
233
+ "Download ffmpeg": "ffmpeg downloaden",
234
+ "Downloads a static ffmpeg (ffmpeg-static, 20-30 MB) into the data directory of the instance. It is used when no path is entered.": "Downloadt een statische ffmpeg (ffmpeg-static, 20-30 MB) naar de gegevensmap van de instantie. Die wordt gebruikt als er geen pad is ingevuld."
230
235
  }
@@ -226,5 +226,10 @@
226
226
  "Speech recognition server (optional)": "Serwer rozpoznawania mowy (opcjonalnie)",
227
227
  "OpenAI-compatible server instead of OpenAI, e.g. a local Whisper: http://192.168.1.10:8000/v1 (speaches, faster-whisper-server, LocalAI). Empty = OpenAI.": "Serwer zgodny z OpenAI zamiast OpenAI, np. lokalny Whisper: http://192.168.1.10:8000/v1 (speaches, faster-whisper-server, LocalAI). Puste = OpenAI.",
228
228
  "Key of the speech recognition server": "Klucz serwera rozpoznawania mowy",
229
- "Only if the server wants one. The OpenAI key is never sent to it.": "Tylko jeśli serwer go wymaga. Klucz OpenAI nigdy nie jest tam wysyłany."
229
+ "Only if the server wants one. The OpenAI key is never sent to it.": "Tylko jeśli serwer go wymaga. Klucz OpenAI nigdy nie jest tam wysyłany.",
230
+ "Path to ffmpeg": "Ścieżka do ffmpeg",
231
+ "Empty = the downloaded ffmpeg, otherwise ffmpeg from the search path. Needed for timer/alarm sounds, Telegram voice messages and MP3/Opus replies. Example: /usr/bin/ffmpeg or C:\\ffmpeg\\bin\\ffmpeg.exe": "Puste = pobrany ffmpeg, w przeciwnym razie ffmpeg ze ścieżki wyszukiwania. Potrzebny do dźwięków minutnika/budzika, wiadomości głosowych z Telegrama i odpowiedzi MP3/Opus. Przykład: /usr/bin/ffmpeg lub C:\\ffmpeg\\bin\\ffmpeg.exe",
232
+ "Check ffmpeg": "Sprawdź ffmpeg",
233
+ "Download ffmpeg": "Pobierz ffmpeg",
234
+ "Downloads a static ffmpeg (ffmpeg-static, 20-30 MB) into the data directory of the instance. It is used when no path is entered.": "Pobiera statyczny ffmpeg (ffmpeg-static, 20–30 MB) do katalogu danych instancji. Jest używany, gdy nie podano ścieżki."
230
235
  }
@@ -226,5 +226,10 @@
226
226
  "Speech recognition server (optional)": "Servidor de reconhecimento de voz (opcional)",
227
227
  "OpenAI-compatible server instead of OpenAI, e.g. a local Whisper: http://192.168.1.10:8000/v1 (speaches, faster-whisper-server, LocalAI). Empty = OpenAI.": "Servidor compatível com OpenAI em vez da OpenAI, p. ex. um Whisper local: http://192.168.1.10:8000/v1 (speaches, faster-whisper-server, LocalAI). Vazio = OpenAI.",
228
228
  "Key of the speech recognition server": "Chave do servidor de reconhecimento de voz",
229
- "Only if the server wants one. The OpenAI key is never sent to it.": "Só se o servidor pedir uma. A chave da OpenAI nunca é enviada para lá."
229
+ "Only if the server wants one. The OpenAI key is never sent to it.": "Só se o servidor pedir uma. A chave da OpenAI nunca é enviada para lá.",
230
+ "Path to ffmpeg": "Caminho para o ffmpeg",
231
+ "Empty = the downloaded ffmpeg, otherwise ffmpeg from the search path. Needed for timer/alarm sounds, Telegram voice messages and MP3/Opus replies. Example: /usr/bin/ffmpeg or C:\\ffmpeg\\bin\\ffmpeg.exe": "Vazio = o ffmpeg transferido, caso contrário ffmpeg do caminho de pesquisa. Necessário para os sons de temporizador/alarme, as mensagens de voz do Telegram e as respostas MP3/Opus. Exemplo: /usr/bin/ffmpeg ou C:\\ffmpeg\\bin\\ffmpeg.exe",
232
+ "Check ffmpeg": "Verificar ffmpeg",
233
+ "Download ffmpeg": "Transferir ffmpeg",
234
+ "Downloads a static ffmpeg (ffmpeg-static, 20-30 MB) into the data directory of the instance. It is used when no path is entered.": "Transfere um ffmpeg estático (ffmpeg-static, 20-30 MB) para o diretório de dados da instância. É usado quando nenhum caminho é indicado."
230
235
  }
@@ -226,5 +226,10 @@
226
226
  "Speech recognition server (optional)": "Сервер распознавания речи (опционально)",
227
227
  "OpenAI-compatible server instead of OpenAI, e.g. a local Whisper: http://192.168.1.10:8000/v1 (speaches, faster-whisper-server, LocalAI). Empty = OpenAI.": "OpenAI-совместимый сервер вместо OpenAI, напр. локальный Whisper: http://192.168.1.10:8000/v1 (speaches, faster-whisper-server, LocalAI). Пусто = OpenAI.",
228
228
  "Key of the speech recognition server": "Ключ сервера распознавания речи",
229
- "Only if the server wants one. The OpenAI key is never sent to it.": "Только если сервер его требует. Ключ OpenAI туда никогда не отправляется."
229
+ "Only if the server wants one. The OpenAI key is never sent to it.": "Только если сервер его требует. Ключ OpenAI туда никогда не отправляется.",
230
+ "Path to ffmpeg": "Путь к ffmpeg",
231
+ "Empty = the downloaded ffmpeg, otherwise ffmpeg from the search path. Needed for timer/alarm sounds, Telegram voice messages and MP3/Opus replies. Example: /usr/bin/ffmpeg or C:\\ffmpeg\\bin\\ffmpeg.exe": "Пусто = загруженный ffmpeg, иначе ffmpeg из пути поиска (PATH). Нужен для звуков таймера/будильника, голосовых сообщений Telegram и ответов в MP3/Opus. Пример: /usr/bin/ffmpeg или C:\\ffmpeg\\bin\\ffmpeg.exe",
232
+ "Check ffmpeg": "Проверить ffmpeg",
233
+ "Download ffmpeg": "Загрузить ffmpeg",
234
+ "Downloads a static ffmpeg (ffmpeg-static, 20-30 MB) into the data directory of the instance. It is used when no path is entered.": "Загружает статический ffmpeg (ffmpeg-static, 20–30 МБ) в каталог данных экземпляра. Используется, если путь не указан."
230
235
  }
@@ -226,5 +226,10 @@
226
226
  "Speech recognition server (optional)": "Сервер розпізнавання мовлення (необов'язково)",
227
227
  "OpenAI-compatible server instead of OpenAI, e.g. a local Whisper: http://192.168.1.10:8000/v1 (speaches, faster-whisper-server, LocalAI). Empty = OpenAI.": "OpenAI-сумісний сервер замість OpenAI, напр. локальний Whisper: http://192.168.1.10:8000/v1 (speaches, faster-whisper-server, LocalAI). Порожньо = OpenAI.",
228
228
  "Key of the speech recognition server": "Ключ сервера розпізнавання мовлення",
229
- "Only if the server wants one. The OpenAI key is never sent to it.": "Лише якщо сервер його вимагає. Ключ OpenAI туди ніколи не надсилається."
229
+ "Only if the server wants one. The OpenAI key is never sent to it.": "Лише якщо сервер його вимагає. Ключ OpenAI туди ніколи не надсилається.",
230
+ "Path to ffmpeg": "Шлях до ffmpeg",
231
+ "Empty = the downloaded ffmpeg, otherwise ffmpeg from the search path. Needed for timer/alarm sounds, Telegram voice messages and MP3/Opus replies. Example: /usr/bin/ffmpeg or C:\\ffmpeg\\bin\\ffmpeg.exe": "Порожньо = завантажений ffmpeg, інакше ffmpeg зі шляху пошуку (PATH). Потрібен для звуків таймера/будильника, голосових повідомлень Telegram і відповідей у MP3/Opus. Приклад: /usr/bin/ffmpeg або C:\\ffmpeg\\bin\\ffmpeg.exe",
232
+ "Check ffmpeg": "Перевірити ffmpeg",
233
+ "Download ffmpeg": "Завантажити ffmpeg",
234
+ "Downloads a static ffmpeg (ffmpeg-static, 20-30 MB) into the data directory of the instance. It is used when no path is entered.": "Завантажує статичний ffmpeg (ffmpeg-static, 20–30 МБ) до каталогу даних екземпляра. Використовується, якщо шлях не вказано."
230
235
  }
@@ -226,5 +226,10 @@
226
226
  "Speech recognition server (optional)": "语音识别服务器(可选)",
227
227
  "OpenAI-compatible server instead of OpenAI, e.g. a local Whisper: http://192.168.1.10:8000/v1 (speaches, faster-whisper-server, LocalAI). Empty = OpenAI.": "代替 OpenAI 的 OpenAI 兼容服务器,例如本地 Whisper:http://192.168.1.10:8000/v1(speaches、faster-whisper-server、LocalAI)。空 = OpenAI。",
228
228
  "Key of the speech recognition server": "语音识别服务器的密钥",
229
- "Only if the server wants one. The OpenAI key is never sent to it.": "仅当服务器需要时填写。OpenAI 密钥永远不会发送到该服务器。"
229
+ "Only if the server wants one. The OpenAI key is never sent to it.": "仅当服务器需要时填写。OpenAI 密钥永远不会发送到该服务器。",
230
+ "Path to ffmpeg": "ffmpeg 路径",
231
+ "Empty = the downloaded ffmpeg, otherwise ffmpeg from the search path. Needed for timer/alarm sounds, Telegram voice messages and MP3/Opus replies. Example: /usr/bin/ffmpeg or C:\\ffmpeg\\bin\\ffmpeg.exe": "留空 = 使用已下载的 ffmpeg,否则使用搜索路径中的 ffmpeg。计时器/闹钟提示音、Telegram 语音消息以及 MP3/Opus 回复需要它。示例:/usr/bin/ffmpeg 或 C:\\ffmpeg\\bin\\ffmpeg.exe",
232
+ "Check ffmpeg": "检查 ffmpeg",
233
+ "Download ffmpeg": "下载 ffmpeg",
234
+ "Downloads a static ffmpeg (ffmpeg-static, 20-30 MB) into the data directory of the instance. It is used when no path is entered.": "将静态 ffmpeg(ffmpeg-static,20–30 MB)下载到实例的数据目录。未填写路径时使用它。"
230
235
  }
@@ -1510,6 +1510,47 @@
1510
1510
  "lg": 5,
1511
1511
  "xl": 5
1512
1512
  },
1513
+ "ffmpegPath": {
1514
+ "newLine": true,
1515
+ "type": "text",
1516
+ "label": "Path to ffmpeg",
1517
+ "help": "Empty = the downloaded ffmpeg, otherwise ffmpeg from the search path. Needed for timer/alarm sounds, Telegram voice messages and MP3/Opus replies. Example: /usr/bin/ffmpeg or C:\\ffmpeg\\bin\\ffmpeg.exe",
1518
+ "default": "",
1519
+ "hidden": "!data.voiceEnabled",
1520
+ "xs": 12,
1521
+ "sm": 8,
1522
+ "md": 5,
1523
+ "lg": 5,
1524
+ "xl": 5
1525
+ },
1526
+ "_testFfmpeg": {
1527
+ "type": "sendTo",
1528
+ "command": "testFfmpeg",
1529
+ "label": "Check ffmpeg",
1530
+ "variant": "outlined",
1531
+ "jsonData": "{\"path\":\"${data.ffmpegPath}\"}",
1532
+ "hidden": "!data.voiceEnabled",
1533
+ "xs": 12,
1534
+ "sm": 4,
1535
+ "md": 3,
1536
+ "lg": 3,
1537
+ "xl": 3
1538
+ },
1539
+ "_downloadFfmpeg": {
1540
+ "type": "sendTo",
1541
+ "command": "downloadFfmpeg",
1542
+ "label": "Download ffmpeg",
1543
+ "title": "Downloads a static ffmpeg (ffmpeg-static, 20-30 MB) into the data directory of the instance. It is used when no path is entered.",
1544
+ "variant": "outlined",
1545
+ "showProcess": true,
1546
+ "jsonData": "{\"path\":\"${data.ffmpegPath}\"}",
1547
+ "hidden": "!data.voiceEnabled",
1548
+ "xs": 12,
1549
+ "sm": 4,
1550
+ "md": 3,
1551
+ "lg": 3,
1552
+ "xl": 3
1553
+ },
1513
1554
  "_voiceTest": {
1514
1555
  "newLine": true,
1515
1556
  "type": "custom",
package/build/lib/ask.js CHANGED
@@ -14,10 +14,20 @@
14
14
  */
15
15
  Object.defineProperty(exports, "__esModule", { value: true });
16
16
  exports.PendingQuestions = exports.ASK_TIMEOUT_MS = exports.ANY_SOURCE = void 0;
17
+ exports.endsWithQuestion = endsWithQuestion;
17
18
  /** Key for a question that any source may answer (a broadcast question). */
18
19
  exports.ANY_SOURCE = '*';
19
20
  /** Default time to wait for an answer before giving up. */
20
21
  exports.ASK_TIMEOUT_MS = 60_000;
22
+ /**
23
+ * Whether a reply ends with a question, so a satellite should re-open its mic for the answer. Only the end counts:
24
+ * "Wie spät es ist? Es ist zwölf." waits for nothing, and a mic opened for nothing hears the room and ends with
25
+ * "No text recognized" (field report 2026-11-04). A model that asks in the middle ("… setzen? Sag ja oder nein.")
26
+ * marks its reply with [[LISTEN]] instead — pi-agent does that for its confirmations.
27
+ */
28
+ function endsWithQuestion(reply) {
29
+ return /[??؟][\s"'»«“”„)\]]*$/u.test(reply || '');
30
+ }
21
31
  class PendingQuestions {
22
32
  defaultTimeoutMs;
23
33
  /** Every armed key points at its entry; several keys can share one (a question asked in two rooms). */
@@ -1 +1 @@
1
- {"version":3,"file":"ask.js","sourceRoot":"","sources":["../../src/lib/ask.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Pending questions — the other direction of a conversation: the adapter asks and waits for the answer.\n *\n * Everywhere else the user starts the exchange. Here a script, a timer or (later) a trigger asks\n * something (\"The fryer has been on for 5 hours. Shall I switch it off?\") and the *next* utterance from\n * that source is the answer: it must not go to the NLU or the LLM, which would treat a bare \"yes\" as a\n * command and swallow it.\n *\n * A question is armed for one or more **sources** — the same key the answer pipeline sees: a satellite's\n * device name, `'chat'`, `'telegram:<user>'`, … — or for {@link ANY_SOURCE} when it was broadcast and\n * whoever hears it may answer. If nothing arrives within the timeout the promise resolves with `null`,\n * so a question can never block the pipeline for good.\n */\n\n/** Key for a question that any source may answer (a broadcast question). */\nexport const ANY_SOURCE = '*';\n\n/** Default time to wait for an answer before giving up. */\nexport const ASK_TIMEOUT_MS = 60_000;\n\ninterface Entry {\n keys: string[];\n question: string;\n asked: number;\n timer: NodeJS.Timeout | null;\n done: boolean;\n settle: (answer: string | null) => void;\n}\n\n/** One open question, for logging and diagnostics. */\nexport interface PendingInfo {\n question: string;\n /** Sources that may answer it (`'*'` = any). */\n keys: string[];\n /** When it was asked (ms epoch). */\n asked: number;\n}\n\nexport class PendingQuestions {\n /** Every armed key points at its entry; several keys can share one (a question asked in two rooms). */\n private readonly byKey = new Map<string, Entry>();\n\n constructor(private readonly defaultTimeoutMs = ASK_TIMEOUT_MS) {}\n\n /**\n * Arm `question` for `keys` (empty → {@link ANY_SOURCE}) and resolve with the first answer from one of\n * them, or with `null` on timeout. Arming a key that already holds a question cancels that older one:\n * the newest question is the one the user just heard, so it has to be the one that wins.\n */\n ask(keys: string[], question: string, timeoutMs?: number): Promise<string | null> {\n const unique = [...new Set(keys.map(k => String(k ?? '').trim()).filter(Boolean))];\n if (!unique.length) {\n unique.push(ANY_SOURCE);\n }\n for (const key of unique) {\n this.cancel(key);\n }\n return new Promise<string | null>(resolve => {\n const entry: Entry = {\n keys: unique,\n question,\n asked: Date.now(),\n timer: null,\n done: false,\n settle: resolve,\n };\n // Deliberately not unref'd: the timeout is what releases the caller, so it has to fire. It is\n // cleared on an answer, on cancel, and by cancelAll() on unload, so nothing is held for long.\n entry.timer = setTimeout(() => this.finish(entry, null), timeoutMs ?? this.defaultTimeoutMs);\n for (const key of unique) {\n this.byKey.set(key, entry);\n }\n });\n }\n\n /**\n * Consume an utterance as the answer to an open question. Returns true when it was taken — the caller\n * must then stop processing it. Blank text is never an answer (a dropped utterance would otherwise\n * silently satisfy the question).\n */\n deliver(source: string, text: string): boolean {\n if (!String(text ?? '').trim()) {\n return false;\n }\n const entry = this.find(source);\n if (!entry) {\n return false;\n }\n this.finish(entry, text);\n return true;\n }\n\n /** Whether an open question would take the next utterance from `source`. */\n has(source: string): boolean {\n return !!this.find(source);\n }\n\n /** The open question `source` is expected to answer, if any (for logs). */\n question(source: string): string {\n return this.find(source)?.question ?? '';\n }\n\n /** Give up on the question armed for `key` (resolves it with `null`). */\n cancel(key: string): boolean {\n const entry = this.byKey.get(key);\n if (!entry) {\n return false;\n }\n this.finish(entry, null);\n return true;\n }\n\n /** Give up on every open question — on unload, so no caller is left hanging. */\n cancelAll(): void {\n for (const entry of [...new Set(this.byKey.values())]) {\n this.finish(entry, null);\n }\n this.byKey.clear();\n }\n\n /** All open questions (one entry per question, not per key). */\n list(): PendingInfo[] {\n return [...new Set(this.byKey.values())].map(e => ({\n question: e.question,\n keys: [...e.keys],\n asked: e.asked,\n }));\n }\n\n /** The entry responsible for `source`: armed for it by name, else the broadcast one. */\n private find(source: string): Entry | undefined {\n const key = String(source ?? '').trim();\n return (key ? this.byKey.get(key) : undefined) ?? this.byKey.get(ANY_SOURCE);\n }\n\n private finish(entry: Entry, answer: string | null): void {\n if (entry.done) {\n return;\n }\n entry.done = true;\n if (entry.timer) {\n clearTimeout(entry.timer);\n entry.timer = null;\n }\n for (const key of entry.keys) {\n // Only drop keys still pointing here — a newer question may already have taken one over.\n if (this.byKey.get(key) === entry) {\n this.byKey.delete(key);\n }\n }\n entry.settle(answer);\n }\n}\n"]}
1
+ {"version":3,"file":"ask.js","sourceRoot":"","sources":["../../src/lib/ask.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Pending questions — the other direction of a conversation: the adapter asks and waits for the answer.\n *\n * Everywhere else the user starts the exchange. Here a script, a timer or (later) a trigger asks\n * something (\"The fryer has been on for 5 hours. Shall I switch it off?\") and the *next* utterance from\n * that source is the answer: it must not go to the NLU or the LLM, which would treat a bare \"yes\" as a\n * command and swallow it.\n *\n * A question is armed for one or more **sources** — the same key the answer pipeline sees: a satellite's\n * device name, `'chat'`, `'telegram:<user>'`, … — or for {@link ANY_SOURCE} when it was broadcast and\n * whoever hears it may answer. If nothing arrives within the timeout the promise resolves with `null`,\n * so a question can never block the pipeline for good.\n */\n\n/** Key for a question that any source may answer (a broadcast question). */\nexport const ANY_SOURCE = '*';\n\n/** Default time to wait for an answer before giving up. */\nexport const ASK_TIMEOUT_MS = 60_000;\n\n/**\n * Whether a reply ends with a question, so a satellite should re-open its mic for the answer. Only the end counts:\n * \"Wie spät es ist? Es ist zwölf.\" waits for nothing, and a mic opened for nothing hears the room and ends with\n * \"No text recognized\" (field report 2026-11-04). A model that asks in the middle (\"… setzen? Sag ja oder nein.\")\n * marks its reply with [[LISTEN]] instead — pi-agent does that for its confirmations.\n */\nexport function endsWithQuestion(reply: string): boolean {\n return /[??؟][\\s\"'»«“”„)\\]]*$/u.test(reply || '');\n}\n\ninterface Entry {\n keys: string[];\n question: string;\n asked: number;\n timer: NodeJS.Timeout | null;\n done: boolean;\n settle: (answer: string | null) => void;\n}\n\n/** One open question, for logging and diagnostics. */\nexport interface PendingInfo {\n question: string;\n /** Sources that may answer it (`'*'` = any). */\n keys: string[];\n /** When it was asked (ms epoch). */\n asked: number;\n}\n\nexport class PendingQuestions {\n /** Every armed key points at its entry; several keys can share one (a question asked in two rooms). */\n private readonly byKey = new Map<string, Entry>();\n\n constructor(private readonly defaultTimeoutMs = ASK_TIMEOUT_MS) {}\n\n /**\n * Arm `question` for `keys` (empty → {@link ANY_SOURCE}) and resolve with the first answer from one of\n * them, or with `null` on timeout. Arming a key that already holds a question cancels that older one:\n * the newest question is the one the user just heard, so it has to be the one that wins.\n */\n ask(keys: string[], question: string, timeoutMs?: number): Promise<string | null> {\n const unique = [...new Set(keys.map(k => String(k ?? '').trim()).filter(Boolean))];\n if (!unique.length) {\n unique.push(ANY_SOURCE);\n }\n for (const key of unique) {\n this.cancel(key);\n }\n return new Promise<string | null>(resolve => {\n const entry: Entry = {\n keys: unique,\n question,\n asked: Date.now(),\n timer: null,\n done: false,\n settle: resolve,\n };\n // Deliberately not unref'd: the timeout is what releases the caller, so it has to fire. It is\n // cleared on an answer, on cancel, and by cancelAll() on unload, so nothing is held for long.\n entry.timer = setTimeout(() => this.finish(entry, null), timeoutMs ?? this.defaultTimeoutMs);\n for (const key of unique) {\n this.byKey.set(key, entry);\n }\n });\n }\n\n /**\n * Consume an utterance as the answer to an open question. Returns true when it was taken — the caller\n * must then stop processing it. Blank text is never an answer (a dropped utterance would otherwise\n * silently satisfy the question).\n */\n deliver(source: string, text: string): boolean {\n if (!String(text ?? '').trim()) {\n return false;\n }\n const entry = this.find(source);\n if (!entry) {\n return false;\n }\n this.finish(entry, text);\n return true;\n }\n\n /** Whether an open question would take the next utterance from `source`. */\n has(source: string): boolean {\n return !!this.find(source);\n }\n\n /** The open question `source` is expected to answer, if any (for logs). */\n question(source: string): string {\n return this.find(source)?.question ?? '';\n }\n\n /** Give up on the question armed for `key` (resolves it with `null`). */\n cancel(key: string): boolean {\n const entry = this.byKey.get(key);\n if (!entry) {\n return false;\n }\n this.finish(entry, null);\n return true;\n }\n\n /** Give up on every open question — on unload, so no caller is left hanging. */\n cancelAll(): void {\n for (const entry of [...new Set(this.byKey.values())]) {\n this.finish(entry, null);\n }\n this.byKey.clear();\n }\n\n /** All open questions (one entry per question, not per key). */\n list(): PendingInfo[] {\n return [...new Set(this.byKey.values())].map(e => ({\n question: e.question,\n keys: [...e.keys],\n asked: e.asked,\n }));\n }\n\n /** The entry responsible for `source`: armed for it by name, else the broadcast one. */\n private find(source: string): Entry | undefined {\n const key = String(source ?? '').trim();\n return (key ? this.byKey.get(key) : undefined) ?? this.byKey.get(ANY_SOURCE);\n }\n\n private finish(entry: Entry, answer: string | null): void {\n if (entry.done) {\n return;\n }\n entry.done = true;\n if (entry.timer) {\n clearTimeout(entry.timer);\n entry.timer = null;\n }\n for (const key of entry.keys) {\n // Only drop keys still pointing here — a newer question may already have taken one over.\n if (this.byKey.get(key) === entry) {\n this.byKey.delete(key);\n }\n }\n entry.settle(answer);\n }\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"journal.js","sourceRoot":"","sources":["../../src/lib/journal.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Conversation journal: one entry per request the assistant answered — where it came from (chat, script,\n * Telegram, which satellite, which wake word), which tier answered it (routine, rule-based NLU, local\n * model, cloud model), which tools the model called and which state ids were read or written along the\n * way. The admin chat shows it live, so one can check what a satellite heard and what was done about it.\n *\n * Tools and state accesses happen deep inside the pipeline (NLU executor, LLM tool loop, routine\n * executor). Instead of threading an entry object through all of them, the entry rides along in an\n * {@link AsyncLocalStorage}: {@link Journal.run} opens it, and {@link Journal.note}/{@link Journal.tool}/\n * {@link Journal.stateAccess} write to whatever entry the current call chain belongs to — and do nothing\n * outside of one (a trigger writing a state is not a conversation).\n *\n * Besides the entry, every request keeps a **trace**: the steps of its processing with their timing and\n * whatever explains them — the NLU's parse, the full prompt and every round of the model, tool arguments\n * and results in full, speech recognition and synthesis. Traces are too big to ride along in `journal.last`\n * and in `journal.json`, so the chat fetches one when a request is opened ({@link Journal.trace}); each is\n * kept in a file of its own and only for the newest {@link MAX_TRACES} requests.\n */\nimport { AsyncLocalStorage } from 'node:async_hooks';\nimport { promises as fs } from 'node:fs';\nimport * as path from 'node:path';\nimport type { VadReport } from './voice/vad';\n\n/** How a request reached us. */\nexport type JournalTransport = 'chat' | 'script' | 'state' | 'telegram' | 'esphome' | 'udp' | 'native' | 'wyoming';\n\n/** Which stage of the pipeline produced the answer. */\nexport type JournalTier =\n | 'stop' // stop word silenced a ringing timer/alarm\n | 'askReply' // the utterance answered an open `askUser` question\n | 'routine'\n | 'nlu'\n | 'localLlm'\n | 'cloud'\n | 'none'; // nothing local understood it and there is no cloud provider\n\n/** Tokens a model call used (summed over the rounds of a request in {@link JournalEntry.tokens}). */\nexport interface JournalTokens {\n input: number;\n output: number;\n /** Prompt tokens served from the provider's prompt cache. */\n cacheRead?: number;\n /** Prompt tokens written to the provider's prompt cache (Anthropic). */\n cacheWrite?: number;\n}\n\n/** One step of how a request was processed — what the debug view of the admin chat shows. */\nexport interface JournalStep {\n /** Start of the step in ms after the request started (negative for speech recognition, which comes first). */\n t: number;\n /** Duration, for steps that took time. */\n ms?: number;\n /** Pipeline stage: request, stt, context, stop, askReply, routine, youtube, nlu, localLlm, cloud, tool, state, answer, tts, error. */\n stage: string;\n title: string;\n level?: 'warn' | 'error';\n tokens?: JournalTokens;\n /** Whatever explains the step (prompts, model responses, parse results) — a JSON snapshot. */\n data?: unknown;\n}\n\n/** What a caller records; the journal adds the timing. */\nexport type JournalStepInput = Omit<JournalStep, 't'>;\n\n/** Hook for library code that should not know the journal (the LLM agent). */\nexport type TraceFn = (step: JournalStepInput) => void;\n\nexport interface JournalTool {\n name: string;\n args: Record<string, unknown>;\n ms: number;\n /** Result, JSON, shortened. */\n result?: string;\n error?: string;\n}\n\nexport interface JournalEntry {\n id: string;\n /** Start of the answer, ms. */\n ts: number;\n /** False while the answer is still being produced. */\n done: boolean;\n /** Time to produce the answer (without STT/TTS). */\n ms?: number;\n source: string;\n transport: JournalTransport;\n satellite?: string;\n room?: string;\n /** Wake word the satellite reported; '' = the mic was re-opened for a follow-up, absent = unknown. */\n wakeWord?: string;\n /** Speech recognition time, for voice requests. */\n sttMs?: number;\n /** Length of the recorded utterance. */\n audioMs?: number;\n /** Speech synthesis time of the reply (voice requests, filled in after the answer). */\n ttsMs?: number;\n /** Length of the spoken reply. */\n replyAudioMs?: number;\n question: string;\n answer?: string;\n error?: string;\n tier?: JournalTier;\n /** Model name, routine name or NLU actions — whatever explains the tier. */\n detail?: string;\n tools: JournalTool[];\n /** State ids read to produce the answer. */\n read: string[];\n /** States written to produce the answer. */\n written: { id: string; value: unknown }[];\n /** The reply asks back, so a satellite re-opened its mic. */\n followUp?: boolean;\n /** Tokens of all model calls, when a cloud model answered. */\n tokens?: JournalTokens;\n}\n\n/** What the caller knows about a request before the pipeline starts. */\nexport interface JournalOrigin {\n transport?: JournalTransport;\n satellite?: string;\n room?: string;\n wakeWord?: string;\n sttMs?: number;\n audioMs?: number;\n /** Voice activity detection of the utterance (ESPHome) — trace only. */\n vad?: VadReport;\n}\n\n/** What a transport reports after it synthesised the reply. */\nexport interface JournalSpeech {\n ms: number;\n audioMs: number;\n provider: string;\n fallback?: string;\n voice?: string;\n}\n\nexport interface JournalOptions {\n /** Retention in hours; 0 disables the journal entirely. */\n hours: number;\n /** File the journal is kept in across restarts ('' = memory only). */\n file: string;\n /** Called with every new or updated entry (drives the `journal.last` state the chat listens to). */\n onChange?: (entry: JournalEntry) => void;\n log: ioBroker.Logger;\n}\n\n/** Hard cap independent of the retention — a script asking every few seconds must not fill the disk. */\nconst MAX_ENTRIES = 3000;\nconst SAVE_DELAY_MS = 5000;\nconst RESULT_CHARS = 300;\n/** Requests whose trace is kept on disk. */\nexport const MAX_TRACES = 200;\n/** Traces held in memory (the newest ones — the ones somebody is about to open). */\nconst MEMORY_TRACES = 30;\n/** Steps per request; a tool loop gone wild must not grow one trace without end. */\nconst MAX_STEPS = 400;\n/** Longer strings are cut — enough for a system prompt with a few hundred devices. */\nconst STEP_STRING_CHARS = 100_000;\n/** A reply is matched to its synthesis only this long after the answer. */\nconst SPEECH_WINDOW_MS = 120_000;\n/** Entry ids as {@link Journal.run} makes them — checked before one becomes a file name. */\nconst ID_RE = /^[a-z0-9-]+$/;\n\n/** Directory the traces of a journal file are kept in. */\nexport function traceDirFor(file: string): string {\n return path.join(path.dirname(file), 'journal-trace');\n}\n\n/**\n * JSON snapshot of a step's data — taken when it is recorded, because the objects go on changing (the\n * model's message list grows, cache markers move). Long strings are cut and buffers summarised.\n */\nexport function snapshot(value: unknown): unknown {\n if (value === undefined) {\n return undefined;\n }\n try {\n const text = JSON.stringify(value, (_key, v: unknown) => {\n if (typeof v === 'string' && v.length > STEP_STRING_CHARS) {\n return `${v.slice(0, STEP_STRING_CHARS)}… [+${v.length - STEP_STRING_CHARS} chars]`;\n }\n if (typeof v === 'bigint') {\n return v.toString();\n }\n // Buffer.toJSON() has already run when the replacer sees it.\n const b = v as { type?: unknown; data?: unknown } | null;\n if (b && typeof b === 'object' && b.type === 'Buffer' && Array.isArray(b.data)) {\n return `<${b.data.length} bytes>`;\n }\n return v;\n });\n return text === undefined ? undefined : JSON.parse(text);\n } catch (e) {\n return `<not serializable: ${(e as Error).message}>`;\n }\n}\n\n/** Everything an error carries that helps — an API error's status and body, the stack. */\nexport function errorInfo(e: unknown): Record<string, unknown> {\n if (!(e instanceof Error)) {\n return { message: String(e) };\n }\n const info: Record<string, unknown> = { name: e.name, message: e.message };\n for (const key of ['status', 'code', 'type', 'param', 'error', 'request_id', 'requestID']) {\n const v = (e as unknown as Record<string, unknown>)[key];\n if (v !== undefined && v !== null) {\n info[key] = v;\n }\n }\n if (e.stack) {\n info.stack = e.stack;\n }\n return info;\n}\n\n/** Derive the transport from a request source when the caller did not say. */\nexport function transportForSource(source: string): JournalTransport {\n if (source === 'chat') {\n return 'chat';\n }\n if (source === '') {\n return 'state';\n }\n if (source === 'wyoming') {\n return 'wyoming';\n }\n if (source.startsWith('telegram:')) {\n return 'telegram';\n }\n return 'script';\n}\n\nexport class Journal {\n private entries: JournalEntry[] = [];\n private readonly store = new AsyncLocalStorage<JournalEntry>();\n private saveTimer: NodeJS.Timeout | null = null;\n private counter = 0;\n /** Steps of the newest requests; older ones are read back from their file. */\n private readonly traces = new Map<string, JournalStep[]>();\n /** Traces changed since they were last written. */\n private readonly dirty = new Set<string>();\n /** LLM tool calls in progress per request: a state access inside one belongs to that tool's step. */\n private readonly toolDepth = new WeakMap<JournalEntry, number>();\n private readonly traceDir: string;\n\n constructor(private readonly opts: JournalOptions) {\n this.traceDir = opts.file ? traceDirFor(opts.file) : '';\n }\n\n get enabled(): boolean {\n return this.opts.hours > 0;\n }\n\n /** Load the entries of the last run (silently starts empty when there are none). */\n async load(): Promise<void> {\n if (!this.enabled || !this.opts.file) {\n return;\n }\n try {\n const raw = JSON.parse(await fs.readFile(this.opts.file, 'utf8')) as unknown;\n if (Array.isArray(raw)) {\n // An entry still open at shutdown never got its answer.\n this.entries = (raw as JournalEntry[])\n .filter(e => e && typeof e.id === 'string' && typeof e.ts === 'number')\n .map(e => (e.done ? e : { ...e, done: true, error: e.error || 'interrupted' }));\n this.prune();\n }\n } catch (e) {\n if ((e as NodeJS.ErrnoException).code !== 'ENOENT') {\n this.opts.log.warn(`Journal could not be read: ${(e as Error).message}`);\n }\n }\n }\n\n /** Entries newer than `since` (ms), oldest first. */\n list(since = 0): JournalEntry[] {\n this.prune();\n return this.entries.filter(e => e.ts > since);\n }\n\n clear(): void {\n this.entries = [];\n this.traces.clear();\n this.dirty.clear();\n this.scheduleSave();\n }\n\n /** The entry of a request (the chat asks for it together with its trace). */\n get(id: string): JournalEntry | undefined {\n return this.entries.find(e => e.id === id);\n }\n\n /** The processing steps of a request, or null when none were kept (too old, or from an older version). */\n async trace(id: string): Promise<JournalStep[] | null> {\n if (!ID_RE.test(id) || !this.get(id)) {\n return null;\n }\n const steps = this.traces.get(id);\n if (steps) {\n return steps;\n }\n if (!this.traceDir) {\n return null;\n }\n try {\n const raw = JSON.parse(await fs.readFile(path.join(this.traceDir, `${id}.json`), 'utf8')) as {\n steps?: unknown;\n };\n return Array.isArray(raw?.steps) ? (raw.steps as JournalStep[]) : null;\n } catch {\n return null;\n }\n }\n\n /**\n * Run `fn` as one journalled request. The entry is published once when the request starts (so the\n * chat shows the question while the answer is still being worked out) and once when it is finished.\n */\n async run(question: string, source: string, origin: JournalOrigin, fn: () => Promise<string>): Promise<string> {\n if (!this.enabled) {\n return fn();\n }\n const entry: JournalEntry = {\n id: `${Date.now().toString(36)}-${(this.counter++).toString(36)}`,\n ts: Date.now(),\n done: false,\n source,\n transport: origin.transport || transportForSource(source),\n question,\n tools: [],\n read: [],\n written: [],\n };\n for (const key of ['satellite', 'room', 'wakeWord', 'sttMs', 'audioMs'] as const) {\n if (origin[key] !== undefined && origin[key] !== '') {\n (entry as unknown as Record<string, unknown>)[key] = origin[key];\n }\n }\n // A satellite that re-opened its mic reports no wake word — say so instead of \"unknown\".\n if (origin.wakeWord === '') {\n entry.wakeWord = '';\n }\n this.entries.push(entry);\n this.traces.set(entry.id, []);\n this.addStep(entry, {\n stage: 'request',\n title: `Request from ${source || entry.transport} (${entry.transport})`,\n data: { question, source, origin },\n });\n this.publish(entry);\n try {\n const answer = await this.store.run(entry, fn);\n entry.answer = answer;\n return answer;\n } catch (e) {\n entry.error = (e as Error).message;\n this.addStep(entry, { stage: 'error', level: 'error', title: entry.error, data: errorInfo(e) });\n throw e;\n } finally {\n entry.done = true;\n entry.ms = Date.now() - entry.ts;\n this.dirty.add(entry.id);\n this.publish(entry);\n this.prune();\n this.scheduleSave();\n }\n }\n\n /** Add a step to the current request's trace (nothing outside of one). */\n step(input: JournalStepInput): void {\n const entry = this.store.getStore();\n if (entry) {\n this.addStep(entry, input);\n }\n }\n\n /**\n * Run an LLM tool as part of the current request: the call lands in the entry's tool list and, with the\n * full arguments and result, in the trace.\n */\n async toolCall<T>(name: string, args: Record<string, unknown>, fn: () => Promise<T>): Promise<T> {\n const entry = this.store.getStore();\n if (!entry) {\n return fn();\n }\n this.toolDepth.set(entry, (this.toolDepth.get(entry) || 0) + 1);\n const start = Date.now();\n try {\n const result = await fn();\n const ms = Date.now() - start;\n this.addStep(entry, { stage: 'tool', title: `Tool ${name}`, ms, data: { args, result } });\n this.tool(name, args, ms, result);\n return result;\n } catch (e) {\n const ms = Date.now() - start;\n const message = (e as Error).message;\n this.addStep(entry, {\n stage: 'tool',\n level: 'error',\n title: `Tool ${name} failed: ${message}`,\n ms,\n data: { args, error: errorInfo(e) },\n });\n this.tool(name, args, ms, undefined, message);\n throw e;\n } finally {\n this.toolDepth.set(entry, (this.toolDepth.get(entry) || 1) - 1);\n }\n }\n\n /**\n * Run a state call (MCP `get_states`/`set_state`/…) as part of the current request: the ids land in the\n * entry's read/written lists, and — unless an LLM tool made the call, whose step already shows it — the\n * call with its result in the trace.\n */\n async stateCall<T>(name: string, args: Record<string, unknown> | undefined, fn: () => Promise<T>): Promise<T> {\n const entry = this.store.getStore();\n if (!entry) {\n return fn();\n }\n this.stateAccess(name, args);\n if (this.toolDepth.get(entry)) {\n return fn();\n }\n const start = Date.now();\n try {\n const result = await fn();\n this.addStep(entry, { stage: 'state', title: name, ms: Date.now() - start, data: { args, result } });\n return result;\n } catch (e) {\n this.addStep(entry, {\n stage: 'state',\n level: 'error',\n title: `${name} failed: ${(e as Error).message}`,\n ms: Date.now() - start,\n data: { args, error: errorInfo(e) },\n });\n throw e;\n }\n }\n\n /**\n * A transport synthesised the reply to the latest request of `source`. That happens after the answer,\n * outside of the request, so the entry is found by its source.\n */\n spoken(source: string, speech: JournalSpeech): void {\n const now = Date.now();\n for (let i = this.entries.length - 1; i >= 0; i--) {\n const entry = this.entries[i];\n if (entry.source !== source) {\n continue;\n }\n if (!entry.done || entry.ttsMs !== undefined || now - entry.ts - (entry.ms || 0) > SPEECH_WINDOW_MS) {\n return;\n }\n entry.ttsMs = speech.ms;\n entry.replyAudioMs = Math.round(speech.audioMs);\n const voice = speech.voice ? ` (${speech.voice})` : '';\n this.addStep(entry, {\n stage: 'tts',\n title: `Speech synthesis: ${speech.provider}${voice}, ${(speech.audioMs / 1000).toFixed(1)} s of audio`,\n ms: speech.ms,\n data: speech,\n });\n this.dirty.add(entry.id);\n this.publish(entry);\n this.scheduleSave();\n return;\n }\n }\n\n private addStep(entry: JournalEntry, input: JournalStepInput): void {\n const steps = this.traces.get(entry.id);\n if (!steps || steps.length > MAX_STEPS) {\n return;\n }\n if (steps.length === MAX_STEPS) {\n steps.push({\n t: Date.now() - entry.ts,\n stage: 'error',\n level: 'warn',\n title: `More than ${MAX_STEPS} steps — the rest is not recorded`,\n });\n return;\n }\n const step: JournalStep = {\n t: Date.now() - entry.ts - (input.ms || 0),\n stage: input.stage,\n title: input.title,\n };\n if (input.ms !== undefined) {\n step.ms = input.ms;\n }\n if (input.level) {\n step.level = input.level;\n }\n if (input.tokens) {\n step.tokens = input.tokens;\n const sum = (entry.tokens ||= { input: 0, output: 0 });\n sum.input += input.tokens.input || 0;\n sum.output += input.tokens.output || 0;\n if (input.tokens.cacheRead) {\n sum.cacheRead = (sum.cacheRead || 0) + input.tokens.cacheRead;\n }\n if (input.tokens.cacheWrite) {\n sum.cacheWrite = (sum.cacheWrite || 0) + input.tokens.cacheWrite;\n }\n }\n if (input.data !== undefined) {\n step.data = snapshot(input.data);\n }\n steps.push(step);\n }\n\n /** Fill in fields of the current request's entry (tier, detail, followUp …). */\n note(fields: Partial<Pick<JournalEntry, 'tier' | 'detail' | 'followUp'>>): void {\n const entry = this.store.getStore();\n if (entry) {\n Object.assign(entry, fields);\n }\n }\n\n /** Record a tool call the model made for the current request. */\n tool(name: string, args: Record<string, unknown>, ms: number, result: unknown, error?: string): void {\n const entry = this.store.getStore();\n if (!entry) {\n return;\n }\n const item: JournalTool = { name, args: args || {}, ms };\n if (error) {\n item.error = error;\n } else {\n let text: string;\n try {\n text = typeof result === 'string' ? result : JSON.stringify(result);\n } catch {\n text = String(result);\n }\n item.result = text.length > RESULT_CHARS ? `${text.slice(0, RESULT_CHARS)}…` : text;\n }\n entry.tools.push(item);\n // Live view: show each tool call as it happens, a cloud round can take seconds.\n this.publish(entry);\n }\n\n /** Record which states a call reads or writes (MCP `get_states`/`set_state`/`set_states`, routines). */\n stateAccess(tool: string, args: Record<string, unknown> | undefined): void {\n const entry = this.store.getStore();\n if (!entry || !args) {\n return;\n }\n if (tool === 'get_states' && Array.isArray(args.ids)) {\n for (const id of args.ids) {\n if (typeof id === 'string' && !entry.read.includes(id)) {\n entry.read.push(id);\n }\n }\n } else if (tool === 'get_state' && typeof args.id === 'string') {\n if (!entry.read.includes(args.id)) {\n entry.read.push(args.id);\n }\n } else if (tool === 'set_state' && typeof args.id === 'string') {\n entry.written.push({ id: args.id, value: args.value });\n } else if (tool === 'set_states' && Array.isArray(args.states)) {\n for (const s of args.states as { id?: unknown; value?: unknown }[]) {\n if (typeof s?.id === 'string') {\n entry.written.push({ id: s.id, value: s.value });\n }\n }\n }\n }\n\n /** Write pending changes now (on unload). */\n async flush(): Promise<void> {\n if (this.saveTimer) {\n clearTimeout(this.saveTimer);\n this.saveTimer = null;\n }\n await this.save();\n }\n\n private publish(entry: JournalEntry): void {\n try {\n this.opts.onChange?.(entry);\n } catch {\n // a listener must never break the answer\n }\n }\n\n private prune(): void {\n const cutoff = Date.now() - this.opts.hours * 3600_000;\n let start = 0;\n while (start < this.entries.length && this.entries[start].ts < cutoff) {\n start++;\n }\n start = Math.max(start, this.entries.length - MAX_ENTRIES);\n if (start > 0) {\n for (const e of this.entries.slice(0, start)) {\n this.traces.delete(e.id);\n this.dirty.delete(e.id);\n }\n this.entries = this.entries.slice(start);\n }\n // Without a file the memory is all there is: keep as many as would be on disk.\n const keep = this.traceDir ? MEMORY_TRACES : MAX_TRACES;\n for (const id of this.traces.keys()) {\n if (this.traces.size <= keep) {\n break;\n }\n // Not written yet, or still being answered: needed until it is on disk.\n if ((!this.traceDir || !this.dirty.has(id)) && this.get(id)?.done !== false) {\n this.traces.delete(id);\n }\n }\n }\n\n private scheduleSave(): void {\n if (!this.opts.file || this.saveTimer) {\n return;\n }\n this.saveTimer = setTimeout(() => {\n this.saveTimer = null;\n void this.save();\n }, SAVE_DELAY_MS);\n this.saveTimer.unref?.();\n }\n\n private async save(): Promise<void> {\n if (!this.opts.file) {\n return;\n }\n try {\n await fs.mkdir(path.dirname(this.opts.file), { recursive: true });\n await fs.writeFile(this.opts.file, JSON.stringify(this.entries.filter(e => e.done)));\n } catch (e) {\n this.opts.log.warn(`Journal could not be saved: ${(e as Error).message}`);\n }\n await this.saveTraces();\n this.prune();\n }\n\n /** Write the traces that changed, and delete the files of requests that are no longer kept. */\n private async saveTraces(): Promise<void> {\n try {\n await fs.mkdir(this.traceDir, { recursive: true });\n for (const id of [...this.dirty]) {\n const steps = this.traces.get(id);\n this.dirty.delete(id);\n if (steps) {\n await fs.writeFile(path.join(this.traceDir, `${id}.json`), JSON.stringify({ id, steps }));\n }\n }\n const keep = new Set(this.entries.slice(-MAX_TRACES).map(e => e.id));\n for (const name of await fs.readdir(this.traceDir)) {\n if (name.endsWith('.json') && !keep.has(name.slice(0, -5))) {\n await fs.rm(path.join(this.traceDir, name), { force: true });\n }\n }\n } catch (e) {\n this.opts.log.warn(`Journal traces could not be saved: ${(e as Error).message}`);\n }\n }\n}\n"]}
1
+ {"version":3,"file":"journal.js","sourceRoot":"","sources":["../../src/lib/journal.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Conversation journal: one entry per request the assistant answered — where it came from (chat, script,\n * Telegram, which satellite, which wake word), which tier answered it (routine, rule-based NLU, local\n * model, cloud model), which tools the model called and which state ids were read or written along the\n * way. The admin chat shows it live, so one can check what a satellite heard and what was done about it.\n *\n * Tools and state accesses happen deep inside the pipeline (NLU executor, LLM tool loop, routine\n * executor). Instead of threading an entry object through all of them, the entry rides along in an\n * {@link AsyncLocalStorage}: {@link Journal.run} opens it, and {@link Journal.note}/{@link Journal.tool}/\n * {@link Journal.stateAccess} write to whatever entry the current call chain belongs to — and do nothing\n * outside of one (a trigger writing a state is not a conversation).\n *\n * Besides the entry, every request keeps a **trace**: the steps of its processing with their timing and\n * whatever explains them — the NLU's parse, the full prompt and every round of the model, tool arguments\n * and results in full, speech recognition and synthesis. Traces are too big to ride along in `journal.last`\n * and in `journal.json`, so the chat fetches one when a request is opened ({@link Journal.trace}); each is\n * kept in a file of its own and only for the newest {@link MAX_TRACES} requests.\n */\nimport { AsyncLocalStorage } from 'node:async_hooks';\nimport { promises as fs } from 'node:fs';\nimport * as path from 'node:path';\nimport type { VadReport } from './voice/vad';\n\n/** How a request reached us. */\nexport type JournalTransport = 'chat' | 'script' | 'state' | 'telegram' | 'esphome' | 'udp' | 'native' | 'wyoming';\n\n/** Which stage of the pipeline produced the answer. */\nexport type JournalTier =\n | 'stop' // stop word silenced a ringing timer/alarm\n | 'askReply' // the utterance answered an open `askUser` question\n | 'routine'\n | 'nlu'\n | 'localLlm'\n | 'cloud'\n | 'none'; // nothing local understood it and there is no cloud provider\n\n/** Tokens a model call used (summed over the rounds of a request in {@link JournalEntry.tokens}). */\nexport interface JournalTokens {\n input: number;\n output: number;\n /** Prompt tokens served from the provider's prompt cache. */\n cacheRead?: number;\n /** Prompt tokens written to the provider's prompt cache (Anthropic). */\n cacheWrite?: number;\n}\n\n/** One step of how a request was processed — what the debug view of the admin chat shows. */\nexport interface JournalStep {\n /** Start of the step in ms after the request started (negative for speech recognition, which comes first). */\n t: number;\n /** Duration, for steps that took time. */\n ms?: number;\n /** Pipeline stage: request, stt, context, stop, askReply, routine, youtube, nlu, localLlm, cloud, tool, state, answer, tts, error. */\n stage: string;\n title: string;\n level?: 'warn' | 'error';\n tokens?: JournalTokens;\n /** Whatever explains the step (prompts, model responses, parse results) — a JSON snapshot. */\n data?: unknown;\n}\n\n/** What a caller records; the journal adds the timing. */\nexport type JournalStepInput = Omit<JournalStep, 't'>;\n\n/** Hook for library code that should not know the journal (the LLM agent). */\nexport type TraceFn = (step: JournalStepInput) => void;\n\nexport interface JournalTool {\n name: string;\n args: Record<string, unknown>;\n ms: number;\n /** Result, JSON, shortened. */\n result?: string;\n error?: string;\n}\n\nexport interface JournalEntry {\n id: string;\n /** Start of the answer, ms. */\n ts: number;\n /** False while the answer is still being produced. */\n done: boolean;\n /** Time to produce the answer (without STT/TTS). */\n ms?: number;\n source: string;\n transport: JournalTransport;\n satellite?: string;\n room?: string;\n /** Wake word the satellite reported; '' = the mic was re-opened for a follow-up, absent = unknown. */\n wakeWord?: string;\n /** Speech recognition time, for voice requests. */\n sttMs?: number;\n /** Length of the recorded utterance. */\n audioMs?: number;\n /** Speech synthesis time of the reply (voice requests, filled in after the answer). */\n ttsMs?: number;\n /** Length of the spoken reply. */\n replyAudioMs?: number;\n question: string;\n answer?: string;\n error?: string;\n tier?: JournalTier;\n /** Model name, routine name or NLU actions — whatever explains the tier. */\n detail?: string;\n tools: JournalTool[];\n /** State ids read to produce the answer. */\n read: string[];\n /** States written to produce the answer. */\n written: { id: string; value: unknown }[];\n /** The reply asks back, so a satellite re-opened its mic. */\n followUp?: boolean;\n /** Tokens of all model calls, when a cloud model answered. */\n tokens?: JournalTokens;\n}\n\n/** What the caller knows about a request before the pipeline starts. */\nexport interface JournalOrigin {\n transport?: JournalTransport;\n satellite?: string;\n room?: string;\n wakeWord?: string;\n sttMs?: number;\n audioMs?: number;\n /** Voice activity detection of the utterance (ESPHome) — trace only. */\n vad?: VadReport;\n /** Answered with the yes/no button of a satellite's screen instead of speech (ESPHome) — trace only. */\n button?: 'yes' | 'no';\n}\n\n/** What a transport reports after it synthesised the reply. */\nexport interface JournalSpeech {\n ms: number;\n audioMs: number;\n provider: string;\n fallback?: string;\n voice?: string;\n}\n\nexport interface JournalOptions {\n /** Retention in hours; 0 disables the journal entirely. */\n hours: number;\n /** File the journal is kept in across restarts ('' = memory only). */\n file: string;\n /** Called with every new or updated entry (drives the `journal.last` state the chat listens to). */\n onChange?: (entry: JournalEntry) => void;\n log: ioBroker.Logger;\n}\n\n/** Hard cap independent of the retention — a script asking every few seconds must not fill the disk. */\nconst MAX_ENTRIES = 3000;\nconst SAVE_DELAY_MS = 5000;\nconst RESULT_CHARS = 300;\n/** Requests whose trace is kept on disk. */\nexport const MAX_TRACES = 200;\n/** Traces held in memory (the newest ones — the ones somebody is about to open). */\nconst MEMORY_TRACES = 30;\n/** Steps per request; a tool loop gone wild must not grow one trace without end. */\nconst MAX_STEPS = 400;\n/** Longer strings are cut — enough for a system prompt with a few hundred devices. */\nconst STEP_STRING_CHARS = 100_000;\n/** A reply is matched to its synthesis only this long after the answer. */\nconst SPEECH_WINDOW_MS = 120_000;\n/** Entry ids as {@link Journal.run} makes them — checked before one becomes a file name. */\nconst ID_RE = /^[a-z0-9-]+$/;\n\n/** Directory the traces of a journal file are kept in. */\nexport function traceDirFor(file: string): string {\n return path.join(path.dirname(file), 'journal-trace');\n}\n\n/**\n * JSON snapshot of a step's data — taken when it is recorded, because the objects go on changing (the\n * model's message list grows, cache markers move). Long strings are cut and buffers summarised.\n */\nexport function snapshot(value: unknown): unknown {\n if (value === undefined) {\n return undefined;\n }\n try {\n const text = JSON.stringify(value, (_key, v: unknown) => {\n if (typeof v === 'string' && v.length > STEP_STRING_CHARS) {\n return `${v.slice(0, STEP_STRING_CHARS)}… [+${v.length - STEP_STRING_CHARS} chars]`;\n }\n if (typeof v === 'bigint') {\n return v.toString();\n }\n // Buffer.toJSON() has already run when the replacer sees it.\n const b = v as { type?: unknown; data?: unknown } | null;\n if (b && typeof b === 'object' && b.type === 'Buffer' && Array.isArray(b.data)) {\n return `<${b.data.length} bytes>`;\n }\n return v;\n });\n return text === undefined ? undefined : JSON.parse(text);\n } catch (e) {\n return `<not serializable: ${(e as Error).message}>`;\n }\n}\n\n/** Everything an error carries that helps — an API error's status and body, the stack. */\nexport function errorInfo(e: unknown): Record<string, unknown> {\n if (!(e instanceof Error)) {\n return { message: String(e) };\n }\n const info: Record<string, unknown> = { name: e.name, message: e.message };\n for (const key of ['status', 'code', 'type', 'param', 'error', 'request_id', 'requestID']) {\n const v = (e as unknown as Record<string, unknown>)[key];\n if (v !== undefined && v !== null) {\n info[key] = v;\n }\n }\n if (e.stack) {\n info.stack = e.stack;\n }\n return info;\n}\n\n/** Derive the transport from a request source when the caller did not say. */\nexport function transportForSource(source: string): JournalTransport {\n if (source === 'chat') {\n return 'chat';\n }\n if (source === '') {\n return 'state';\n }\n if (source === 'wyoming') {\n return 'wyoming';\n }\n if (source.startsWith('telegram:')) {\n return 'telegram';\n }\n return 'script';\n}\n\nexport class Journal {\n private entries: JournalEntry[] = [];\n private readonly store = new AsyncLocalStorage<JournalEntry>();\n private saveTimer: NodeJS.Timeout | null = null;\n private counter = 0;\n /** Steps of the newest requests; older ones are read back from their file. */\n private readonly traces = new Map<string, JournalStep[]>();\n /** Traces changed since they were last written. */\n private readonly dirty = new Set<string>();\n /** LLM tool calls in progress per request: a state access inside one belongs to that tool's step. */\n private readonly toolDepth = new WeakMap<JournalEntry, number>();\n private readonly traceDir: string;\n\n constructor(private readonly opts: JournalOptions) {\n this.traceDir = opts.file ? traceDirFor(opts.file) : '';\n }\n\n get enabled(): boolean {\n return this.opts.hours > 0;\n }\n\n /** Load the entries of the last run (silently starts empty when there are none). */\n async load(): Promise<void> {\n if (!this.enabled || !this.opts.file) {\n return;\n }\n try {\n const raw = JSON.parse(await fs.readFile(this.opts.file, 'utf8')) as unknown;\n if (Array.isArray(raw)) {\n // An entry still open at shutdown never got its answer.\n this.entries = (raw as JournalEntry[])\n .filter(e => e && typeof e.id === 'string' && typeof e.ts === 'number')\n .map(e => (e.done ? e : { ...e, done: true, error: e.error || 'interrupted' }));\n this.prune();\n }\n } catch (e) {\n if ((e as NodeJS.ErrnoException).code !== 'ENOENT') {\n this.opts.log.warn(`Journal could not be read: ${(e as Error).message}`);\n }\n }\n }\n\n /** Entries newer than `since` (ms), oldest first. */\n list(since = 0): JournalEntry[] {\n this.prune();\n return this.entries.filter(e => e.ts > since);\n }\n\n clear(): void {\n this.entries = [];\n this.traces.clear();\n this.dirty.clear();\n this.scheduleSave();\n }\n\n /** The entry of a request (the chat asks for it together with its trace). */\n get(id: string): JournalEntry | undefined {\n return this.entries.find(e => e.id === id);\n }\n\n /** The processing steps of a request, or null when none were kept (too old, or from an older version). */\n async trace(id: string): Promise<JournalStep[] | null> {\n if (!ID_RE.test(id) || !this.get(id)) {\n return null;\n }\n const steps = this.traces.get(id);\n if (steps) {\n return steps;\n }\n if (!this.traceDir) {\n return null;\n }\n try {\n const raw = JSON.parse(await fs.readFile(path.join(this.traceDir, `${id}.json`), 'utf8')) as {\n steps?: unknown;\n };\n return Array.isArray(raw?.steps) ? (raw.steps as JournalStep[]) : null;\n } catch {\n return null;\n }\n }\n\n /**\n * Run `fn` as one journalled request. The entry is published once when the request starts (so the\n * chat shows the question while the answer is still being worked out) and once when it is finished.\n */\n async run(question: string, source: string, origin: JournalOrigin, fn: () => Promise<string>): Promise<string> {\n if (!this.enabled) {\n return fn();\n }\n const entry: JournalEntry = {\n id: `${Date.now().toString(36)}-${(this.counter++).toString(36)}`,\n ts: Date.now(),\n done: false,\n source,\n transport: origin.transport || transportForSource(source),\n question,\n tools: [],\n read: [],\n written: [],\n };\n for (const key of ['satellite', 'room', 'wakeWord', 'sttMs', 'audioMs'] as const) {\n if (origin[key] !== undefined && origin[key] !== '') {\n (entry as unknown as Record<string, unknown>)[key] = origin[key];\n }\n }\n // A satellite that re-opened its mic reports no wake word — say so instead of \"unknown\".\n if (origin.wakeWord === '') {\n entry.wakeWord = '';\n }\n this.entries.push(entry);\n this.traces.set(entry.id, []);\n this.addStep(entry, {\n stage: 'request',\n title: `Request from ${source || entry.transport} (${entry.transport})`,\n data: { question, source, origin },\n });\n this.publish(entry);\n try {\n const answer = await this.store.run(entry, fn);\n entry.answer = answer;\n return answer;\n } catch (e) {\n entry.error = (e as Error).message;\n this.addStep(entry, { stage: 'error', level: 'error', title: entry.error, data: errorInfo(e) });\n throw e;\n } finally {\n entry.done = true;\n entry.ms = Date.now() - entry.ts;\n this.dirty.add(entry.id);\n this.publish(entry);\n this.prune();\n this.scheduleSave();\n }\n }\n\n /** Add a step to the current request's trace (nothing outside of one). */\n step(input: JournalStepInput): void {\n const entry = this.store.getStore();\n if (entry) {\n this.addStep(entry, input);\n }\n }\n\n /**\n * Run an LLM tool as part of the current request: the call lands in the entry's tool list and, with the\n * full arguments and result, in the trace.\n */\n async toolCall<T>(name: string, args: Record<string, unknown>, fn: () => Promise<T>): Promise<T> {\n const entry = this.store.getStore();\n if (!entry) {\n return fn();\n }\n this.toolDepth.set(entry, (this.toolDepth.get(entry) || 0) + 1);\n const start = Date.now();\n try {\n const result = await fn();\n const ms = Date.now() - start;\n this.addStep(entry, { stage: 'tool', title: `Tool ${name}`, ms, data: { args, result } });\n this.tool(name, args, ms, result);\n return result;\n } catch (e) {\n const ms = Date.now() - start;\n const message = (e as Error).message;\n this.addStep(entry, {\n stage: 'tool',\n level: 'error',\n title: `Tool ${name} failed: ${message}`,\n ms,\n data: { args, error: errorInfo(e) },\n });\n this.tool(name, args, ms, undefined, message);\n throw e;\n } finally {\n this.toolDepth.set(entry, (this.toolDepth.get(entry) || 1) - 1);\n }\n }\n\n /**\n * Run a state call (MCP `get_states`/`set_state`/…) as part of the current request: the ids land in the\n * entry's read/written lists, and — unless an LLM tool made the call, whose step already shows it — the\n * call with its result in the trace.\n */\n async stateCall<T>(name: string, args: Record<string, unknown> | undefined, fn: () => Promise<T>): Promise<T> {\n const entry = this.store.getStore();\n if (!entry) {\n return fn();\n }\n this.stateAccess(name, args);\n if (this.toolDepth.get(entry)) {\n return fn();\n }\n const start = Date.now();\n try {\n const result = await fn();\n this.addStep(entry, { stage: 'state', title: name, ms: Date.now() - start, data: { args, result } });\n return result;\n } catch (e) {\n this.addStep(entry, {\n stage: 'state',\n level: 'error',\n title: `${name} failed: ${(e as Error).message}`,\n ms: Date.now() - start,\n data: { args, error: errorInfo(e) },\n });\n throw e;\n }\n }\n\n /**\n * A transport synthesised the reply to the latest request of `source`. That happens after the answer,\n * outside of the request, so the entry is found by its source.\n */\n spoken(source: string, speech: JournalSpeech): void {\n const now = Date.now();\n for (let i = this.entries.length - 1; i >= 0; i--) {\n const entry = this.entries[i];\n if (entry.source !== source) {\n continue;\n }\n if (!entry.done || entry.ttsMs !== undefined || now - entry.ts - (entry.ms || 0) > SPEECH_WINDOW_MS) {\n return;\n }\n entry.ttsMs = speech.ms;\n entry.replyAudioMs = Math.round(speech.audioMs);\n const voice = speech.voice ? ` (${speech.voice})` : '';\n this.addStep(entry, {\n stage: 'tts',\n title: `Speech synthesis: ${speech.provider}${voice}, ${(speech.audioMs / 1000).toFixed(1)} s of audio`,\n ms: speech.ms,\n data: speech,\n });\n this.dirty.add(entry.id);\n this.publish(entry);\n this.scheduleSave();\n return;\n }\n }\n\n private addStep(entry: JournalEntry, input: JournalStepInput): void {\n const steps = this.traces.get(entry.id);\n if (!steps || steps.length > MAX_STEPS) {\n return;\n }\n if (steps.length === MAX_STEPS) {\n steps.push({\n t: Date.now() - entry.ts,\n stage: 'error',\n level: 'warn',\n title: `More than ${MAX_STEPS} steps — the rest is not recorded`,\n });\n return;\n }\n const step: JournalStep = {\n t: Date.now() - entry.ts - (input.ms || 0),\n stage: input.stage,\n title: input.title,\n };\n if (input.ms !== undefined) {\n step.ms = input.ms;\n }\n if (input.level) {\n step.level = input.level;\n }\n if (input.tokens) {\n step.tokens = input.tokens;\n const sum = (entry.tokens ||= { input: 0, output: 0 });\n sum.input += input.tokens.input || 0;\n sum.output += input.tokens.output || 0;\n if (input.tokens.cacheRead) {\n sum.cacheRead = (sum.cacheRead || 0) + input.tokens.cacheRead;\n }\n if (input.tokens.cacheWrite) {\n sum.cacheWrite = (sum.cacheWrite || 0) + input.tokens.cacheWrite;\n }\n }\n if (input.data !== undefined) {\n step.data = snapshot(input.data);\n }\n steps.push(step);\n }\n\n /** Fill in fields of the current request's entry (tier, detail, followUp …). */\n note(fields: Partial<Pick<JournalEntry, 'tier' | 'detail' | 'followUp'>>): void {\n const entry = this.store.getStore();\n if (entry) {\n Object.assign(entry, fields);\n }\n }\n\n /** Record a tool call the model made for the current request. */\n tool(name: string, args: Record<string, unknown>, ms: number, result: unknown, error?: string): void {\n const entry = this.store.getStore();\n if (!entry) {\n return;\n }\n const item: JournalTool = { name, args: args || {}, ms };\n if (error) {\n item.error = error;\n } else {\n let text: string;\n try {\n text = typeof result === 'string' ? result : JSON.stringify(result);\n } catch {\n text = String(result);\n }\n item.result = text.length > RESULT_CHARS ? `${text.slice(0, RESULT_CHARS)}…` : text;\n }\n entry.tools.push(item);\n // Live view: show each tool call as it happens, a cloud round can take seconds.\n this.publish(entry);\n }\n\n /** Record which states a call reads or writes (MCP `get_states`/`set_state`/`set_states`, routines). */\n stateAccess(tool: string, args: Record<string, unknown> | undefined): void {\n const entry = this.store.getStore();\n if (!entry || !args) {\n return;\n }\n if (tool === 'get_states' && Array.isArray(args.ids)) {\n for (const id of args.ids) {\n if (typeof id === 'string' && !entry.read.includes(id)) {\n entry.read.push(id);\n }\n }\n } else if (tool === 'get_state' && typeof args.id === 'string') {\n if (!entry.read.includes(args.id)) {\n entry.read.push(args.id);\n }\n } else if (tool === 'set_state' && typeof args.id === 'string') {\n entry.written.push({ id: args.id, value: args.value });\n } else if (tool === 'set_states' && Array.isArray(args.states)) {\n for (const s of args.states as { id?: unknown; value?: unknown }[]) {\n if (typeof s?.id === 'string') {\n entry.written.push({ id: s.id, value: s.value });\n }\n }\n }\n }\n\n /** Write pending changes now (on unload). */\n async flush(): Promise<void> {\n if (this.saveTimer) {\n clearTimeout(this.saveTimer);\n this.saveTimer = null;\n }\n await this.save();\n }\n\n private publish(entry: JournalEntry): void {\n try {\n this.opts.onChange?.(entry);\n } catch {\n // a listener must never break the answer\n }\n }\n\n private prune(): void {\n const cutoff = Date.now() - this.opts.hours * 3600_000;\n let start = 0;\n while (start < this.entries.length && this.entries[start].ts < cutoff) {\n start++;\n }\n start = Math.max(start, this.entries.length - MAX_ENTRIES);\n if (start > 0) {\n for (const e of this.entries.slice(0, start)) {\n this.traces.delete(e.id);\n this.dirty.delete(e.id);\n }\n this.entries = this.entries.slice(start);\n }\n // Without a file the memory is all there is: keep as many as would be on disk.\n const keep = this.traceDir ? MEMORY_TRACES : MAX_TRACES;\n for (const id of this.traces.keys()) {\n if (this.traces.size <= keep) {\n break;\n }\n // Not written yet, or still being answered: needed until it is on disk.\n if ((!this.traceDir || !this.dirty.has(id)) && this.get(id)?.done !== false) {\n this.traces.delete(id);\n }\n }\n }\n\n private scheduleSave(): void {\n if (!this.opts.file || this.saveTimer) {\n return;\n }\n this.saveTimer = setTimeout(() => {\n this.saveTimer = null;\n void this.save();\n }, SAVE_DELAY_MS);\n this.saveTimer.unref?.();\n }\n\n private async save(): Promise<void> {\n if (!this.opts.file) {\n return;\n }\n try {\n await fs.mkdir(path.dirname(this.opts.file), { recursive: true });\n await fs.writeFile(this.opts.file, JSON.stringify(this.entries.filter(e => e.done)));\n } catch (e) {\n this.opts.log.warn(`Journal could not be saved: ${(e as Error).message}`);\n }\n await this.saveTraces();\n this.prune();\n }\n\n /** Write the traces that changed, and delete the files of requests that are no longer kept. */\n private async saveTraces(): Promise<void> {\n try {\n await fs.mkdir(this.traceDir, { recursive: true });\n for (const id of [...this.dirty]) {\n const steps = this.traces.get(id);\n this.dirty.delete(id);\n if (steps) {\n await fs.writeFile(path.join(this.traceDir, `${id}.json`), JSON.stringify({ id, steps }));\n }\n }\n const keep = new Set(this.entries.slice(-MAX_TRACES).map(e => e.id));\n for (const name of await fs.readdir(this.traceDir)) {\n if (name.endsWith('.json') && !keep.has(name.slice(0, -5))) {\n await fs.rm(path.join(this.traceDir, name), { force: true });\n }\n }\n } catch (e) {\n this.opts.log.warn(`Journal traces could not be saved: ${(e as Error).message}`);\n }\n }\n}\n"]}