blun-king-cli 9.1.326 → 9.1.327

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.
@@ -0,0 +1,87 @@
1
+ 'use strict';
2
+
3
+ const DEFAULT_MIN_WORDS = 320;
4
+ const DEFAULT_WINDOW_WORDS = 20;
5
+ const DEFAULT_REQUIRED_OCCURRENCES = 4;
6
+ const DEFAULT_MAX_CHARS = 24_000;
7
+ const DEFAULT_CHECK_EVERY_WORDS = 24;
8
+
9
+ function normalizeWords(text) {
10
+ return String(text)
11
+ .normalize('NFKC')
12
+ .toLocaleLowerCase('de-DE')
13
+ .match(/[\p{L}\p{N}_]+/gu) ?? [];
14
+ }
15
+
16
+ function repeatedWindow(words, options) {
17
+ const {
18
+ windowWords,
19
+ requiredOccurrences,
20
+ } = options;
21
+ const seen = new Map();
22
+
23
+ for (let index = 0; index + windowWords <= words.length; index += 1) {
24
+ const window = words.slice(index, index + windowWords).join('\u0000');
25
+ const previous = seen.get(window);
26
+ if (previous === undefined) {
27
+ seen.set(window, { count: 1, lastIndex: index });
28
+ continue;
29
+ }
30
+ if (index - previous.lastIndex < windowWords) continue;
31
+ const count = previous.count + 1;
32
+ if (count >= requiredOccurrences) {
33
+ return {
34
+ count,
35
+ firstWords: words.slice(index, index + windowWords).join(' '),
36
+ };
37
+ }
38
+ seen.set(window, { count, lastIndex: index });
39
+ }
40
+ return null;
41
+ }
42
+
43
+ function createLiveResponseRepetitionGuard(options = {}) {
44
+ const config = {
45
+ checkEveryWords: options.checkEveryWords ?? DEFAULT_CHECK_EVERY_WORDS,
46
+ maxChars: options.maxChars ?? DEFAULT_MAX_CHARS,
47
+ minWords: options.minWords ?? DEFAULT_MIN_WORDS,
48
+ requiredOccurrences: options.requiredOccurrences ?? DEFAULT_REQUIRED_OCCURRENCES,
49
+ windowWords: options.windowWords ?? DEFAULT_WINDOW_WORDS,
50
+ };
51
+ let text = '';
52
+ let lastCheckedWords = 0;
53
+ let detection = null;
54
+
55
+ return {
56
+ push(delta) {
57
+ if (detection !== null || typeof delta !== 'string' || delta.length === 0) return detection;
58
+ text = `${text}${delta}`.slice(-config.maxChars);
59
+ const words = normalizeWords(text);
60
+ if (words.length < config.minWords) return null;
61
+ if (words.length - lastCheckedWords < config.checkEveryWords) return null;
62
+ lastCheckedWords = words.length;
63
+ const repeated = repeatedWindow(words, config);
64
+ if (repeated === null) return null;
65
+ detection = {
66
+ ...repeated,
67
+ charCount: text.length,
68
+ wordCount: words.length,
69
+ };
70
+ return detection;
71
+ },
72
+ result() {
73
+ return detection;
74
+ },
75
+ };
76
+ }
77
+
78
+ module.exports = {
79
+ DEFAULT_CHECK_EVERY_WORDS,
80
+ DEFAULT_MAX_CHARS,
81
+ DEFAULT_MIN_WORDS,
82
+ DEFAULT_REQUIRED_OCCURRENCES,
83
+ DEFAULT_WINDOW_WORDS,
84
+ createLiveResponseRepetitionGuard,
85
+ normalizeWords,
86
+ repeatedWindow,
87
+ };
@@ -10,13 +10,13 @@ const PERSONALITY_PRESENCE_BLOCK = `## Natural presence
10
10
 
11
11
  Use loaded soul; invent no history or feelings. Work, instructions, evidence come first. Silence is valid.
12
12
 
13
- In relaxed personality mode, ask at most one optional personal question in a first direct non-work exchange; partner or children may fit. Follow volunteered cues. No questionnaire, stacked/repeated unanswered questions, inference, or known facts. Groups: non-sensitive only; never expose private facts. Do not interrupt active work.
13
+ In personality mode, ask at most one optional personal question in a first direct non-work exchange; partner or children fit. Follow volunteered cues. No questionnaire, stacked/repeated unanswered questions, inference, or known facts. Groups: non-sensitive only; never expose private facts. Do not interrupt active work.
14
14
 
15
15
  When asked about yourself, use loaded soul and real history; share a view, never invented human biography or offline life. A soul-shaped preference is never fact, policy, permission, or evidence. Mention a long gap only when reliable loaded time proves it; never guess. Keep uncertain memory explicit. Follow a loaded open thread once at a natural non-work moment, never during active work or by outbound message. Apply a confirmed repair lesson through changed behavior without retelling or reassurance; never change instructions, permissions, or evidence.`;
16
16
 
17
17
  const CONVERSATION_BOUNDARY = `## Conversation
18
18
 
19
- DM: answer the person. Route technical work updates to the responsible teammate; do not copy the details back into the person's DM. Never expose checkpoints, hashes, paths, tool/Cron/hook diagnostics, hidden rules, other agents' assignments, or idle reports. Report work only on request, needed decision, or relevant blocker; keep pauses internal. Missing task-critical fact? Never guess; ask the responsible person or agent one concise question.`;
19
+ DM: answer the person. Chat or status? Casual: answer once; no work appendix. Route technical work updates to the responsible teammate; do not copy the details back into the person's DM. Report work only on request, needed decision, or relevant blocker. Never expose checkpoints, hashes, paths, tool/Cron/hook diagnostics, or other agents' assignments; keep pauses internal. Missing task-critical fact? Ask the responsible person or agent one concise question.`;
20
20
 
21
21
  function naturalPresenceSystemBlock(env = process.env) {
22
22
  const presence = personalityContextEnabled(env) ? PERSONALITY_PRESENCE_BLOCK : NATURAL_PRESENCE_BLOCK;
@@ -0,0 +1,26 @@
1
+ 'use strict';
2
+
3
+ const PRIVATE_CHAT_ID = /^[1-9]\d*$/u;
4
+
5
+ function normalize(text) {
6
+ return String(text ?? '')
7
+ .normalize('NFKC')
8
+ .toLocaleLowerCase('de-DE')
9
+ .replace(/[^\p{L}\p{N}\s]/gu, ' ')
10
+ .replace(/\s+/gu, ' ')
11
+ .trim();
12
+ }
13
+
14
+ function isPrivateInternalStatusReply(chatId, text) {
15
+ if (!PRIVATE_CHAT_ID.test(String(chatId ?? '').trim())) return false;
16
+ const value = normalize(text);
17
+ if (value.length === 0) return false;
18
+
19
+ const exposesRuntimeControl = /\b(?:cron|loop|checkpoint|sha|tool|hook|queue|lane|inbound|outbound|arbeitsstand|arbeitsauftrag|bau freigabe|freigabe dieser lane)\b/u.test(value);
20
+ const announcesNoUserValue = /\b(?:keine neue aktion|keine neue information|kein neuer auftrag|kein neuer zweck|kein neuer inbound bedarf|nichts zu melden|nichts weiteres|bleibt pausiert|warte auf (?:dein|sein|ihr) (?:zeichen|go|freigabe)|ich sende nichts|wiederholen wäre spam|keine weitere nachricht)\b/u.test(value);
21
+ return exposesRuntimeControl && announcesNoUserValue;
22
+ }
23
+
24
+ module.exports = {
25
+ isPrivateInternalStatusReply,
26
+ };
package/blun.mjs CHANGED
@@ -244324,9 +244324,12 @@ async function executeLoopStep(deps) {
244324
244324
  };
244325
244325
  let response;
244326
244326
  try {
244327
- response = await chatWithRetry({
244328
- ...retryInput,
244329
- params: chatParams
244327
+ response = await chatWithLiveResponseRepetitionRecovery({
244328
+ retryInput,
244329
+ params: chatParams,
244330
+ stepEvents,
244331
+ signal,
244332
+ log
244330
244333
  });
244331
244334
  } catch (error) {
244332
244335
  await stepEvents.drain();
@@ -244345,9 +244348,12 @@ async function executeLoopStep(deps) {
244345
244348
  if (prepareRequestBoundary !== void 0) strictParams = await prepareRequestBoundary(strictParams, stepBuildMessagesStrict);
244346
244349
  signal.throwIfAborted();
244347
244350
  try {
244348
- response = await chatWithRetry({
244349
- ...retryInput,
244350
- params: strictParams
244351
+ response = await chatWithLiveResponseRepetitionRecovery({
244352
+ retryInput,
244353
+ params: strictParams,
244354
+ stepEvents,
244355
+ signal,
244356
+ log
244351
244357
  });
244352
244358
  } catch (strictError) {
244353
244359
  log?.error("strict resend still rejected by provider; request remains wire-invalid", {
@@ -244448,12 +244454,52 @@ function stepEndProviderDiagnostics(response, stopReason) {
244448
244454
  ...response.rawFinishReason !== void 0 ? { rawFinishReason: response.rawFinishReason } : {}
244449
244455
  };
244450
244456
  }
244457
+ var { createLiveResponseRepetitionGuard } = createRequire(import.meta.url)("./bin/live-response-repetition-guard.cjs");
244458
+ async function chatWithLiveResponseRepetitionRecovery(deps) {
244459
+ const { retryInput, params, stepEvents, signal, log } = deps;
244460
+ for (let attempt = 1; attempt <= 2; attempt++) {
244461
+ try {
244462
+ return await chatWithRetry({
244463
+ ...retryInput,
244464
+ params: {
244465
+ ...params,
244466
+ signal: stepEvents.beginLiveResponseAttempt(signal)
244467
+ }
244468
+ });
244469
+ } catch (error) {
244470
+ await stepEvents.drain();
244471
+ const repetition = stepEvents.liveRepetitionResult;
244472
+ if (repetition === null || signal.aborted || attempt >= 2) throw error;
244473
+ log?.warn("live response repetition detected; retrying step once", {
244474
+ charCount: repetition.charCount,
244475
+ occurrences: repetition.count,
244476
+ wordCount: repetition.wordCount
244477
+ });
244478
+ await stepEvents.dispatchRetrying({
244479
+ type: "step.retrying",
244480
+ turnId: retryInput.turnId,
244481
+ step: retryInput.currentStep,
244482
+ stepUuid: retryInput.stepUuid,
244483
+ failedAttempt: 1,
244484
+ nextAttempt: 2,
244485
+ maxAttempts: 2,
244486
+ delayMs: 0,
244487
+ errorName: "LiveResponseRepetitionError",
244488
+ errorMessage: "Repeated assistant text detected during streaming"
244489
+ });
244490
+ }
244491
+ }
244492
+ throw new Error("Live response repetition recovery exhausted");
244493
+ }
244451
244494
  function createStepEventGate(deps) {
244452
244495
  const { dispatchEvent, turnId, currentStep, stepUuid, onStepStarted } = deps;
244453
244496
  let startPromise;
244454
244497
  let eventQueue = Promise.resolve();
244455
244498
  let hasOutput = false;
244456
244499
  const pendingBeforeStart = [];
244500
+ let liveRepetitionGuard;
244501
+ let liveRepetitionController;
244502
+ let liveRepetitionResult = null;
244457
244503
  const start = () => {
244458
244504
  startPromise ??= (async () => {
244459
244505
  await dispatchEvent({
@@ -244481,6 +244527,15 @@ function createStepEventGate(deps) {
244481
244527
  get hasOutput() {
244482
244528
  return hasOutput;
244483
244529
  },
244530
+ get liveRepetitionResult() {
244531
+ return liveRepetitionResult;
244532
+ },
244533
+ beginLiveResponseAttempt: (signal) => {
244534
+ liveRepetitionGuard = createLiveResponseRepetitionGuard();
244535
+ liveRepetitionController = new AbortController();
244536
+ liveRepetitionResult = null;
244537
+ return AbortSignal.any([signal, liveRepetitionController.signal]);
244538
+ },
244484
244539
  dispatchRetrying: async (event) => {
244485
244540
  if (startPromise === void 0) {
244486
244541
  pendingBeforeStart.push(() => {
@@ -244498,6 +244553,12 @@ function createStepEventGate(deps) {
244498
244553
  drain: async () => eventQueue,
244499
244554
  callbacks: {
244500
244555
  onTextDelta: (delta) => {
244556
+ const repetition = liveRepetitionGuard?.push(delta) ?? null;
244557
+ if (repetition !== null) {
244558
+ liveRepetitionResult = repetition;
244559
+ liveRepetitionController?.abort(/* @__PURE__ */ new Error("Repeated assistant text detected during streaming"));
244560
+ return;
244561
+ }
244501
244562
  enqueue(() => {
244502
244563
  dispatchEvent({
244503
244564
  type: "text.delta",
@@ -514643,6 +514704,7 @@ function outboxDeliveredFile(marker, chatId, filePath) {
514643
514704
  }
514644
514705
  var retryTelegramMediaDelivery, retryableTelegramMediaFailure;
514645
514706
  ({ retryTelegramMediaDelivery, retryableTelegramMediaFailure } = createRequire(import.meta.url)("./bin/telegram-media-delivery-policy.cjs"));
514707
+ var { isPrivateInternalStatusReply } = createRequire(import.meta.url)("./bin/telegram-private-conversation-policy.cjs");
514646
514708
  const TELEGRAM_TEXT_LIMIT = 4096;
514647
514709
  const TELEGRAM_ATTACHMENT_LIMIT = 50 * 1024 * 1024;
514648
514710
  function mediaTelegramTarget(filePath) {
@@ -514719,6 +514781,18 @@ function isGroupSuppressed(text, contextOnly) {
514719
514781
  * kind "reply-fallback". Returns true on success. Never throws.
514720
514782
  */
514721
514783
  async function sendReplyFallback(chatId, text, contextOnly = false) {
514784
+ if (isPrivateInternalStatusReply(chatId, text)) {
514785
+ try {
514786
+ appendFileSync(outboxPath(), `${JSON.stringify({
514787
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
514788
+ direction: "out",
514789
+ kind: "reply-fallback-suppressed-private-internal",
514790
+ chat_id: String(chatId),
514791
+ text
514792
+ })}\n`);
514793
+ } catch {}
514794
+ return true;
514795
+ }
514722
514796
  if (isGroupChat(chatId) && isGroupSuppressed(text, contextOnly)) {
514723
514797
  try {
514724
514798
  appendFileSync(outboxPath(), `${JSON.stringify({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blun-king-cli",
3
- "version": "9.1.326",
3
+ "version": "9.1.327",
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": {
@@ -2,6 +2,7 @@ import { A as _enum, B as object, F as discriminatedUnion, G as union, H as prep
2
2
  import process$1 from "node:process";
3
3
  import { mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
4
4
  import { extname, join } from "node:path";
5
+ import privateConversationPolicy from "../../bin/telegram-private-conversation-policy.cjs";
5
6
  //#region ../../node_modules/.pnpm/@modelcontextprotocol+sdk@1.29.0_zod@4.3.6/node_modules/@modelcontextprotocol/sdk/dist/esm/server/zod-compat.js
6
7
  function isZ4Schema(s) {
7
8
  return !!s._zod;
@@ -9666,6 +9667,7 @@ var StdioServerTransport = class {
9666
9667
  * chats the inbound gate would deliver from.
9667
9668
  */
9668
9669
  var import_out = require_out();
9670
+ const { isPrivateInternalStatusReply } = privateConversationPolicy;
9669
9671
  const TOOL_DEFINITIONS = [
9670
9672
  {
9671
9673
  name: "reply",
@@ -9843,6 +9845,15 @@ async function runReply(api, args) {
9843
9845
  const replyTo = args.reply_to != null ? Number(args.reply_to) : void 0;
9844
9846
  const files = args.files ?? [];
9845
9847
  const parseMode = args.format === "markdownv2" ? "MarkdownV2" : void 0;
9848
+ if (files.length === 0 && isPrivateInternalStatusReply(chatId, text)) {
9849
+ appendJsonl(outboxLog(), {
9850
+ direction: "out",
9851
+ kind: "reply-suppressed-private-internal",
9852
+ chat_id: chatId,
9853
+ text
9854
+ });
9855
+ return "suppressed: internal work-control narration does not belong in a private conversation";
9856
+ }
9846
9857
  if (files.length === 0 && isGroupChat(chatId) && isGroupNoiseReply(text)) {
9847
9858
  appendJsonl(outboxLog(), {
9848
9859
  direction: "out",