blun-king-cli 9.1.576 → 9.1.578

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/CHANGELOG.md CHANGED
@@ -1,3 +1,19 @@
1
+ ## 9.1.578 - 2026-09-08
2
+
3
+ - Bound thinking-only recovery to one additional attempt per step. A second thinking-only timeout stops that step instead of repeatedly issuing the same request. The chosen reasoning effort and existing timeout remain unchanged; this is not a promise of faster first-token response times.
4
+ - Preserve short Telegram group presence answers when reply_to binds them to a recent same-chat input explicitly addressed to the agent, or a supported German/English presence question beginning with the agent's own configured name. Unsolicited presence and silence-only narration remain filtered; ordinary automatic group fallback stays disabled.
5
+ - Include reply_to in outgoing reply and suppression diagnostics. Missing or ambiguous context does not grant a presence exception. This does not add general recipient recognition or prevent turns caused by legacy semantic addressing.
6
+ - Apply the outbound repair to two exact known managed Telegram MCP revisions, including the final plugin configuration merge. The legacy sender receives a narrowly changed compatibility copy. Existing incoming bridges, access settings, profiles and registers are not rewritten; unknown/custom senders and explicit overrides are preserved. A newly started King process is required.
7
+
8
+
9
+ ## 9.1.577 - 2026-09-07
10
+
11
+ - Stop automatically forwarding ordinary assistant turn-end text to Telegram groups, in both the CLI core and the standalone bridge. This prevents the automatic fallback from posting explanations that there is nothing to report or that a message addresses someone else.
12
+ - Group replies must be sent deliberately through the Telegram reply tool. Normal input, active steering and standalone group prompts describe the same delivery rule.
13
+ - Preserve private-chat replies, explicit reply tools, required goal results, command/status messages and media delivery. Mention rules, access settings and managed-plugin resolution are unchanged.
14
+ - This does not guarantee that a model will never make an unnecessary explicit reply, and it does not stop unrelated group input from reaching the model. No AgentSpine or language-guard update is included.
15
+ - Running sessions are not restarted. The updated core must be loaded by a new process. Existing user profiles and session history are retained.
16
+
1
17
  ## 9.1.576 - 2026-09-07
2
18
 
3
19
  - Preserve standalone C++ and C# as distinct terms in local research recall,
@@ -0,0 +1,49 @@
1
+ 'use strict';
2
+ const fs = require('node:fs');
3
+ const path = require('node:path');
4
+ const { createHash } = require('node:crypto');
5
+ const REPAIRS = {
6
+ fa511cd1e2a0a911aafa100d7a41b3ebd0abd0599c3e2e66daca1c8421298573: {
7
+ file: 'compat/mcp-server-fa511cd1.mjs', sha256: '0d2d078722245e1e559ea146ba5139d999f9ec1f3d4f21ce2dc6d68f6783d0a9',
8
+ },
9
+ '4ed83af87c8d4d89b1f21172aeabfd609bd7772242a9f7d046685acdd24d316c': {
10
+ file: 'dist/mcp-server.mjs', sha256: 'fa9ab7a79a0a98dad505d761f5e1e20a8f72ae640f67b0e9151bcce2bccd0998',
11
+ },
12
+ };
13
+ function fileHash(file) {
14
+ const info = fs.lstatSync(file);
15
+ if (!info.isFile() || info.size > 3 * 1024 * 1024) return null;
16
+ return createHash('sha256').update(fs.readFileSync(file)).digest('hex');
17
+ }
18
+ function resolveKnownTelegramMcp(selected, { explicitOverride = false } = {}) {
19
+ if (explicitOverride) return selected;
20
+ try {
21
+ const repair = REPAIRS[fileHash(selected)];
22
+ if (!repair) return selected;
23
+ const replacement = path.join(__dirname, '..', 'telegram-plugin', repair.file);
24
+ // Never replace unknown/custom code or launch a damaged compatibility payload.
25
+ return fileHash(replacement) === repair.sha256 ? replacement : selected;
26
+ } catch {
27
+ return selected;
28
+ }
29
+ }
30
+ function repairKnownTelegramPluginConfig(entry, { recipientName = '' } = {}) {
31
+ const config = entry.config;
32
+ if (entry.pluginId !== 'telegram' || entry.serverName !== 'telegram' || config?.transport !== 'stdio') return config;
33
+ if (process.env.BLUN_TELEGRAM_MCP_SERVER) return config;
34
+ const args = config.args;
35
+ if (!Array.isArray(args) || typeof config.cwd !== 'string') return config;
36
+ const native = args.length === 2 && args[0] === '__plugin_run_node';
37
+ if (!native && !(args.length === 1 && /^node(?:\.exe)?$/iu.test(path.basename(config.command || '')))) return config;
38
+ const index = native ? 1 : 0;
39
+ if (typeof args[index] !== 'string') return config;
40
+ const selected = path.resolve(config.cwd, args[index]);
41
+ const replacement = resolveKnownTelegramMcp(selected);
42
+ if (selected === replacement) return config;
43
+ return { ...config, args: args.map((value, i) => i === index ? replacement : value), env: {
44
+ ...config.env,
45
+ BLUN_TELEGRAM_PERSONA_NAME: typeof recipientName === 'string' ? recipientName : '',
46
+ ...(native ? { BLUN_PLUGIN_ROOT: path.dirname(path.dirname(replacement)) } : {}),
47
+ } };
48
+ }
49
+ module.exports = { resolveKnownTelegramMcp, repairKnownTelegramPluginConfig };
package/blun.mjs CHANGED
@@ -245623,6 +245623,7 @@ async function chatWithLiveResponseRepetitionRecovery(deps) {
245623
245623
  const { retryInput, params, stepEvents, signal, log } = deps;
245624
245624
  let currentParams = params;
245625
245625
  let repetitionRetries = 0;
245626
+ let thinkingRetries = 0;
245626
245627
  for (let attempt = 1;; attempt++) {
245627
245628
  try {
245628
245629
  const response = await chatWithRetry({
@@ -245640,6 +245641,15 @@ async function chatWithLiveResponseRepetitionRecovery(deps) {
245640
245641
  const thinkingTimeout = stepEvents.thinkingOnlyTimeoutResult;
245641
245642
  stepEvents.finishLiveResponseAttempt();
245642
245643
  if (thinkingTimeout !== null && !signal.aborted) {
245644
+ if (thinkingRetries >= 1) {
245645
+ log?.warn("thinking-only recovery exhausted; stopping this step", {
245646
+ elapsedMs: thinkingTimeout.elapsedMs,
245647
+ turnStep: `${retryInput.turnId}.${String(retryInput.currentStep)}`,
245648
+ thinkingRetries
245649
+ });
245650
+ throw error;
245651
+ }
245652
+ thinkingRetries += 1;
245643
245653
  log?.warn("thinking-only phase exceeded limit; forcing concrete action", {
245644
245654
  elapsedMs: thinkingTimeout.elapsedMs,
245645
245655
  turnStep: `${retryInput.turnId}.${String(retryInput.currentStep)}`
@@ -319635,7 +319645,7 @@ agentSpineRuntime: this.agentSpineRuntimeRecipients(pluginHooks, mcpConfig),
319635
319645
  mergePluginMcpConfig(base) {
319636
319646
  const pluginEntries = this.plugins.mcpServerConfigs();
319637
319647
  if (pluginEntries.length === 0) return base;
319638
- const pluginServers = this.withManagedBlunPluginEnv(Object.fromEntries(pluginEntries.map((entry) => [entry.runtimeName, entry.config])));
319648
+ const pluginServers = this.withManagedBlunPluginEnv(Object.fromEntries(pluginEntries.map((entry) => [entry.runtimeName, repairKnownTelegramPluginConfig(entry, { recipientName: readProfilePersona({ ...process.env, BLUN_HOME: this.homeDir }).persona?.name || "" })])));
319639
319649
  return {
319640
319650
  servers: {
319641
319651
  ...base?.servers,
@@ -422188,7 +422198,8 @@ function telegramChannelMcpServers(dir = telegramStateDir()) {
422188
422198
  args: launch.args,
422189
422199
  env: {
422190
422200
  ...launch.env,
422191
- BLUN_TELEGRAM_STATE_DIR: dir
422201
+ BLUN_TELEGRAM_STATE_DIR: dir,
422202
+ BLUN_TELEGRAM_PERSONA_NAME: readPersona$1()?.name || ""
422192
422203
  }
422193
422204
  } };
422194
422205
  }
@@ -422209,6 +422220,7 @@ function managedTelegramPluginRoot() {
422209
422220
  return void 0;
422210
422221
  }
422211
422222
  }
422223
+ var { resolveKnownTelegramMcp, repairKnownTelegramPluginConfig } = createRequire(import.meta.url)("./bin/telegram-mcp-compatibility.cjs");
422212
422224
  function resolveTelegramMcpEntry() {
422213
422225
  const main = process.argv[1];
422214
422226
  const managedRoot = managedTelegramPluginRoot();
@@ -422218,7 +422230,7 @@ function resolveTelegramMcpEntry() {
422218
422230
  join(homedir(), ".blun", "plugins", "managed", "telegram", "dist", "mcp-server.mjs"),
422219
422231
  main === void 0 ? void 0 : join(dirname(main), "..", "..", "..", "plugins", "telegram", "dist", "mcp-server.mjs")
422220
422232
  ];
422221
- for (const candidate of candidates) if (candidate !== void 0 && existsSync(candidate)) return candidate;
422233
+ for (const candidate of candidates) if (candidate !== void 0 && existsSync(candidate)) return resolveKnownTelegramMcp(candidate, { explicitOverride: candidate === process.env["BLUN_TELEGRAM_MCP_SERVER"] });
422222
422234
  }
422223
422235
  var TelegramChannelController = class {
422224
422236
  host;
@@ -519177,6 +519189,17 @@ async function saveAutoRetrievedMedia(result) {
519177
519189
  function isGroupChat(chatId) {
519178
519190
  return chatId.startsWith("-");
519179
519191
  }
519192
+ function withTelegramGroupReplyPolicy(chatId, text) {
519193
+ if (chatId === void 0 || !isGroupChat(chatId)) return text;
519194
+ return [
519195
+ "Telegram group delivery: use the telegram reply tool only for an intended, useful group answer.",
519196
+ "Ordinary final assistant text is not forwarded automatically to this group.",
519197
+ "If there is no useful contribution, make no reply call and do not announce silence or lack of work.",
519198
+ "Do not respond merely to explain that a message addresses someone else.",
519199
+ "",
519200
+ text
519201
+ ].join("\n");
519202
+ }
519180
519203
  function normalizeGroupNoise(text) {
519181
519204
  return text
519182
519205
  .toLowerCase()
@@ -521670,11 +521693,11 @@ const telegramBoundary = this.captureTelegramQueueBoundary();
521670
521693
  }, ...items.flatMap((queued) => {
521671
521694
  const textPart = {
521672
521695
  type: "text",
521673
- text: queued.text.trim()
521696
+ text: withTelegramGroupReplyPolicy(queued.channelChatId, queued.text.trim())
521674
521697
  };
521675
521698
  const imagePart = queued.channelImagePath === void 0 ? void 0 : buildChannelImagePart(queued.channelImagePath);
521676
521699
  return imagePart === void 0 ? [textPart] : [textPart, imagePart];
521677
- })] : `${notice}\n\n${items.map((queued) => queued.text.trim()).join("\n\n")}`;
521700
+ })] : `${notice}\n\n${items.map((queued) => withTelegramGroupReplyPolicy(queued.channelChatId, queued.text.trim())).join("\n\n")}`;
521678
521701
  const expectedTurnId = turnId !== void 0 && /^\d+$/.test(turnId) ? Number(turnId) : void 0;
521679
521702
  const restoreHead = (error) => {
521680
521703
  return this.restoreQueuedSteer(inFlight, error);
@@ -522365,7 +522388,7 @@ const telegramBoundary = this.captureTelegramQueueBoundary();
522365
522388
  if (installedGuard !== void 0) this.pendingChannelReplyGuard = installedGuard;
522366
522389
  const imagePart = channelImagePath !== void 0 ? buildChannelImagePart(channelImagePath) : void 0;
522367
522390
  const directNotice = channelDirect === true ? ["A private Telegram DM has priority over background, group, and loop work.", channelDirectResume === true ? "The user explicitly resumed work. Answer any remaining direct content, then continue the exact saved work checkpoint." : "The runtime saved the active work checkpoint. Answer the private conversation naturally, without exposing internal task, cron, checkpoint, queue, or lane narration. Unless the user explicitly asks to pause, stop, or wait, continue the exact saved work checkpoint automatically after the direct conversation."].join("\n") : void 0;
522368
- const focusedModelInput = directNotice === void 0 ? modelInput : `${directNotice}\n\n${modelInput}`;
522391
+ const focusedModelInput = directNotice === void 0 ? withTelegramGroupReplyPolicy(channelChatId, modelInput) : `${directNotice}\n\n${modelInput}`;
522369
522392
  const promptInput = imagePart !== void 0 && this.canReadImages() ? [{
522370
522393
  type: "text",
522371
522394
  text: focusedModelInput
@@ -522834,6 +522857,10 @@ if (this.session !== session) return;
522834
522857
  if (guard.directFocus === true) this.directFocusController.noteReplyDelivered(guard.chatId);
522835
522858
  continue;
522836
522859
  }
522860
+ if (isGroupChat(guard.chatId)) {
522861
+ this.clearAddressedChannelFocusIds?.([guard.channelFocusId]);
522862
+ continue;
522863
+ }
522837
522864
  const decision = channelReplyRecoveryDecision({
522838
522865
  hasPendingReply: true,
522839
522866
  lastStepFinishReason,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blun-king-cli",
3
- "version": "9.1.576",
3
+ "version": "9.1.578",
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": {
@@ -0,0 +1,48 @@
1
+ 'use strict';
2
+
3
+ const { readJsonlTail } = require('./telegram-private-conversation-policy.cjs');
4
+ const MAX_AGE_MS = 30 * 60 * 1000;
5
+ const PRESENCE_ANSWER = /^(?:(?:ja )?(?:bin da|bin hier|ich bin da|hier bin ich|ich bin hier|bereit|i am here|i m here|here|ready))(?: (?:was brauchst du|was gibt es|was liegt an|wie kann ich helfen|what do you need|how can i help))?$/u;
6
+ const PRESENCE_QUESTION = /^(?:bist du da|bist du hier|bist du erreichbar|are you there|are you here)$/u;
7
+
8
+ function normalize(value) {
9
+ return String(value ?? '').normalize('NFKC').toLocaleLowerCase('de-DE')
10
+ .replace(/[^\p{L}\p{N}\s]/gu, ' ').replace(/\s+/gu, ' ').trim();
11
+ }
12
+
13
+ function messageId(value) {
14
+ if (typeof value !== 'string' && typeof value !== 'number') return null;
15
+ if (!/^[1-9]\d*$/u.test(String(value))) return null;
16
+ const number = Number(value);
17
+ return Number.isSafeInteger(number) ? number : null;
18
+ }
19
+
20
+ function ownPresenceQuestion(content, recipientName) {
21
+ if (typeof content !== 'string' || content.length > 512) return false;
22
+ if (typeof recipientName !== 'string' || recipientName.length > 128) return false;
23
+ const name = normalize(recipientName), text = normalize(content);
24
+ if (!name) return false;
25
+ if (text === name) return true;
26
+ return text.startsWith(name + ' ') && PRESENCE_QUESTION.test(text.slice(name.length + 1));
27
+ }
28
+
29
+ function permitsPresenceReply({ text, chatId, replyTo, inboxFile,
30
+ recipientName = process.env.BLUN_TELEGRAM_PERSONA_NAME || '', now = Date.now() }) {
31
+ if (typeof text !== 'string' || text.length > 160 || !PRESENCE_ANSWER.test(normalize(text))) return false;
32
+ const wantedId = messageId(replyTo), wantedChat = String(chatId ?? '');
33
+ if (wantedId === null || !/^-[1-9]\d*$/u.test(wantedChat) || !Number.isFinite(now)) return false;
34
+ // A semantic marker is shared by legacy recipients; only a bound own-name ping can refine it.
35
+ const records = readJsonlTail(inboxFile);
36
+ for (let index = records.length - 1; index >= 0; index -= 1) {
37
+ const record = records[index], meta = record.meta;
38
+ if (record.direction !== 'in' || !meta || String(meta.chat_id) !== wantedChat || messageId(meta.message_id) !== wantedId) continue;
39
+ const timestamp = Date.parse(String(meta.ts ?? record.ts ?? ''));
40
+ if (!Number.isFinite(timestamp) || timestamp > now || now - timestamp > MAX_AGE_MS) return false;
41
+ return meta.addressed === true || meta.addressed === 'true'
42
+ || meta.addressed === 1 || meta.addressed === '1'
43
+ || (meta.addressed === 'semantic' && ownPresenceQuestion(record.content, recipientName));
44
+ }
45
+ return false;
46
+ }
47
+
48
+ module.exports = { MAX_AGE_MS, messageId, ownPresenceQuestion, permitsPresenceReply };
@@ -174,6 +174,7 @@ function isPrivateInternalStatusReply(chatId, text) {
174
174
  }
175
175
 
176
176
  module.exports = {
177
+ readJsonlTail,
177
178
  DUPLICATE_LOG_TAIL_BYTES,
178
179
  DUPLICATE_MAX_AGE_MS,
179
180
  NEAR_DUPLICATE_MIN_CHARS,