blun-king-cli 9.1.403 → 9.1.405

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
  ---------------------------
@@ -391,6 +395,15 @@ Anfrage höchstens 1.000 Zeichen. Eingebettete Inhalte aus
391
395
  große Anhänge weder die Auswahl unnötig verteuern noch anhand ihres Inhalts ein
392
396
  fachfremdes Werkzeug vorladen.
393
397
 
398
+ Ruhige Konfigurationsaktualisierung
399
+ -----------------------------------
400
+
401
+ Ab BLUN King 9.1.404 ersetzt eine Konfigurationsaktualisierung die private
402
+ config.toml nur noch, wenn sich ihre serialisierten Bytes tatsächlich ändern.
403
+ Bei identischem Inhalt bleiben Dateidentität und Änderungszeit erhalten; die
404
+ Prüfung und Härtung der privaten Dateirechte läuft trotzdem weiter. Echte
405
+ Änderungen verwenden unverändert den crash-sicheren atomaren Schreibweg.
406
+
394
407
  Der eigentliche Anhang und die vollständige Nutzernachricht bleiben für den Zug
395
408
  unverändert verfügbar. Die Begrenzung betrifft ausschließlich die kleine
396
409
  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
@@ -400,6 +404,14 @@ Anfrage höchstens 1.000 Zeichen. Eingebettete Inhalte aus
400
404
  große Anhänge weder die Auswahl unnötig verteuern noch anhand ihres Inhalts ein
401
405
  fachfremdes Werkzeug vorladen.
402
406
 
407
+ ## Ruhige Konfigurationsaktualisierung
408
+
409
+ Ab BLUN King 9.1.404 ersetzt eine Konfigurationsaktualisierung die private
410
+ `config.toml` nur noch, wenn sich ihre serialisierten Bytes tatsächlich ändern.
411
+ Bei identischem Inhalt bleiben Dateidentität und Änderungszeit erhalten; die
412
+ Prüfung und Härtung der privaten Dateirechte läuft trotzdem weiter. Echte
413
+ Änderungen verwenden unverändert den crash-sicheren atomaren Schreibweg.
414
+
403
415
  Der eigentliche Anhang und die vollständige Nutzernachricht bleiben für den Zug
404
416
  unverändert verfügbar. Die Begrenzung betrifft ausschließlich die kleine
405
417
  lexikalische Vorauswahl von höchstens zwei Werkzeugschemas; ToolSearch und alle
@@ -0,0 +1,27 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs/promises');
4
+
5
+ async function configFileMatches(filePath, nextContent, fsImpl = fs) {
6
+ const content = Buffer.isBuffer(nextContent)
7
+ ? nextContent
8
+ : Buffer.from(String(nextContent), 'utf8');
9
+
10
+ try {
11
+ const stat = await fsImpl.lstat(filePath);
12
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size !== content.byteLength) {
13
+ return false;
14
+ }
15
+
16
+ const current = await fsImpl.readFile(filePath);
17
+ return Buffer.isBuffer(current)
18
+ ? current.equals(content)
19
+ : Buffer.from(current).equals(content);
20
+ } catch {
21
+ return false;
22
+ }
23
+ }
24
+
25
+ module.exports = {
26
+ configFileMatches,
27
+ };
@@ -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";
@@ -16051,6 +16053,7 @@ function transformLoopControlData(data) {
16051
16053
  delete out["maxStepsPerRun"];
16052
16054
  return out;
16053
16055
  }
16056
+ const { configFileMatches } = createRequire(import.meta.url)("./bin/config-write-dedup-policy.cjs");
16054
16057
  async function writeConfigFile(filePath, config) {
16055
16058
  const validated = validateConfig(stripEnvModelConfig(config));
16056
16059
  const configDirectory = dirname$3(filePath);
@@ -16058,7 +16061,12 @@ async function writeConfigFile(filePath, config) {
16058
16061
  recursive: true,
16059
16062
  mode: 448
16060
16063
  }) !== void 0) await hardenPrivatePath(configDirectory, "directory");
16061
- await atomicWritePrivate$1(filePath, `${stringify$1(configToTomlData(validated))}\n`);
16064
+ const serialized = `${stringify$1(configToTomlData(validated))}\n`;
16065
+ if (await configFileMatches(filePath, serialized)) {
16066
+ await hardenPrivatePath(filePath, "file");
16067
+ return;
16068
+ }
16069
+ await atomicWritePrivate$1(filePath, serialized);
16062
16070
  }
16063
16071
  function configToTomlData(config) {
16064
16072
  const out = cloneRecord$2(config.raw);
@@ -511023,6 +511031,8 @@ var StreamingUIController = class {
511023
511031
  pendingToolCallFlushIds = /* @__PURE__ */ new Set();
511024
511032
  _currentTurnId = void 0;
511025
511033
  _currentStep = 0;
511034
+ _remoteTodos = [];
511035
+ _remoteProcessStartedAt = new Date(Date.now() - process.uptime() * 1e3).toISOString();
511026
511036
  _liveTurnStartedAtMs = void 0;
511027
511037
  _liveOutputTokens = new LiveOutputTokenCounter();
511028
511038
  _countedToolCallIds = /* @__PURE__ */ new Set();
@@ -511048,9 +511058,25 @@ var StreamingUIController = class {
511048
511058
  }
511049
511059
  setTurnId(turnId) {
511050
511060
  this._currentTurnId = turnId;
511061
+ this.publishTelegramConsoleStatus();
511051
511062
  }
511052
511063
  setStep(step) {
511053
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 {}
511054
511080
  }
511055
511081
  hasActiveTurn() {
511056
511082
  return this._currentTurnId !== void 0;
@@ -511358,8 +511384,10 @@ var StreamingUIController = class {
511358
511384
  this._pendingReadGroup = null;
511359
511385
  this._currentTurnId = void 0;
511360
511386
  this._currentStep = 0;
511387
+ this._remoteTodos = [];
511361
511388
  this._streamingToolCallArguments.clear();
511362
511389
  this.pendingToolCallFlushIds.clear();
511390
+ this.publishTelegramConsoleStatus();
511363
511391
  this.host.state.ui.requestRender();
511364
511392
  }
511365
511393
  disposeActiveThinkingComponent() {
@@ -511621,9 +511649,11 @@ var StreamingUIController = class {
511621
511649
  }
511622
511650
  setTodoList(todos) {
511623
511651
  const { state } = this.host;
511652
+ this._remoteTodos = todos;
511624
511653
  state.todoPanel.setTodos(todos);
511625
511654
  state.todoPanelContainer.clear();
511626
511655
  if (!state.todoPanel.isEmpty()) state.todoPanelContainer.addChild(state.todoPanel);
511656
+ this.publishTelegramConsoleStatus();
511627
511657
  state.ui.requestRender();
511628
511658
  }
511629
511659
  beginCompaction(instruction) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blun-king-cli",
3
- "version": "9.1.403",
3
+ "version": "9.1.405",
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({