iobroker.assistant 1.0.3 → 1.2.0

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.
Files changed (73) hide show
  1. package/README.md +65 -0
  2. package/admin/custom/.vite/manifest.json +62 -62
  3. package/admin/custom/assets/Components-BSewCBfS.js +2 -0
  4. package/admin/custom/assets/{_virtual_mf-localSharedImportMap___mfe_internal__ConfigCustomAssistant__mf_owner__1-A4gakomR.js → _virtual_mf-localSharedImportMap___mfe_internal__ConfigCustomAssistant__mf_owner__1-C5NPM0Of.js} +1 -1
  5. package/admin/custom/assets/{_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__201375093093590__loadShare___mf_0_emotion_mf_1_styled__loadShare__.js-CwqmfMvn.js → _virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__163539842885036__loadShare___mf_0_emotion_mf_1_styled__loadShare__.js-IaosaymG.js} +1 -1
  6. package/admin/custom/assets/{_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__201375093093590__loadShare___mf_0_iobroker_mf_1_gui_mf_2_components__loadShare__.js-BMuLYe6f.js → _virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__163539842885036__loadShare___mf_0_iobroker_mf_1_gui_mf_2_components__loadShare__.js-Cdyjt5KT.js} +39 -39
  7. package/admin/custom/assets/{_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__201375093093590__loadShare__shared-BxjZmnpc.js → _virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__163539842885036__loadShare__shared-Dsw5zWS8.js} +1 -1
  8. package/admin/custom/assets/_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__163539842885036__prebuild___mf_0_iobroker_mf_1_json_mf_2_config__prebuild__-SgSagTOv.js +122 -0
  9. package/admin/custom/assets/{hostInit-4roC4hEj.js → hostInit-jJe8bIfV.js} +1 -1
  10. package/admin/custom/assets/{lib-CchzgTUs.js → lib-CL8mXvI6.js} +1 -1
  11. package/admin/custom/assets/{mf-entry-bootstrap-0-4806bbe4.js → mf-entry-bootstrap-0-ab227aa6.js} +2 -2
  12. package/admin/custom/assets/{pendingShares-DLlv8yFY.js → pendingShares-wrGtfliQ.js} +1 -1
  13. package/admin/custom/customComponents.js +2 -2
  14. package/admin/custom/i18n/de.json +40 -0
  15. package/admin/custom/i18n/en.json +40 -0
  16. package/admin/custom/i18n/es.json +40 -0
  17. package/admin/custom/i18n/fr.json +40 -0
  18. package/admin/custom/i18n/it.json +40 -0
  19. package/admin/custom/i18n/nl.json +40 -0
  20. package/admin/custom/i18n/pl.json +40 -0
  21. package/admin/custom/i18n/pt.json +40 -0
  22. package/admin/custom/i18n/ru.json +40 -0
  23. package/admin/custom/i18n/uk.json +40 -0
  24. package/admin/custom/i18n/zh-cn.json +40 -0
  25. package/admin/custom/index.html +2 -2
  26. package/admin/custom/mf-manifest.json +1 -1
  27. package/admin/custom/mf-stats.json +1 -1
  28. package/admin/i18n/de.json +13 -3
  29. package/admin/i18n/en.json +13 -3
  30. package/admin/i18n/es.json +13 -3
  31. package/admin/i18n/fr.json +13 -3
  32. package/admin/i18n/it.json +13 -3
  33. package/admin/i18n/nl.json +13 -3
  34. package/admin/i18n/pl.json +13 -3
  35. package/admin/i18n/pt.json +13 -3
  36. package/admin/i18n/ru.json +13 -3
  37. package/admin/i18n/uk.json +13 -3
  38. package/admin/i18n/zh-cn.json +13 -3
  39. package/admin/jsonConfig.json +61 -2
  40. package/build/lib/journal.js +298 -2
  41. package/build/lib/journal.js.map +1 -1
  42. package/build/lib/llm.js +105 -4
  43. package/build/lib/llm.js.map +1 -1
  44. package/build/lib/localLlm.js +47 -10
  45. package/build/lib/localLlm.js.map +1 -1
  46. package/build/lib/nlu.js +124 -5
  47. package/build/lib/nlu.js.map +1 -1
  48. package/build/lib/tools.js +3 -0
  49. package/build/lib/tools.js.map +1 -1
  50. package/build/lib/voice/esphome.js +81 -7
  51. package/build/lib/voice/esphome.js.map +1 -1
  52. package/build/lib/voice/esphomeProto.js.map +1 -1
  53. package/build/lib/voice/mediaServer.js +29 -1
  54. package/build/lib/voice/mediaServer.js.map +1 -1
  55. package/build/lib/voice/protocol.js.map +1 -1
  56. package/build/lib/voice/voiceServer.js +81 -7
  57. package/build/lib/voice/voiceServer.js.map +1 -1
  58. package/build/lib/voice/wakeWords.js +284 -0
  59. package/build/lib/voice/wakeWords.js.map +1 -0
  60. package/build/lib/voice/wyoming.js +12 -4
  61. package/build/lib/voice/wyoming.js.map +1 -1
  62. package/build/lib/weather.js +160 -0
  63. package/build/lib/weather.js.map +1 -1
  64. package/build/lib/youtube.js +625 -0
  65. package/build/lib/youtube.js.map +1 -0
  66. package/build/main.js +1152 -76
  67. package/build/main.js.map +1 -1
  68. package/io-package.json +30 -14
  69. package/package.json +6 -6
  70. package/admin/custom/assets/Components-C6qcpaRC.js +0 -1
  71. package/admin/custom/assets/_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__201375093093590__prebuild___mf_0_iobroker_mf_1_json_mf_2_config__prebuild__-TM4l0OYm.js +0 -122
  72. /package/admin/custom/assets/{_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__201375093093590__loadShare___mf_0_emotion_mf_1_react__loadShare__.js-BkH7fzDw.js → _virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__163539842885036__loadShare___mf_0_emotion_mf_1_react__loadShare__.js-BkH7fzDw.js} +0 -0
  73. /package/admin/custom/assets/{_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__201375093093590__prebuild__react_mf_1_jsx_mf_2_runtime__prebuild__-M61rjtmp.js → _virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__163539842885036__prebuild__react_mf_1_jsx_mf_2_runtime__prebuild__-M61rjtmp.js} +0 -0
@@ -33,7 +33,10 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.Journal = void 0;
36
+ exports.Journal = exports.MAX_TRACES = void 0;
37
+ exports.traceDirFor = traceDirFor;
38
+ exports.snapshot = snapshot;
39
+ exports.errorInfo = errorInfo;
37
40
  exports.transportForSource = transportForSource;
38
41
  /**
39
42
  * Conversation journal: one entry per request the assistant answered — where it came from (chat, script,
@@ -46,6 +49,12 @@ exports.transportForSource = transportForSource;
46
49
  * {@link AsyncLocalStorage}: {@link Journal.run} opens it, and {@link Journal.note}/{@link Journal.tool}/
47
50
  * {@link Journal.stateAccess} write to whatever entry the current call chain belongs to — and do nothing
48
51
  * outside of one (a trigger writing a state is not a conversation).
52
+ *
53
+ * Besides the entry, every request keeps a **trace**: the steps of its processing with their timing and
54
+ * whatever explains them — the NLU's parse, the full prompt and every round of the model, tool arguments
55
+ * and results in full, speech recognition and synthesis. Traces are too big to ride along in `journal.last`
56
+ * and in `journal.json`, so the chat fetches one when a request is opened ({@link Journal.trace}); each is
57
+ * kept in a file of its own and only for the newest {@link MAX_TRACES} requests.
49
58
  */
50
59
  const node_async_hooks_1 = require("node:async_hooks");
51
60
  const node_fs_1 = require("node:fs");
@@ -54,6 +63,68 @@ const path = __importStar(require("node:path"));
54
63
  const MAX_ENTRIES = 3000;
55
64
  const SAVE_DELAY_MS = 5000;
56
65
  const RESULT_CHARS = 300;
66
+ /** Requests whose trace is kept on disk. */
67
+ exports.MAX_TRACES = 200;
68
+ /** Traces held in memory (the newest ones — the ones somebody is about to open). */
69
+ const MEMORY_TRACES = 30;
70
+ /** Steps per request; a tool loop gone wild must not grow one trace without end. */
71
+ const MAX_STEPS = 400;
72
+ /** Longer strings are cut — enough for a system prompt with a few hundred devices. */
73
+ const STEP_STRING_CHARS = 100_000;
74
+ /** A reply is matched to its synthesis only this long after the answer. */
75
+ const SPEECH_WINDOW_MS = 120_000;
76
+ /** Entry ids as {@link Journal.run} makes them — checked before one becomes a file name. */
77
+ const ID_RE = /^[a-z0-9-]+$/;
78
+ /** Directory the traces of a journal file are kept in. */
79
+ function traceDirFor(file) {
80
+ return path.join(path.dirname(file), 'journal-trace');
81
+ }
82
+ /**
83
+ * JSON snapshot of a step's data — taken when it is recorded, because the objects go on changing (the
84
+ * model's message list grows, cache markers move). Long strings are cut and buffers summarised.
85
+ */
86
+ function snapshot(value) {
87
+ if (value === undefined) {
88
+ return undefined;
89
+ }
90
+ try {
91
+ const text = JSON.stringify(value, (_key, v) => {
92
+ if (typeof v === 'string' && v.length > STEP_STRING_CHARS) {
93
+ return `${v.slice(0, STEP_STRING_CHARS)}… [+${v.length - STEP_STRING_CHARS} chars]`;
94
+ }
95
+ if (typeof v === 'bigint') {
96
+ return v.toString();
97
+ }
98
+ // Buffer.toJSON() has already run when the replacer sees it.
99
+ const b = v;
100
+ if (b && typeof b === 'object' && b.type === 'Buffer' && Array.isArray(b.data)) {
101
+ return `<${b.data.length} bytes>`;
102
+ }
103
+ return v;
104
+ });
105
+ return text === undefined ? undefined : JSON.parse(text);
106
+ }
107
+ catch (e) {
108
+ return `<not serializable: ${e.message}>`;
109
+ }
110
+ }
111
+ /** Everything an error carries that helps — an API error's status and body, the stack. */
112
+ function errorInfo(e) {
113
+ if (!(e instanceof Error)) {
114
+ return { message: String(e) };
115
+ }
116
+ const info = { name: e.name, message: e.message };
117
+ for (const key of ['status', 'code', 'type', 'param', 'error', 'request_id', 'requestID']) {
118
+ const v = e[key];
119
+ if (v !== undefined && v !== null) {
120
+ info[key] = v;
121
+ }
122
+ }
123
+ if (e.stack) {
124
+ info.stack = e.stack;
125
+ }
126
+ return info;
127
+ }
57
128
  /** Derive the transport from a request source when the caller did not say. */
58
129
  function transportForSource(source) {
59
130
  if (source === 'chat') {
@@ -76,8 +147,16 @@ class Journal {
76
147
  store = new node_async_hooks_1.AsyncLocalStorage();
77
148
  saveTimer = null;
78
149
  counter = 0;
150
+ /** Steps of the newest requests; older ones are read back from their file. */
151
+ traces = new Map();
152
+ /** Traces changed since they were last written. */
153
+ dirty = new Set();
154
+ /** LLM tool calls in progress per request: a state access inside one belongs to that tool's step. */
155
+ toolDepth = new WeakMap();
156
+ traceDir;
79
157
  constructor(opts) {
80
158
  this.opts = opts;
159
+ this.traceDir = opts.file ? traceDirFor(opts.file) : '';
81
160
  }
82
161
  get enabled() {
83
162
  return this.opts.hours > 0;
@@ -110,8 +189,34 @@ class Journal {
110
189
  }
111
190
  clear() {
112
191
  this.entries = [];
192
+ this.traces.clear();
193
+ this.dirty.clear();
113
194
  this.scheduleSave();
114
195
  }
196
+ /** The entry of a request (the chat asks for it together with its trace). */
197
+ get(id) {
198
+ return this.entries.find(e => e.id === id);
199
+ }
200
+ /** The processing steps of a request, or null when none were kept (too old, or from an older version). */
201
+ async trace(id) {
202
+ if (!ID_RE.test(id) || !this.get(id)) {
203
+ return null;
204
+ }
205
+ const steps = this.traces.get(id);
206
+ if (steps) {
207
+ return steps;
208
+ }
209
+ if (!this.traceDir) {
210
+ return null;
211
+ }
212
+ try {
213
+ const raw = JSON.parse(await node_fs_1.promises.readFile(path.join(this.traceDir, `${id}.json`), 'utf8'));
214
+ return Array.isArray(raw?.steps) ? raw.steps : null;
215
+ }
216
+ catch {
217
+ return null;
218
+ }
219
+ }
115
220
  /**
116
221
  * Run `fn` as one journalled request. The entry is published once when the request starts (so the
117
222
  * chat shows the question while the answer is still being worked out) and once when it is finished.
@@ -131,7 +236,7 @@ class Journal {
131
236
  read: [],
132
237
  written: [],
133
238
  };
134
- for (const key of ['satellite', 'room', 'wakeWord', 'sttMs']) {
239
+ for (const key of ['satellite', 'room', 'wakeWord', 'sttMs', 'audioMs']) {
135
240
  if (origin[key] !== undefined && origin[key] !== '') {
136
241
  entry[key] = origin[key];
137
242
  }
@@ -141,6 +246,12 @@ class Journal {
141
246
  entry.wakeWord = '';
142
247
  }
143
248
  this.entries.push(entry);
249
+ this.traces.set(entry.id, []);
250
+ this.addStep(entry, {
251
+ stage: 'request',
252
+ title: `Request from ${source || entry.transport} (${entry.transport})`,
253
+ data: { question, source, origin },
254
+ });
144
255
  this.publish(entry);
145
256
  try {
146
257
  const answer = await this.store.run(entry, fn);
@@ -149,16 +260,162 @@ class Journal {
149
260
  }
150
261
  catch (e) {
151
262
  entry.error = e.message;
263
+ this.addStep(entry, { stage: 'error', level: 'error', title: entry.error, data: errorInfo(e) });
152
264
  throw e;
153
265
  }
154
266
  finally {
155
267
  entry.done = true;
156
268
  entry.ms = Date.now() - entry.ts;
269
+ this.dirty.add(entry.id);
157
270
  this.publish(entry);
158
271
  this.prune();
159
272
  this.scheduleSave();
160
273
  }
161
274
  }
275
+ /** Add a step to the current request's trace (nothing outside of one). */
276
+ step(input) {
277
+ const entry = this.store.getStore();
278
+ if (entry) {
279
+ this.addStep(entry, input);
280
+ }
281
+ }
282
+ /**
283
+ * Run an LLM tool as part of the current request: the call lands in the entry's tool list and, with the
284
+ * full arguments and result, in the trace.
285
+ */
286
+ async toolCall(name, args, fn) {
287
+ const entry = this.store.getStore();
288
+ if (!entry) {
289
+ return fn();
290
+ }
291
+ this.toolDepth.set(entry, (this.toolDepth.get(entry) || 0) + 1);
292
+ const start = Date.now();
293
+ try {
294
+ const result = await fn();
295
+ const ms = Date.now() - start;
296
+ this.addStep(entry, { stage: 'tool', title: `Tool ${name}`, ms, data: { args, result } });
297
+ this.tool(name, args, ms, result);
298
+ return result;
299
+ }
300
+ catch (e) {
301
+ const ms = Date.now() - start;
302
+ const message = e.message;
303
+ this.addStep(entry, {
304
+ stage: 'tool',
305
+ level: 'error',
306
+ title: `Tool ${name} failed: ${message}`,
307
+ ms,
308
+ data: { args, error: errorInfo(e) },
309
+ });
310
+ this.tool(name, args, ms, undefined, message);
311
+ throw e;
312
+ }
313
+ finally {
314
+ this.toolDepth.set(entry, (this.toolDepth.get(entry) || 1) - 1);
315
+ }
316
+ }
317
+ /**
318
+ * Run a state call (MCP `get_states`/`set_state`/…) as part of the current request: the ids land in the
319
+ * entry's read/written lists, and — unless an LLM tool made the call, whose step already shows it — the
320
+ * call with its result in the trace.
321
+ */
322
+ async stateCall(name, args, fn) {
323
+ const entry = this.store.getStore();
324
+ if (!entry) {
325
+ return fn();
326
+ }
327
+ this.stateAccess(name, args);
328
+ if (this.toolDepth.get(entry)) {
329
+ return fn();
330
+ }
331
+ const start = Date.now();
332
+ try {
333
+ const result = await fn();
334
+ this.addStep(entry, { stage: 'state', title: name, ms: Date.now() - start, data: { args, result } });
335
+ return result;
336
+ }
337
+ catch (e) {
338
+ this.addStep(entry, {
339
+ stage: 'state',
340
+ level: 'error',
341
+ title: `${name} failed: ${e.message}`,
342
+ ms: Date.now() - start,
343
+ data: { args, error: errorInfo(e) },
344
+ });
345
+ throw e;
346
+ }
347
+ }
348
+ /**
349
+ * A transport synthesised the reply to the latest request of `source`. That happens after the answer,
350
+ * outside of the request, so the entry is found by its source.
351
+ */
352
+ spoken(source, speech) {
353
+ const now = Date.now();
354
+ for (let i = this.entries.length - 1; i >= 0; i--) {
355
+ const entry = this.entries[i];
356
+ if (entry.source !== source) {
357
+ continue;
358
+ }
359
+ if (!entry.done || entry.ttsMs !== undefined || now - entry.ts - (entry.ms || 0) > SPEECH_WINDOW_MS) {
360
+ return;
361
+ }
362
+ entry.ttsMs = speech.ms;
363
+ entry.replyAudioMs = Math.round(speech.audioMs);
364
+ const voice = speech.voice ? ` (${speech.voice})` : '';
365
+ this.addStep(entry, {
366
+ stage: 'tts',
367
+ title: `Speech synthesis: ${speech.provider}${voice}, ${(speech.audioMs / 1000).toFixed(1)} s of audio`,
368
+ ms: speech.ms,
369
+ data: speech,
370
+ });
371
+ this.dirty.add(entry.id);
372
+ this.publish(entry);
373
+ this.scheduleSave();
374
+ return;
375
+ }
376
+ }
377
+ addStep(entry, input) {
378
+ const steps = this.traces.get(entry.id);
379
+ if (!steps || steps.length > MAX_STEPS) {
380
+ return;
381
+ }
382
+ if (steps.length === MAX_STEPS) {
383
+ steps.push({
384
+ t: Date.now() - entry.ts,
385
+ stage: 'error',
386
+ level: 'warn',
387
+ title: `More than ${MAX_STEPS} steps — the rest is not recorded`,
388
+ });
389
+ return;
390
+ }
391
+ const step = {
392
+ t: Date.now() - entry.ts - (input.ms || 0),
393
+ stage: input.stage,
394
+ title: input.title,
395
+ };
396
+ if (input.ms !== undefined) {
397
+ step.ms = input.ms;
398
+ }
399
+ if (input.level) {
400
+ step.level = input.level;
401
+ }
402
+ if (input.tokens) {
403
+ step.tokens = input.tokens;
404
+ const sum = (entry.tokens ||= { input: 0, output: 0 });
405
+ sum.input += input.tokens.input || 0;
406
+ sum.output += input.tokens.output || 0;
407
+ if (input.tokens.cacheRead) {
408
+ sum.cacheRead = (sum.cacheRead || 0) + input.tokens.cacheRead;
409
+ }
410
+ if (input.tokens.cacheWrite) {
411
+ sum.cacheWrite = (sum.cacheWrite || 0) + input.tokens.cacheWrite;
412
+ }
413
+ }
414
+ if (input.data !== undefined) {
415
+ step.data = snapshot(input.data);
416
+ }
417
+ steps.push(step);
418
+ }
162
419
  /** Fill in fields of the current request's entry (tier, detail, followUp …). */
163
420
  note(fields) {
164
421
  const entry = this.store.getStore();
@@ -243,8 +500,23 @@ class Journal {
243
500
  }
244
501
  start = Math.max(start, this.entries.length - MAX_ENTRIES);
245
502
  if (start > 0) {
503
+ for (const e of this.entries.slice(0, start)) {
504
+ this.traces.delete(e.id);
505
+ this.dirty.delete(e.id);
506
+ }
246
507
  this.entries = this.entries.slice(start);
247
508
  }
509
+ // Without a file the memory is all there is: keep as many as would be on disk.
510
+ const keep = this.traceDir ? MEMORY_TRACES : exports.MAX_TRACES;
511
+ for (const id of this.traces.keys()) {
512
+ if (this.traces.size <= keep) {
513
+ break;
514
+ }
515
+ // Not written yet, or still being answered: needed until it is on disk.
516
+ if ((!this.traceDir || !this.dirty.has(id)) && this.get(id)?.done !== false) {
517
+ this.traces.delete(id);
518
+ }
519
+ }
248
520
  }
249
521
  scheduleSave() {
250
522
  if (!this.opts.file || this.saveTimer) {
@@ -267,6 +539,30 @@ class Journal {
267
539
  catch (e) {
268
540
  this.opts.log.warn(`Journal could not be saved: ${e.message}`);
269
541
  }
542
+ await this.saveTraces();
543
+ this.prune();
544
+ }
545
+ /** Write the traces that changed, and delete the files of requests that are no longer kept. */
546
+ async saveTraces() {
547
+ try {
548
+ await node_fs_1.promises.mkdir(this.traceDir, { recursive: true });
549
+ for (const id of [...this.dirty]) {
550
+ const steps = this.traces.get(id);
551
+ this.dirty.delete(id);
552
+ if (steps) {
553
+ await node_fs_1.promises.writeFile(path.join(this.traceDir, `${id}.json`), JSON.stringify({ id, steps }));
554
+ }
555
+ }
556
+ const keep = new Set(this.entries.slice(-exports.MAX_TRACES).map(e => e.id));
557
+ for (const name of await node_fs_1.promises.readdir(this.traceDir)) {
558
+ if (name.endsWith('.json') && !keep.has(name.slice(0, -5))) {
559
+ await node_fs_1.promises.rm(path.join(this.traceDir, name), { force: true });
560
+ }
561
+ }
562
+ }
563
+ catch (e) {
564
+ this.opts.log.warn(`Journal traces could not be saved: ${e.message}`);
565
+ }
270
566
  }
271
567
  }
272
568
  exports.Journal = Journal;
@@ -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 */\nimport { AsyncLocalStorage } from 'node:async_hooks';\nimport { promises as fs } from 'node:fs';\nimport * as path from 'node:path';\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\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 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}\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}\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\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\n constructor(private readonly opts: JournalOptions) {}\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.scheduleSave();\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'] 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.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 throw e;\n } finally {\n entry.done = true;\n entry.ms = Date.now() - entry.ts;\n this.publish(entry);\n this.prune();\n this.scheduleSave();\n }\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 this.entries = this.entries.slice(start);\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 }\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';\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?: { peak: number; noiseFloor: number; startLevel: number; endLevel: number };\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"]}