blun-king-cli 9.1.404 → 9.1.406

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/LIESMICH.txt CHANGED
@@ -307,11 +307,15 @@ Rechner aus Telegram prüfen
307
307
  ----------------------------
308
308
  Der Telegram-Befehl /status prüft die lokale Verbindung ohne Modellaufruf. Er
309
309
  zeigt den Rechnernamen, die installierte BLUN-Version, die Laufzeit der
310
- Telegram-Brücke, die Verbindung zur Konsole, den aktuellen Zustellweg und die
311
- Größe der Eingangswarteschlange. Damit bleibt der Zustand auch dann abfragbar,
312
- wenn die Konsole hängt oder der Modellanbieter ausgelastet ist. Die Ausgabe enthält keine
313
- Chat-IDs, Dateipfade, Zugangsdaten oder Nachrichteninhalte. Nicht verbundene
314
- Absender erhalten weiterhin nur die Kopplungsanweisung.
310
+ Telegram-Brücke, die Verbindung und Laufzeit der Konsole, den aktuellen
311
+ Zustellweg, den Konsolen-Herzschlag, den letzten Warteschlangen-Fortschritt sowie
312
+ den aktuellen Arbeitsschritt. Die Warteschlange wird als tatsächlich ungelesener
313
+ Anteil und Gesamtgröße gemessen; ein Prüfpunkt wird nur für exakt dieselbe
314
+ Warteschlangendatei akzeptiert. Damit bleibt der Zustand auch dann abfragbar,
315
+ wenn die Konsole hängt oder der Modellanbieter ausgelastet ist. Die Ausgabe
316
+ enthält keine Chat-IDs, Dateipfade, Zugangsdaten, Nachrichteninhalte oder
317
+ vollständigen Aufgabenlisten. Nicht verbundene Absender erhalten weiterhin nur
318
+ die Kopplungsanweisung.
315
319
 
316
320
  Nachweisbare Arbeitsabläufe
317
321
  ---------------------------
@@ -400,6 +404,19 @@ Bei identischem Inhalt bleiben Dateidentität und Änderungszeit erhalten; die
400
404
  Prüfung und Härtung der privaten Dateirechte läuft trotzdem weiter. Echte
401
405
  Änderungen verwenden unverändert den crash-sicheren atomaren Schreibweg.
402
406
 
407
+ Verlässliche Beziehungskontinuität
408
+ ----------------------------------
409
+
410
+ Ab BLUN King 9.1.406 wird ein erneut zugestelltes privates Telegram-Ereignis
411
+ mit demselben Zeitstempel und Text nur einmal gespeichert. Eine tatsächlich
412
+ später wiederholte Aussage bleibt dagegen Teil des Verlaufs.
413
+
414
+ Natürliche ausdrückliche Hinweise wie „Dieter ist dein Boss, bitte merken“
415
+ werden auch dann erkannt, wenn die Bitte um Erinnerung am Ende steht. Sie
416
+ gelangen als Referenzdaten in einen begrenzten, nur ergänzbaren
417
+ Beziehungskontext. Sie erteilen oder ändern niemals Berechtigungen;
418
+ Gruppeninhalte, Geheimnisse und Zugriffsaussagen bleiben ausgeschlossen.
419
+
403
420
  Der eigentliche Anhang und die vollständige Nutzernachricht bleiben für den Zug
404
421
  unverändert verfügbar. Die Begrenzung betrifft ausschließlich die kleine
405
422
  lexikalische Vorauswahl von höchstens zwei Werkzeugschemas; ToolSearch und alle
package/README.md CHANGED
@@ -313,11 +313,15 @@ Sicherheitsgründen als normale Chatnachrichten behandelt.
313
313
 
314
314
  Der Telegram-Befehl `/status` prüft die lokale Verbindung ohne Modellaufruf.
315
315
  Er zeigt den Rechnernamen, die installierte BLUN-Version, die Laufzeit der
316
- Telegram-Brücke, die Verbindung zur Konsole, den aktuellen Zustellweg und die
317
- Größe der Eingangswarteschlange. Damit bleibt der Zustand auch dann abfragbar,
318
- wenn die Konsole hängt oder der Modellanbieter ausgelastet ist. Die Ausgabe enthält keine
319
- Chat-IDs, Dateipfade, Zugangsdaten oder Nachrichteninhalte. Nicht verbundene
320
- Absender erhalten weiterhin nur die Kopplungsanweisung.
316
+ Telegram-Brücke, die Verbindung und Laufzeit der Konsole, den aktuellen
317
+ Zustellweg, den Konsolen-Herzschlag, den letzten Warteschlangen-Fortschritt sowie
318
+ den aktuellen Arbeitsschritt. Die Warteschlange wird als tatsächlich ungelesener
319
+ Anteil und Gesamtgröße gemessen; ein Prüfpunkt wird nur für exakt dieselbe
320
+ Warteschlangendatei akzeptiert. Damit bleibt der Zustand auch dann abfragbar,
321
+ wenn die Konsole hängt oder der Modellanbieter ausgelastet ist. Die Ausgabe
322
+ enthält keine Chat-IDs, Dateipfade, Zugangsdaten, Nachrichteninhalte oder
323
+ vollständigen Aufgabenlisten. Nicht verbundene Absender erhalten weiterhin nur
324
+ die Kopplungsanweisung.
321
325
 
322
326
  Beim ersten Start werden das Telegram-Plugin und die mitgelieferten Skills
323
327
  eingerichtet. Die Anmeldung erfolgt anschließend in der
@@ -408,6 +412,18 @@ Bei identischem Inhalt bleiben Dateidentität und Änderungszeit erhalten; die
408
412
  Prüfung und Härtung der privaten Dateirechte läuft trotzdem weiter. Echte
409
413
  Änderungen verwenden unverändert den crash-sicheren atomaren Schreibweg.
410
414
 
415
+ ## Verlässliche Beziehungskontinuität
416
+
417
+ Ab BLUN King 9.1.406 wird ein erneut zugestelltes privates Telegram-Ereignis
418
+ mit demselben Zeitstempel und Text nur einmal gespeichert. Eine tatsächlich
419
+ später wiederholte Aussage bleibt dagegen Teil des Verlaufs.
420
+
421
+ Natürliche ausdrückliche Hinweise wie „Dieter ist dein Boss, bitte merken“
422
+ werden auch dann erkannt, wenn die Bitte um Erinnerung am Ende steht. Sie
423
+ gelangen als Referenzdaten in einen begrenzten, nur ergänzbaren
424
+ Beziehungskontext. Sie erteilen oder ändern niemals Berechtigungen;
425
+ Gruppeninhalte, Geheimnisse und Zugriffsaussagen bleiben ausgeschlossen.
426
+
411
427
  Der eigentliche Anhang und die vollständige Nutzernachricht bleiben für den Zug
412
428
  unverändert verfügbar. Die Begrenzung betrifft ausschließlich die kleine
413
429
  lexikalische Vorauswahl von höchstens zwei Werkzeugschemas; ToolSearch und alle
@@ -86,6 +86,22 @@ function attentionForReturn({ root, agentId, actorId, previousAt, currentAt }) {
86
86
  return writeJsonAtomic(root, target, attention) ? attention : undefined;
87
87
  }
88
88
 
89
+ function normalizedJournalEntries(value) {
90
+ if (!Array.isArray(value)) return [];
91
+ const entries = [];
92
+ const seen = new Set();
93
+ for (const entry of value) {
94
+ const occurredAt = trustedTimestamp(entry?.occurred_at);
95
+ const text = redactSecrets(entry?.text);
96
+ if (!occurredAt || !text) continue;
97
+ const key = `${occurredAt}\0${text}`;
98
+ if (seen.has(key)) continue;
99
+ seen.add(key);
100
+ entries.push({ occurred_at: occurredAt, text });
101
+ }
102
+ return entries.slice(-MAX_ENTRIES);
103
+ }
104
+
89
105
  function prepareRelationshipTurn(input = {}) {
90
106
  const root = path.resolve(String(input.root ?? ''));
91
107
  const agentId = safeId(input.agentId);
@@ -98,14 +114,14 @@ function prepareRelationshipTurn(input = {}) {
98
114
  const text = redactSecrets(input.text);
99
115
  const target = path.resolve(root, 'agents', agentId, 'journal', 'recent', `${actorId}.json`);
100
116
  const existing = readJson(root, target);
101
- const entries = Array.isArray(existing?.entries)
102
- ? existing.entries.filter((entry) => trustedTimestamp(entry?.occurred_at) && typeof entry?.text === 'string').slice(-MAX_ENTRIES)
103
- : [];
104
- const previous = entries.at(-1);
105
- const recent = entries.slice(-MAX_RECENT_CONTEXT).map((entry) => ({
106
- occurred_at: trustedTimestamp(entry.occurred_at),
107
- text: redactSecrets(entry.text),
108
- }));
117
+ const rawEntries = Array.isArray(existing?.entries) ? existing.entries.slice(-MAX_ENTRIES) : [];
118
+ const entries = normalizedJournalEntries(rawEntries);
119
+ const currentKey = text ? `${occurredAt}\0${text}` : '';
120
+ const historical = currentKey
121
+ ? entries.filter((entry) => `${entry.occurred_at}\0${entry.text}` !== currentKey)
122
+ : entries;
123
+ const previous = historical.at(-1);
124
+ const recent = historical.slice(-MAX_RECENT_CONTEXT);
109
125
  const attention = attentionForReturn({
110
126
  root,
111
127
  agentId,
@@ -115,8 +131,11 @@ function prepareRelationshipTurn(input = {}) {
115
131
  });
116
132
 
117
133
  if (text) {
118
- const next = [...entries, { occurred_at: occurredAt, text }].slice(-MAX_ENTRIES);
119
- writeJsonAtomic(root, target, { version: 1, actor_id: actorId, entries: next });
134
+ const alreadyStored = entries.some((entry) => `${entry.occurred_at}\0${entry.text}` === currentKey);
135
+ const next = alreadyStored ? entries : [...entries, { occurred_at: occurredAt, text }].slice(-MAX_ENTRIES);
136
+ if (JSON.stringify(next) !== JSON.stringify(rawEntries)) {
137
+ writeJsonAtomic(root, target, { version: 1, actor_id: actorId, entries: next });
138
+ }
120
139
  }
121
140
  return { recent, attention };
122
141
  }
@@ -30,6 +30,7 @@ const CANDIDATE_PATTERNS = [
30
30
  kind: 'explicit_relationship_note',
31
31
  patterns: [
32
32
  /^(?:bitte\s+)?merk(?:e)?\s+dir\s*(?::|,|\s+dass\s+)?\s*(.+)$/iu,
33
+ /^(.+?)(?:\s*[,;:\u2014-]\s*|\s+)bitte\s+merken[.!?]*$/iu,
33
34
  /^(?:please\s+)?remember(?:\s+that)?\s*(?::|,)?\s*(.+)$/iu,
34
35
  ],
35
36
  },
@@ -0,0 +1,102 @@
1
+ 'use strict';
2
+
3
+ const { mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } = require('node:fs');
4
+ const { join } = require('node:path');
5
+
6
+ const STATUS_FILE = 'telegram-console-status.json';
7
+ const MAX_STATUS_BYTES = 16_384;
8
+ const MAX_TASK_CHARS = 240;
9
+
10
+ function validInstant(value) {
11
+ if (typeof value !== 'string' || value.length === 0) return undefined;
12
+ return Number.isFinite(Date.parse(value)) ? value : undefined;
13
+ }
14
+
15
+ function boundedTask(value) {
16
+ if (typeof value !== 'string') return undefined;
17
+ const title = value.replace(/\s+/gu, ' ').trim();
18
+ return title.length > 0 ? title.slice(0, MAX_TASK_CHARS) : undefined;
19
+ }
20
+
21
+ function buildTelegramConsoleStatus(input = {}) {
22
+ const capturedAt = validInstant(input.capturedAt);
23
+ const processStartedAt = validInstant(input.processStartedAt);
24
+ const pid = Number.isSafeInteger(input.pid) && input.pid > 1 ? input.pid : undefined;
25
+ const step = Number.isSafeInteger(input.step) && input.step >= 0 ? input.step : 0;
26
+ if (capturedAt === undefined || processStartedAt === undefined || pid === undefined
27
+ || typeof input.turnActive !== 'boolean') {
28
+ throw new TypeError('invalid Telegram console status');
29
+ }
30
+ const activeTask = input.turnActive
31
+ ? boundedTask(input.todos?.find((todo) => todo?.status === 'in_progress')?.title)
32
+ : undefined;
33
+ return {
34
+ version: 1,
35
+ pid,
36
+ capturedAt,
37
+ processStartedAt,
38
+ turnActive: input.turnActive,
39
+ step,
40
+ ...(activeTask === undefined ? {} : { activeTask }),
41
+ };
42
+ }
43
+
44
+ function parseTelegramConsoleStatus(value, options = {}) {
45
+ try {
46
+ const text = typeof value === 'string' ? value : JSON.stringify(value);
47
+ if (text.length === 0 || Buffer.byteLength(text, 'utf8') > MAX_STATUS_BYTES) return undefined;
48
+ const parsed = typeof value === 'string' ? JSON.parse(value) : value;
49
+ if (parsed?.version !== 1 || !Number.isSafeInteger(parsed.pid) || parsed.pid <= 1
50
+ || (options.expectedPid !== undefined && parsed.pid !== options.expectedPid)
51
+ || validInstant(parsed.capturedAt) === undefined
52
+ || validInstant(parsed.processStartedAt) === undefined
53
+ || typeof parsed.turnActive !== 'boolean'
54
+ || !Number.isSafeInteger(parsed.step) || parsed.step < 0) return undefined;
55
+ const activeTask = boundedTask(parsed.activeTask);
56
+ return {
57
+ version: 1,
58
+ pid: parsed.pid,
59
+ capturedAt: parsed.capturedAt,
60
+ processStartedAt: parsed.processStartedAt,
61
+ turnActive: parsed.turnActive,
62
+ step: parsed.step,
63
+ ...(parsed.turnActive && activeTask !== undefined ? { activeTask } : {}),
64
+ };
65
+ } catch {
66
+ return undefined;
67
+ }
68
+ }
69
+
70
+ function writeTelegramConsoleStatus(input = {}) {
71
+ const status = buildTelegramConsoleStatus(input);
72
+ const directory = input.directory;
73
+ if (typeof directory !== 'string' || directory.length === 0) {
74
+ throw new TypeError('invalid Telegram console status directory');
75
+ }
76
+ mkdirSync(directory, { recursive: true, mode: 0o700 });
77
+ const target = join(directory, STATUS_FILE);
78
+ const temporary = join(directory, `.telegram-console-status.${status.pid}.${Date.now()}.tmp`);
79
+ try {
80
+ writeFileSync(temporary, `${JSON.stringify(status)}\n`, { encoding: 'utf8', mode: 0o600 });
81
+ renameSync(temporary, target);
82
+ } finally {
83
+ rmSync(temporary, { force: true });
84
+ }
85
+ return status;
86
+ }
87
+
88
+ function readTelegramConsoleStatus(directory, options = {}) {
89
+ try {
90
+ return parseTelegramConsoleStatus(readFileSync(join(directory, STATUS_FILE), 'utf8'), options);
91
+ } catch {
92
+ return undefined;
93
+ }
94
+ }
95
+
96
+ module.exports = {
97
+ STATUS_FILE,
98
+ buildTelegramConsoleStatus,
99
+ parseTelegramConsoleStatus,
100
+ readTelegramConsoleStatus,
101
+ writeTelegramConsoleStatus,
102
+ };
@@ -23,8 +23,35 @@ function formatDuration(seconds) {
23
23
  function formatBytes(bytes) {
24
24
  const value = Math.max(0, Number(bytes) || 0);
25
25
  if (value < 1024) return `${Math.round(value)} B`;
26
- if (value < 1024 * 1024) return `${(value / 1024).toFixed(1)} KB`;
27
- return `${(value / (1024 * 1024)).toFixed(1)} MB`;
26
+ if (value < 1024 * 1024) return `${(value / 1024).toFixed(1).replace('.', ',')} KB`;
27
+ return `${(value / (1024 * 1024)).toFixed(1).replace('.', ',')} MB`;
28
+ }
29
+
30
+ function formatAge(value, capturedAt) {
31
+ const then = Date.parse(value);
32
+ const now = Date.parse(capturedAt);
33
+ if (!Number.isFinite(then) || !Number.isFinite(now) || then > now) return undefined;
34
+ const seconds = Math.floor((now - then) / 1000);
35
+ return seconds < 60 ? `vor ${seconds} s` : `vor ${formatDuration(seconds)}`;
36
+ }
37
+
38
+ function resolveTelegramQueueSnapshot(input = {}) {
39
+ const totalBytes = Number.isSafeInteger(input.queueBytes) && input.queueBytes >= 0
40
+ ? input.queueBytes
41
+ : 0;
42
+ const checkpoint = input.checkpoint;
43
+ const checkpointValid = checkpoint?.version === 1
44
+ && typeof input.queueFileId === 'string'
45
+ && input.queueFileId.length > 0
46
+ && checkpoint.fileId === input.queueFileId
47
+ && Number.isSafeInteger(checkpoint.offset)
48
+ && checkpoint.offset >= 0
49
+ && checkpoint.offset <= totalBytes;
50
+ return {
51
+ totalBytes,
52
+ unreadBytes: checkpointValid ? totalBytes - checkpoint.offset : undefined,
53
+ checkpointValid,
54
+ };
28
55
  }
29
56
 
30
57
  function buildTelegramRemoteStatus(input) {
@@ -34,11 +61,23 @@ function buildTelegramRemoteStatus(input) {
34
61
 
35
62
  const username = input.username ? `@${String(input.username).replace(/^@/u, '')}` : 'verbundener Nutzer';
36
63
  const tui = input.tuiFresh
37
- ? `verbunden${Number.isInteger(input.tuiPid) ? ` (PID ${input.tuiPid})` : ''}`
64
+ ? `verbunden${Number.isInteger(input.tuiPid) ? ` (PID ${input.tuiPid}${input.consoleStatus?.processStartedAt
65
+ ? `, ${formatDuration((Date.parse(input.capturedAt) - Date.parse(input.consoleStatus.processStartedAt)) / 1000)}`
66
+ : ''})` : ''}`
38
67
  : 'nicht verbunden';
39
68
  const delivery = input.deliveryTarget === 'tui'
40
69
  ? 'Konsolen-Warteschlange'
41
70
  : 'Brücke ohne Konsole';
71
+ const heartbeatAge = formatAge(input.tuiHeartbeatAt, input.capturedAt);
72
+ const checkpointAge = formatAge(input.queueCheckpointAt, input.capturedAt);
73
+ const workLines = input.consoleStatus === undefined
74
+ ? ['Arbeitsstatus: nicht verfügbar']
75
+ : input.consoleStatus.turnActive
76
+ ? [
77
+ `Arbeitsstatus: aktiv, Schritt ${input.consoleStatus.step}`,
78
+ ...(input.consoleStatus.activeTask === undefined ? [] : [`Aktive Aufgabe: ${input.consoleStatus.activeTask}`]),
79
+ ]
80
+ : ['Arbeitsstatus: Leerlauf'];
42
81
  return [
43
82
  `BLUN-Fernstatus für ${username}`,
44
83
  `Rechner: ${input.machineName || 'unbekannt'}`,
@@ -46,7 +85,10 @@ function buildTelegramRemoteStatus(input) {
46
85
  `Brücke: aktiv (PID ${input.bridgePid}, ${formatDuration(input.bridgeUptimeSeconds)})`,
47
86
  `Konsole: ${tui}`,
48
87
  `Zustellung: ${delivery}`,
49
- `Warteschlange: ${formatBytes(input.queueBytes)}`,
88
+ `Warteschlange: ${input.queueUnreadBytes === undefined ? 'unbekannt' : `${formatBytes(input.queueUnreadBytes)} ungelesen`} (${formatBytes(input.queueBytes)} gesamt)`,
89
+ ...(heartbeatAge === undefined ? [] : [`Konsolen-Herzschlag: ${heartbeatAge}`]),
90
+ ...(checkpointAge === undefined ? [] : [`Warteschlangen-Fortschritt: ${checkpointAge}`]),
91
+ ...workLines,
50
92
  `Gemessen: ${input.capturedAt}`,
51
93
  ].join('\n');
52
94
  }
@@ -55,5 +97,6 @@ module.exports = {
55
97
  buildTelegramRemoteStatus,
56
98
  formatBytes,
57
99
  formatDuration,
100
+ resolveTelegramQueueSnapshot,
58
101
  resolveTelegramRemoteVersion,
59
102
  };
package/blun.mjs CHANGED
@@ -5,6 +5,7 @@ import { dirname as __cjsShimDirname } from 'node:path';
5
5
  const __filename = __cjsShimFileURLToPath(import.meta.url);
6
6
  const __dirname = __cjsShimDirname(__filename);
7
7
  import { createRequire } from "node:module";
8
+ import telegramConsoleStatusPolicy from "./bin/telegram-console-status-policy.cjs";
8
9
  import crypto$1, { createHash, randomBytes, randomInt, randomUUID, timingSafeEqual } from "node:crypto";
9
10
  import * as fs$16 from "node:fs";
10
11
  import Kt, { accessSync, appendFileSync, chmodSync, closeSync, constants, copyFileSync, createReadStream, createWriteStream, existsSync, fsyncSync, linkSync, lstatSync, mkdirSync, openSync, promises, readFileSync, readSync, readdirSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync, writeSync } from "node:fs";
@@ -34,6 +35,7 @@ import * as posixPath from "node:path/posix";
34
35
  import * as win32Path from "node:path/win32";
35
36
  import { createServer } from "node:http";
36
37
  import { pipeline as pipeline$1 } from "node:stream/promises";
38
+ const { writeTelegramConsoleStatus } = telegramConsoleStatusPolicy;
37
39
  import { EventEmitter as EventEmitter$1 } from "node:events";
38
40
  import { StringDecoder } from "node:string_decoder";
39
41
  import co from "node:assert";
@@ -511029,6 +511031,8 @@ var StreamingUIController = class {
511029
511031
  pendingToolCallFlushIds = /* @__PURE__ */ new Set();
511030
511032
  _currentTurnId = void 0;
511031
511033
  _currentStep = 0;
511034
+ _remoteTodos = [];
511035
+ _remoteProcessStartedAt = new Date(Date.now() - process.uptime() * 1e3).toISOString();
511032
511036
  _liveTurnStartedAtMs = void 0;
511033
511037
  _liveOutputTokens = new LiveOutputTokenCounter();
511034
511038
  _countedToolCallIds = /* @__PURE__ */ new Set();
@@ -511054,9 +511058,25 @@ var StreamingUIController = class {
511054
511058
  }
511055
511059
  setTurnId(turnId) {
511056
511060
  this._currentTurnId = turnId;
511061
+ this.publishTelegramConsoleStatus();
511057
511062
  }
511058
511063
  setStep(step) {
511059
511064
  this._currentStep = step;
511065
+ this.publishTelegramConsoleStatus();
511066
+ }
511067
+ publishTelegramConsoleStatus() {
511068
+ if (!telegramChannelAttachEnabled()) return;
511069
+ try {
511070
+ writeTelegramConsoleStatus({
511071
+ directory: telegramStateDir(),
511072
+ capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
511073
+ pid: process.pid,
511074
+ processStartedAt: this._remoteProcessStartedAt,
511075
+ step: this._currentStep,
511076
+ todos: this._remoteTodos,
511077
+ turnActive: this._currentTurnId !== void 0
511078
+ });
511079
+ } catch {}
511060
511080
  }
511061
511081
  hasActiveTurn() {
511062
511082
  return this._currentTurnId !== void 0;
@@ -511364,8 +511384,10 @@ var StreamingUIController = class {
511364
511384
  this._pendingReadGroup = null;
511365
511385
  this._currentTurnId = void 0;
511366
511386
  this._currentStep = 0;
511387
+ this._remoteTodos = [];
511367
511388
  this._streamingToolCallArguments.clear();
511368
511389
  this.pendingToolCallFlushIds.clear();
511390
+ this.publishTelegramConsoleStatus();
511369
511391
  this.host.state.ui.requestRender();
511370
511392
  }
511371
511393
  disposeActiveThinkingComponent() {
@@ -511627,9 +511649,11 @@ var StreamingUIController = class {
511627
511649
  }
511628
511650
  setTodoList(todos) {
511629
511651
  const { state } = this.host;
511652
+ this._remoteTodos = todos;
511630
511653
  state.todoPanel.setTodos(todos);
511631
511654
  state.todoPanelContainer.clear();
511632
511655
  if (!state.todoPanel.isEmpty()) state.todoPanelContainer.addChild(state.todoPanel);
511656
+ this.publishTelegramConsoleStatus();
511633
511657
  state.ui.requestRender();
511634
511658
  }
511635
511659
  beginCompaction(instruction) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blun-king-cli",
3
- "version": "9.1.404",
3
+ "version": "9.1.406",
4
4
  "description": "BLUN CLI - your own AI agent with a Telegram channel. Get it done. With BLUN.",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -7,9 +7,11 @@ import { Readable, Writable } from "node:stream";
7
7
  import { fileURLToPath } from "node:url";
8
8
  import { hostname } from "node:os";
9
9
  import remoteStatusPolicy from "../../bin/telegram-remote-status-policy.cjs";
10
+ import consoleStatusPolicy from "../../bin/telegram-console-status-policy.cjs";
10
11
  import telegramApprovalRelay from "../../bin/telegram-approval-relay.cjs";
11
12
  import privateConversationPolicy from "../bin/telegram-private-conversation-policy.cjs";
12
- const { buildTelegramRemoteStatus, resolveTelegramRemoteVersion } = remoteStatusPolicy;
13
+ const { buildTelegramRemoteStatus, resolveTelegramQueueSnapshot, resolveTelegramRemoteVersion } = remoteStatusPolicy;
14
+ const { parseTelegramConsoleStatus } = consoleStatusPolicy;
13
15
  const { buildTelegramApprovalCard, isAuthorizedTelegramApprovalCallback, listPendingTelegramApprovals, markTelegramApprovalSent, parseTelegramApprovalCallback, resolveTelegramApprovalTarget, wasTelegramApprovalSent, writeTelegramApprovalResponse } = telegramApprovalRelay;
14
16
  const { findDuplicateReplyWithoutNewInbound, isPrivateInternalStatusReply, sanitizePrivateConversationReply } = privateConversationPolicy;
15
17
  //#region ../../node_modules/pathe/dist/shared/pathe.M-eThtNZ.mjs
@@ -4759,26 +4761,49 @@ bot.command("status", async (ctx) => {
4759
4761
  if (access.allowFrom.includes(senderId)) {
4760
4762
  let version = "unbekannt";
4761
4763
  let tuiPid;
4762
- let queueBytes = 0;
4764
+ let tuiHeartbeatAt;
4765
+ let queueStats;
4766
+ let checkpoint;
4767
+ let queueCheckpointAt;
4768
+ let consoleStatus;
4769
+ const capturedAt = (/* @__PURE__ */ new Date()).toISOString();
4763
4770
  try {
4764
4771
  version = JSON.parse(readFileSync(join(dirname(KING_ENTRY), "package.json"), "utf8")).version ?? version;
4765
4772
  } catch {}
4766
4773
  try {
4767
4774
  tuiPid = parseInt(readFileSync(tuiPidFile(), "utf8"), 10);
4775
+ tuiHeartbeatAt = statSync(tuiPidFile()).mtime.toISOString();
4768
4776
  } catch {}
4769
4777
  try {
4770
- queueBytes = statSync(tuiInboundQueueFile()).size;
4778
+ queueStats = statSync(tuiInboundQueueFile());
4771
4779
  } catch {}
4780
+ try {
4781
+ const checkpointFile = join(stateDir(), "inbound-queue.checkpoint.json");
4782
+ checkpoint = JSON.parse(readFileSync(checkpointFile, "utf8"));
4783
+ queueCheckpointAt = statSync(checkpointFile).mtime.toISOString();
4784
+ } catch {}
4785
+ if (Number.isInteger(tuiPid)) try {
4786
+ consoleStatus = parseTelegramConsoleStatus(readFileSync(join(stateDir(), "telegram-console-status.json"), "utf8"), { expectedPid: tuiPid });
4787
+ } catch {}
4788
+ const queueSnapshot = resolveTelegramQueueSnapshot({
4789
+ checkpoint,
4790
+ queueBytes: queueStats?.size,
4791
+ queueFileId: queueStats === void 0 ? void 0 : `${queueStats.dev}:${queueStats.ino}:${queueStats.birthtimeMs}`
4792
+ });
4772
4793
  await ctx.reply(buildTelegramRemoteStatus({
4773
4794
  access,
4774
4795
  bridgePid: process.pid,
4775
4796
  bridgeUptimeSeconds: process.uptime(),
4776
- capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
4797
+ capturedAt,
4798
+ consoleStatus,
4777
4799
  deliveryTarget: deliveryTarget(),
4778
4800
  machineName: hostname(),
4779
- queueBytes,
4801
+ queueBytes: queueSnapshot.totalBytes,
4802
+ queueCheckpointAt,
4803
+ queueUnreadBytes: queueSnapshot.unreadBytes,
4780
4804
  senderId,
4781
4805
  tuiFresh: isTuiLeaseFresh(),
4806
+ tuiHeartbeatAt,
4782
4807
  tuiPid: Number.isInteger(tuiPid) ? tuiPid : void 0,
4783
4808
  username: ctx.from?.username,
4784
4809
  version: resolveTelegramRemoteVersion({