iobroker.assistant 1.4.2 → 1.4.3

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
@@ -235,6 +235,9 @@ train it twice.
235
235
  Placeholder for the next version (at the beginning of the line):
236
236
  ### **WORK IN PROGRESS**
237
237
  -->
238
+ ### 1.4.3 (2026-11-03)
239
+ * (@GermanBluefox) An empty question sent with `sendTo('ask')` is rejected (`{error: 'empty question'}`) instead of going to the model with nothing but the time context, and the log names the sender once a minute. A script that asked on every answer looped about once a second and used pi-agent tokens. The log line of every `ask` now names the sender
240
+
238
241
  ### 1.4.2 (2026-11-03)
239
242
  * (@GermanBluefox) ESPHome satellites: after 2.5 seconds of speech a pause may be half as long again before the request ends, so a pause for thought no longer cuts off a longer request. Short commands close as fast as before
240
243
  * (@GermanBluefox) ESPHome satellites: speech recognition gets the recording without the device's chirp or beep and without the silence around the speech, and quiet speech is amplified (up to ×10). Whisper made "Продолжение следует..." of the beep and the silence
package/build/main.js CHANGED
@@ -132,6 +132,8 @@ const VOICE_FORM_FIELDS = [
132
132
  ];
133
133
  /** Of those, the protected ones: the browser never gets them, so an empty value there means "keep the saved one" */
134
134
  const VOICE_FORM_SECRETS = ['voiceApiKey', 'openrouterSpeechKey', 'azureSpeechKey', 'awsSecretAccessKey'];
135
+ /** An empty `ask` from the same caller is reported at most this often. */
136
+ const EMPTY_ASK_REPORT_MS = 60_000;
135
137
  /** How long to wait for a device to confirm a write, and how often to look. */
136
138
  const ACK_TIMEOUT_MS = 2000;
137
139
  const ACK_POLL_MS = 150;
@@ -817,6 +819,25 @@ class Assistant extends adapter_core_1.Adapter {
817
819
  voice: (voices[provider] || '').trim() || undefined,
818
820
  });
819
821
  }
822
+ /** Per caller of `ask`: when an empty question was last reported, and how many came since. */
823
+ emptyAsks = new Map();
824
+ /**
825
+ * An empty `ask` is rejected, and reported once a minute per caller. Field report (2026-11-03): a caller asked
826
+ * with empty text every time an answer arrived — about once a second, each one a pi-agent request about "only
827
+ * the time" (the time context was the whole question) and paid tokens. A script that asks on every change of
828
+ * `text.response` loops exactly like that.
829
+ */
830
+ reportEmptyAsk(from) {
831
+ const now = Date.now();
832
+ const entry = this.emptyAsks.get(from) ?? { reportedAt: 0, count: 0 };
833
+ entry.count++;
834
+ if (now - entry.reportedAt >= EMPTY_ASK_REPORT_MS) {
835
+ this.log.warn(`Ignoring an empty question from ${from} (sendTo "ask")${entry.count > 1 ? ` — ${entry.count} since the last report` : ''}. A script that asks whenever text.response changes keeps doing this in a loop.`);
836
+ entry.reportedAt = now;
837
+ entry.count = 0;
838
+ }
839
+ this.emptyAsks.set(from, entry);
840
+ }
820
841
  /** Record the origin of the current text.request/response ('' = state write, 'chat' = message, else satellite). */
821
842
  setQuerySource(source) {
822
843
  this.setStateAsync('text.querySource', { val: source, ack: true }).catch(() => { });
@@ -2696,14 +2717,23 @@ class Assistant extends adapter_core_1.Adapter {
2696
2717
  const text = typeof message === 'string' ? message : message?.text;
2697
2718
  // Origin for text.querySource: caller-provided (e.g. 'telegram:Max') or 'chat' for the admin chat.
2698
2719
  const source = (typeof message === 'string' ? '' : message?.source?.trim()) || 'chat';
2720
+ // Who sent it (javascript.0, telegram.0, admin.0 for the chat tab) — the way to find a script gone wrong.
2721
+ const caller = String(obj.from || '').replace(/^system\.adapter\./, '') || 'unknown';
2722
+ const question = String(text ?? '').trim();
2723
+ if (!question) {
2724
+ this.reportEmptyAsk(caller);
2725
+ if (obj.callback) {
2726
+ this.sendTo(obj.from, obj.command, { error: 'empty question' }, obj.callback);
2727
+ }
2728
+ return;
2729
+ }
2699
2730
  if (!this.ready) {
2700
2731
  if (obj.callback) {
2701
2732
  this.sendTo(obj.from, obj.command, { error: 'agent not ready' }, obj.callback);
2702
2733
  }
2703
2734
  return;
2704
2735
  }
2705
- const question = String(text ?? '');
2706
- this.log.info(`Q (${obj.command}): ${question}`);
2736
+ this.log.info(`Q (${obj.command} from ${caller}): ${question}`);
2707
2737
  this.setStateAsync('text.request', { val: question, ack: true }).catch(() => { });
2708
2738
  this.setQuerySource(source);
2709
2739
  try {
@@ -2732,6 +2762,10 @@ class Assistant extends adapter_core_1.Adapter {
2732
2762
  * later turns have the full thread.
2733
2763
  */
2734
2764
  answer(question, source = '', origin = {}) {
2765
+ if (!question.trim()) {
2766
+ // Nothing to answer — and a model asked with nothing but the time context still costs a request.
2767
+ return Promise.resolve('');
2768
+ }
2735
2769
  const voice = VOICE_TRANSPORTS.has(origin.transport || '');
2736
2770
  return this.requestOrigin.run({ source, room: origin.room || '', voice }, () => this.journal
2737
2771
  ? this.journal.run(question, source, origin, () => {