blun-king-cli 9.1.614 → 9.1.616

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.
@@ -7,7 +7,7 @@ Auflagen Dieter 43102:
7
7
  (Run-3-Befund: Liste trieb RAM-Spitze auf 10,5 GB).
8
8
  - Inkrementell: Content-Hash je Datei im Manifest, Delta statt Vollindex.
9
9
  - Query mit Frische-Auskunft + Selbstauskunft in JEDER Antwort.
10
- - Query-Fehlschlag-Log lokal (query-log.jsonl) — der Moat.
10
+ - Lokales Query-Metadatenlog (query-log.jsonl), ohne Fragetext.
11
11
 
12
12
  Aufruf:
13
13
  python codebase_index.py build <repo>
@@ -351,13 +351,29 @@ def cmd_update(repo: str) -> None:
351
351
  )
352
352
 
353
353
 
354
- def freshness(repo: str, man: dict) -> str:
355
- current = git_head(repo)
356
- same = "== HEAD" if current == man["head"] else f"Index {man['head']} != HEAD {current} — 'update' faellig"
357
- return f"Index-Stand: {man['head']} ({man['built_at']}), {man['chunks']} Chunks | {same}"
358
-
359
-
360
- def cmd_query(repo: str, question: str, top: int, selector: str = "auto") -> int:
354
+ def freshness(repo: str, man: dict) -> str:
355
+ current = git_head(repo)
356
+ same = "== HEAD" if current == man["head"] else f"Index {man['head']} != HEAD {current} — 'update' faellig"
357
+ return f"Index-Stand: {man['head']} ({man['built_at']}), {man['chunks']} Chunks | {same}"
358
+
359
+
360
+ def append_query_metadata(dirpath: str, question: str, hits: list[tuple[str, float]],
361
+ requested_top: int, latency_ms: float, man: dict) -> None:
362
+ record = {
363
+ "ts": datetime.now(timezone.utc).isoformat(),
364
+ "query_bytes": len(question.encode("utf-8")),
365
+ "requested_top": requested_top,
366
+ "result_count": len(hits),
367
+ "result_paths": list(dict.fromkeys(meta.rsplit("#", 1)[0] for meta, _ in hits)),
368
+ "query_ms": round(latency_ms, 1),
369
+ "index_head": man["head"],
370
+ "model": man["model"],
371
+ }
372
+ with open(os.path.join(dirpath, "query-log.jsonl"), "a", encoding="utf-8") as fh:
373
+ fh.write(json.dumps(record) + "\n")
374
+
375
+
376
+ def cmd_query(repo: str, question: str, top: int, selector: str = "auto") -> int:
361
377
  dirpath, man = resolve_existing_index(repo, selector)
362
378
  activate_model(man["model"], man["dim"])
363
379
  mat = np.load(os.path.join(dirpath, "vectors.npy"))
@@ -380,15 +396,7 @@ def cmd_query(repo: str, question: str, top: int, selector: str = "auto") -> int
380
396
  print(f" {s:.4f} {m}")
381
397
  print(f"query_ms={latency_ms:.1f}")
382
398
 
383
- log_path = os.path.join(dirpath, "query-log.jsonl")
384
- with open(log_path, "a", encoding="utf-8") as fh:
385
- fh.write(json.dumps({
386
- "ts": datetime.now(timezone.utc).isoformat(),
387
- "query": question,
388
- "top": hits,
389
- "query_ms": round(latency_ms, 1),
390
- "index_head": man["head"],
391
- }) + "\n")
399
+ append_query_metadata(dirpath, question, hits, top, latency_ms, man)
392
400
  return 0
393
401
 
394
402
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blun-king-cli",
3
- "version": "9.1.614",
3
+ "version": "9.1.616",
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": {
@@ -1,11 +1,12 @@
1
1
  import { createRequire } from "node:module";
2
2
  import { appendFileSync, chmodSync, closeSync, existsSync, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
3
3
  import { dirname, join, resolve } from "node:path";
4
- import { createHash, createHmac, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
4
+ import { createHash, randomBytes, randomUUID } from "node:crypto";
5
5
  import { homedir } from "node:os";
6
6
  import { spawn } from "node:child_process";
7
7
  import { Readable, Writable } from "node:stream";
8
8
  import { fileURLToPath } from "node:url";
9
+ import { StringDecoder } from "node:string_decoder";
9
10
  //#region \0rolldown/runtime.js
10
11
  var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
11
12
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
@@ -41794,10 +41795,10 @@ var require_internal = /* @__PURE__ */ __commonJSMin(((exports, module) => {
41794
41795
  }
41795
41796
  InternalCodec.prototype.encoder = InternalEncoder;
41796
41797
  InternalCodec.prototype.decoder = InternalDecoder;
41797
- var StringDecoder = __require("string_decoder").StringDecoder;
41798
- if (!StringDecoder.prototype.end) StringDecoder.prototype.end = function() {};
41798
+ var StringDecoder$1 = __require("string_decoder").StringDecoder;
41799
+ if (!StringDecoder$1.prototype.end) StringDecoder$1.prototype.end = function() {};
41799
41800
  function InternalDecoder(options, codec) {
41800
- this.decoder = new StringDecoder(codec.enc);
41801
+ this.decoder = new StringDecoder$1(codec.enc);
41801
41802
  }
41802
41803
  InternalDecoder.prototype.write = function(buf) {
41803
41804
  if (!Buffer.isBuffer(buf)) buf = Buffer.from(buf);
@@ -60026,7 +60027,7 @@ function toInboundCtx(ctx, text) {
60026
60027
  chatId: ctx.chat?.id,
60027
60028
  chatType: chatType === "private" || chatType === "group" || chatType === "supergroup" || chatType === "channel" ? chatType : void 0,
60028
60029
  messageId: ctx.message?.message_id,
60029
- messageThreadId: ctx.message?.message_thread_id,
60030
+ messageThreadId: ctx.message?.is_topic_message === true || ctx.chat?.is_forum === true ? ctx.message?.message_thread_id : void 0,
60030
60031
  text,
60031
60032
  entities: (ctx.message?.entities ?? ctx.message?.caption_entities ?? []).map((entity) => ({
60032
60033
  type: entity.type,
@@ -60197,7 +60198,7 @@ var BoundedChannelContextBuffer = class {
60197
60198
  function buildChannelTag(content, meta, addressed = true, inferAddressing = false) {
60198
60199
  const pairs = [["source", "telegram"]];
60199
60200
  for (const [key, value] of Object.entries(meta)) if (value !== void 0) pairs.push([key, String(value)]);
60200
- return `<channel ${pairs.map(([key, value]) => `${key}="${escapeAttr(value)}"`).join(" ")}>\n${content}\n</channel>\n${addressed && meta.chat_id.startsWith("-") ? `[GRUPPENANTWORT: Eine bloße Information oder eine Nachricht, die ausdrücklich keine Bestätigung verlangt, macht keine Antwort erforderlich. Beende den Turn dann ohne Ausgabe und ohne Bestätigung deiner Stille. Eine Antwort auf deine Statusfrage ohne neuen Auftrag braucht keine Zusammenfassung und keinen sichtbaren Text. Falls ein Abschlusstext nötig ist, gib nur [[BLUN_TELEGRAM_SILENT]] aus. Einen konkreten Auftrag an dich nimm kurz an und beginne die Arbeit; melde danach das Ergebnis oder einen echten Blocker. Eine Frage beantworte, wenn du etwas beitragen kannst. Sende eine nötige Antwort mit dem telegram reply-Tool an chat_id=${meta.chat_id}, ohne dieselbe Annahme zu wiederholen.]` : addressed ? `[REPLY-PFLICHT: Der Absender liest NUR Telegram. Sende deine Antwort mit dem telegram reply-Tool an chat_id=${meta.chat_id}. Sichtbarer Antworttext kann auch automatisch weitergeleitet werden; er ist kein privater Denkbereich.]` : inferAddressing ? `[SEMANTISCHE ADRESSIERUNG: Diese menschliche Gruppennachricht nennt dich nicht explizit. Entscheide anhand von Inhalt und Gesprächskontext, ob sie an dich gerichtet ist oder Arbeit von dir verlangt. Falls ja, antworte kurz mit dem telegram reply-Tool an chat_id=${meta.chat_id}. Falls nein, beende den Turn ohne Ausgabe; sende keine Bestätigung deiner Stille.]` : "[NUR MITLESEN — diese Nachricht ist NICHT an dich gerichtet (kein @King, keine Antwort an dich, sie spricht jemand anderen oder die Gruppe allgemein an). ANTWORTE NICHT: kein reply-Tool, keine Bestätigung, kein \"verstanden\", kein \"halte mich raus\" — beende den Turn einfach ohne Ausgabe. NUR wenn du ein echtes, konkretes Ergebnis oder einen echten Blocker beitragen kannst, antwortest du kurz. Im Zweifel: schweigen.]"}`;
60201
+ return `<channel ${pairs.map(([key, value]) => `${key}="${escapeAttr(value)}"`).join(" ")}>\n${content}\n</channel>\n${addressed && meta.chat_id.startsWith("-") ? `[GRUPPENANTWORT: Eine bloße Information oder eine Nachricht, die ausdrücklich keine Bestätigung verlangt, macht keine Antwort erforderlich. Beende den Turn dann ohne Ausgabe und ohne Bestätigung deiner Stille. Antwortet jemand auf deine eigene Statusfrage ohne neuen Auftrag, schweige. Fragt dich jemand nach dem Stand, antworte kurz und ehrlich. Falls ein Abschlusstext nötig ist, gib nur [[BLUN_TELEGRAM_SILENT]] aus. Einen konkreten Auftrag an dich nimm kurz an und beginne die Arbeit; melde danach das Ergebnis oder einen echten Blocker. Eine Frage beantworte, wenn du etwas beitragen kannst. Sende eine nötige Antwort mit dem telegram reply-Tool an chat_id=${meta.chat_id}, ohne dieselbe Annahme zu wiederholen.]` : addressed ? `[REPLY-PFLICHT: Der Absender liest NUR Telegram. Sende deine Antwort mit dem telegram reply-Tool an chat_id=${meta.chat_id}. Sichtbarer Antworttext kann auch automatisch weitergeleitet werden; er ist kein privater Denkbereich.]` : inferAddressing ? `[SEMANTISCHE ADRESSIERUNG: Diese menschliche Gruppennachricht nennt dich nicht explizit. Entscheide anhand von Inhalt und Gesprächskontext, ob sie an dich gerichtet ist oder Arbeit von dir verlangt. Falls ja, antworte kurz mit dem telegram reply-Tool an chat_id=${meta.chat_id}. Falls nein, beende den Turn ohne Ausgabe; sende keine Bestätigung deiner Stille.]` : "[NUR MITLESEN — diese Nachricht ist NICHT an dich gerichtet (kein @King, keine Antwort an dich, sie spricht jemand anderen oder die Gruppe allgemein an). ANTWORTE NICHT: kein reply-Tool, keine Bestätigung, kein \"verstanden\", kein \"halte mich raus\" — beende den Turn einfach ohne Ausgabe. NUR wenn du ein echtes, konkretes Ergebnis oder einen echten Blocker beitragen kannst, antwortest du kurz. Im Zweifel: schweigen.]"}`;
60201
60202
  }
60202
60203
  /**
60203
60204
  * Sent once as the head of the first prompt of every bridged session —
@@ -67686,204 +67687,6 @@ var RequestError = class RequestError extends Error {
67686
67687
  }
67687
67688
  };
67688
67689
  //#endregion
67689
- //#region src/language-release.ts
67690
- const EXACT_LANGUAGE_TAG = /^(?:[A-Za-z]{2,8}|x)(?:-[A-Za-z0-9]{1,8})*$/;
67691
- const LETTER = /^\p{Letter}$/u;
67692
- const LATIN = /^\p{Script=Latin}$/u;
67693
- const CYRILLIC = /^\p{Script=Cyrillic}$/u;
67694
- const GREEK = /^\p{Script=Greek}$/u;
67695
- const ARABIC = /^\p{Script=Arabic}$/u;
67696
- const HEBREW = /^\p{Script=Hebrew}$/u;
67697
- const HAN = /^\p{Script=Han}$/u;
67698
- const HIRAGANA = /^\p{Script=Hiragana}$/u;
67699
- const KATAKANA = /^\p{Script=Katakana}$/u;
67700
- const HANGUL = /^\p{Script=Hangul}$/u;
67701
- const LATIN_LANGUAGES = new Set([
67702
- "af",
67703
- "bs",
67704
- "ca",
67705
- "cs",
67706
- "cy",
67707
- "da",
67708
- "de",
67709
- "en",
67710
- "es",
67711
- "et",
67712
- "eu",
67713
- "fi",
67714
- "fr",
67715
- "ga",
67716
- "gd",
67717
- "gl",
67718
- "hr",
67719
- "hu",
67720
- "id",
67721
- "is",
67722
- "it",
67723
- "la",
67724
- "lb",
67725
- "lt",
67726
- "lv",
67727
- "ms",
67728
- "mt",
67729
- "nl",
67730
- "no",
67731
- "pl",
67732
- "pt",
67733
- "ro",
67734
- "sk",
67735
- "sl",
67736
- "sq",
67737
- "sv",
67738
- "sw",
67739
- "tr",
67740
- "vi"
67741
- ]);
67742
- function canonicalText(text) {
67743
- return text.replace(/^\uFEFF/, "").replace(/\r\n?/g, "\n").normalize("NFC");
67744
- }
67745
- function canonicalHash(text) {
67746
- return createHash("sha256").update(canonicalText(text), "utf8").digest("hex");
67747
- }
67748
- function maskTechnicalText(text) {
67749
- const mask = (value) => " ".repeat(value.length);
67750
- return text.replace(/```[\s\S]*?```|~~~[\s\S]*?~~~/g, mask).replace(/`[^`\r\n]*`/g, mask);
67751
- }
67752
- function allowedScriptMatchers(locale) {
67753
- if (!EXACT_LANGUAGE_TAG.test(locale) || ["auto", "all"].includes(locale.toLowerCase())) throw new Error("language release blocked: exact output locale required");
67754
- const parts = locale.replaceAll("_", "-").split("-");
67755
- const base = parts[0]?.toLowerCase() ?? "";
67756
- const explicitScript = parts.find((part) => part.length === 4)?.toLowerCase();
67757
- if (explicitScript !== void 0) {
67758
- const explicit = {
67759
- latn: [LATIN],
67760
- cyrl: [CYRILLIC],
67761
- grek: [GREEK],
67762
- arab: [ARABIC],
67763
- hebr: [HEBREW],
67764
- hani: [HAN],
67765
- jpan: [
67766
- HAN,
67767
- HIRAGANA,
67768
- KATAKANA
67769
- ],
67770
- hang: [HANGUL, HAN]
67771
- }[explicitScript];
67772
- if (explicit === void 0) throw new Error("language release blocked: unsupported output script");
67773
- return explicit;
67774
- }
67775
- if (LATIN_LANGUAGES.has(base)) return [LATIN];
67776
- if ([
67777
- "ru",
67778
- "uk",
67779
- "bg",
67780
- "mk"
67781
- ].includes(base)) return [CYRILLIC];
67782
- if (base === "el") return [GREEK];
67783
- if ([
67784
- "ar",
67785
- "fa",
67786
- "ur"
67787
- ].includes(base)) return [ARABIC];
67788
- if (base === "he") return [HEBREW];
67789
- if (base === "zh") return [HAN];
67790
- if (base === "ja") return [
67791
- HAN,
67792
- HIRAGANA,
67793
- KATAKANA
67794
- ];
67795
- if (base === "ko") return [HANGUL, HAN];
67796
- throw new Error("language release blocked: output locale has no deterministic script policy");
67797
- }
67798
- function assertExpectedScript(text, locale) {
67799
- const allowed = allowedScriptMatchers(locale);
67800
- for (const character of maskTechnicalText(text)) {
67801
- if (!LETTER.test(character)) continue;
67802
- if (allowed.some((matcher) => matcher.test(character))) continue;
67803
- const point = character.codePointAt(0)?.toString(16).toUpperCase().padStart(4, "0") ?? "UNKNOWN";
67804
- throw new Error(`language release blocked: unexpected-script-contamination (U+${point})`);
67805
- }
67806
- }
67807
- function parseAndVerifySignature(token, key) {
67808
- if (key.length < 32) throw new Error("language release blocked: invalid verification key");
67809
- if (token.length > 16384) throw new Error("language release blocked: release token too large");
67810
- const parts = token.split(".");
67811
- if (parts.length !== 3 || parts[0] !== "blg6" || parts[1] === "" || parts[2] === "") throw new Error("language release blocked: malformed release token");
67812
- const encoded = parts[1];
67813
- let supplied;
67814
- let payload;
67815
- try {
67816
- supplied = Buffer.from(parts[2], "base64url");
67817
- payload = JSON.parse(Buffer.from(encoded, "base64url").toString("utf8"));
67818
- } catch {
67819
- throw new Error("language release blocked: malformed release token");
67820
- }
67821
- const expected = createHmac("sha256", key).update(encoded).digest();
67822
- if (supplied.length !== expected.length || !timingSafeEqual(supplied, expected)) throw new Error("language release blocked: invalid release token signature");
67823
- if (payload === null || typeof payload !== "object" || Array.isArray(payload)) throw new Error("language release blocked: malformed release payload");
67824
- return payload;
67825
- }
67826
- function assertEqual(actual, expected, field) {
67827
- if (actual !== expected) throw new Error(`language release blocked: ${field} mismatch`);
67828
- }
67829
- function verifyTelegramLanguageRelease(text, releaseToken, options) {
67830
- if (typeof releaseToken !== "string" || releaseToken.length === 0) throw new Error("language release blocked: release token required");
67831
- if (options.expectedVersion.trim().length === 0) throw new Error("language release blocked: expected guard version required");
67832
- assertExpectedScript(text, options.locale);
67833
- const payload = parseAndVerifySignature(releaseToken, options.key);
67834
- const now = Math.floor(options.now ?? Date.now() / 1e3);
67835
- if (!Number.isSafeInteger(payload.iat) || !Number.isSafeInteger(payload.exp)) throw new Error("language release blocked: invalid receipt lifetime");
67836
- const issuedAt = payload.iat;
67837
- const expiresAt = payload.exp;
67838
- if (expiresAt < now) throw new Error("language release blocked: release token expired");
67839
- if (issuedAt > now + 60 || expiresAt < issuedAt || expiresAt - issuedAt > 604800) throw new Error("language release blocked: invalid receipt lifetime");
67840
- if (typeof payload.nonce !== "string" || payload.nonce.length === 0) throw new Error("language release blocked: invalid receipt nonce");
67841
- assertEqual(payload.source_sha256, canonicalHash(""), "source");
67842
- assertEqual(payload.target_sha256, canonicalHash(text), "target");
67843
- if (payload.language !== options.locale) throw new Error(`language release blocked: language mismatch (expected ${options.locale})`);
67844
- assertEqual(payload.purpose, "response", "purpose");
67845
- assertEqual(payload.content_type, "prose", "content type");
67846
- assertEqual(payload.short_text_reviewed, false, "short text review");
67847
- assertEqual(payload.v, options.expectedVersion, "version");
67848
- }
67849
- function isLanguageReleaseRequired() {
67850
- return process.env["BLUN_TELEGRAM_LANGUAGE_GATE"]?.trim().toLowerCase() === "required";
67851
- }
67852
- function requiredSetting(name) {
67853
- const value = process.env[name]?.trim();
67854
- if (value === void 0 || value.length === 0) throw new Error(`language release blocked: host setting ${name} required`);
67855
- return value;
67856
- }
67857
- function telegramLanguageReleaseLocale() {
67858
- if (!isLanguageReleaseRequired()) return void 0;
67859
- const locale = requiredSetting("BLUN_TELEGRAM_OUTPUT_LOCALE");
67860
- allowedScriptMatchers(locale);
67861
- return locale;
67862
- }
67863
- function defaultKeyFile() {
67864
- const configuredHome = process.env["BLUN_HOME"]?.trim();
67865
- return join(configuredHome !== void 0 && configuredHome.length > 0 ? configuredHome : join(homedir(), ".blun"), "secrets", "blun-language-guard-signing.key");
67866
- }
67867
- function assertTelegramLanguageReleased(text, releaseToken, now) {
67868
- if (!isLanguageReleaseRequired()) return;
67869
- if (typeof releaseToken !== "string" || releaseToken.length === 0) throw new Error("language release blocked: release token required");
67870
- const locale = telegramLanguageReleaseLocale();
67871
- if (locale === void 0) throw new Error("language release blocked: exact output locale required");
67872
- const keyFile = process.env["BLUN_LANGUAGE_GUARD_KEY_FILE"]?.trim() || defaultKeyFile();
67873
- let key;
67874
- try {
67875
- key = readFileSync(keyFile);
67876
- } catch {
67877
- throw new Error("language release blocked: verification key unavailable");
67878
- }
67879
- verifyTelegramLanguageRelease(text, releaseToken, {
67880
- locale,
67881
- expectedVersion: requiredSetting("BLUN_LANGUAGE_GUARD_VERSION"),
67882
- key,
67883
- now
67884
- });
67885
- }
67886
- //#endregion
67887
67690
  //#region src/noise.ts
67888
67691
  /**
67889
67692
  * Conservative outbound guard for group acknowledgements and narrated silence.
@@ -67895,9 +67698,12 @@ function assertTelegramLanguageReleased(text, releaseToken, now) {
67895
67698
  function isGroupChat(chatId) {
67896
67699
  return chatId.startsWith("-");
67897
67700
  }
67898
- const SILENT_TURN_MARKER = "[[BLUN_TELEGRAM_SILENT]]";
67899
- function isSilentTurnMarker(text) {
67900
- return text.trim() === SILENT_TURN_MARKER;
67701
+ function stripTrailingSilentTurnMarker(text) {
67702
+ const trimmed = text.trimEnd();
67703
+ if (!trimmed.endsWith("[[BLUN_TELEGRAM_SILENT]]")) return text;
67704
+ const prefix = trimmed.slice(0, -24);
67705
+ if (prefix.length > 0 && !/\s$/u.test(prefix)) return text;
67706
+ return prefix.trimEnd();
67901
67707
  }
67902
67708
  function normalize(s) {
67903
67709
  return s.toLowerCase().replace(/ä/g, "a").replace(/ö/g, "o").replace(/ü/g, "u").replace(/ß/g, "ss").replace(/[‐-―]/g, " ").replace(/[^\p{L}\p{N}\s]/gu, " ").replace(/\s+/g, " ").trim();
@@ -67907,6 +67713,7 @@ const EMPTY_CLAUSES = [
67907
67713
  /^(bin da|bin hier|hier bin ich|ich bin da|was brauchst du|was gibt es|was liegt an|wie kann ich helfen|bereit|ich warte|warte auf|melde mich|freut mich|here|standing by|ready|awaiting|what do you need)$/,
67908
67714
  /^(keine (gruppen ?)?antwort|gar keine antwort|keine reaktion|kein wort|no ?action|no (response|reply|answer|comment))( (notig|erforderlich|needed|required))?$/,
67909
67715
  /^(ich )?(halte mich raus|(bin|bleibe) (jetzt )?still|schweige|habe nichts zu melden)$/,
67716
+ /^ich best(?:a|ae|ä)tige nichts und beende still$/u,
67910
67717
  /^(stille|still|silence|nichts (zu tun|zu melden|fur mich|fuer mich)|nicht meine lane)$/,
67911
67718
  /^(kein handlungsbedarf|es gibt keinen handlungsbedarf|hier besteht kein handlungsbedarf)( (fur|fuer) mich)?$/,
67912
67719
  /^(i (am |will be )?)?(staying (silent|out|quiet)|keeping (quiet|out)|holding back)$/,
@@ -67935,6 +67742,40 @@ function isGroupNoiseReply(text) {
67935
67742
  return text.split(/[.!?;,\r\n]+|\s+[-\u2013\u2014]\s+/u).map(normalize).filter(Boolean).every((clause) => EMPTY_CLAUSES.some((pattern) => pattern.test(clause)) || NOTIFICATION_CONTEXT.test(clause));
67936
67743
  }
67937
67744
  //#endregion
67745
+ //#region src/outbox-reader.ts
67746
+ /** Read a byte suffix from one outbox descriptor, never the preceding history. */
67747
+ function readOutboxSuffix(start) {
67748
+ if (typeof start === "number" && (!Number.isSafeInteger(start) || start < 0)) return void 0;
67749
+ let fd;
67750
+ try {
67751
+ fd = openSync(outboxLog(), "r");
67752
+ const stat = fstatSync(fd);
67753
+ if (!stat.isFile()) return void 0;
67754
+ const end = stat.size;
67755
+ const offset = typeof start === "number" ? start : start(end);
67756
+ if (!Number.isSafeInteger(offset) || offset < 0) return void 0;
67757
+ if (offset >= end) return "";
67758
+ const buffer = Buffer.allocUnsafe(Math.min(64 * 1024, end - offset));
67759
+ const decoder = new StringDecoder("utf8");
67760
+ const chunks = [];
67761
+ let position = offset;
67762
+ while (position < end) {
67763
+ const count = readSync(fd, buffer, 0, Math.min(buffer.length, end - position), position);
67764
+ if (count === 0) break;
67765
+ position += count;
67766
+ chunks.push(decoder.write(buffer.subarray(0, count)));
67767
+ }
67768
+ chunks.push(decoder.end());
67769
+ return chunks.join("");
67770
+ } catch {
67771
+ return;
67772
+ } finally {
67773
+ if (fd !== void 0) try {
67774
+ closeSync(fd);
67775
+ } catch {}
67776
+ }
67777
+ }
67778
+ //#endregion
67938
67779
  //#region src/acp-session.ts
67939
67780
  /**
67940
67781
  * The King side of the bridge: spawns `<node> <main.mjs> acp`, drives it as
@@ -68028,8 +67869,8 @@ function outboxMarker() {
68028
67869
  /** True when the reply tool appended an outbox entry for this chat since `marker`. */
68029
67870
  function outboxGrewForChat(marker, chatId, threadId) {
68030
67871
  try {
68031
- if (statSync(outboxLog()).size <= marker) return false;
68032
- const fresh = readFileSync(outboxLog(), "utf8").slice(marker);
67872
+ const fresh = readOutboxSuffix(marker);
67873
+ if (fresh === void 0 || fresh.length === 0) return false;
68033
67874
  for (const line of fresh.split(/\r?\n/)) {
68034
67875
  if (line.trim().length === 0) continue;
68035
67876
  try {
@@ -68055,7 +67896,8 @@ function scrubSecrets(text) {
68055
67896
  * log it to outbox.jsonl as kind "reply-fallback". Returns true on success.
68056
67897
  */
68057
67898
  async function sendReplyFallback(chatId, rawText, threadId) {
68058
- if (rawText.trim().length === 0 || isSilentTurnMarker(rawText)) return true;
67899
+ const cleanText = stripTrailingSilentTurnMarker(rawText);
67900
+ if (cleanText.trim().length === 0) return true;
68059
67901
  let topic;
68060
67902
  try {
68061
67903
  topic = threadId === void 0 ? {} : { message_thread_id: telegramThreadId(threadId) };
@@ -68064,14 +67906,13 @@ async function sendReplyFallback(chatId, rawText, threadId) {
68064
67906
  }
68065
67907
  const token = botToken();
68066
67908
  if (token === void 0) return false;
68067
- if (isGroupChat(chatId) && isGroupNoiseReply(rawText)) {
67909
+ if (isGroupChat(chatId) && isGroupNoiseReply(cleanText)) {
68068
67910
  logLine(`reply-fallback suppressed (group meta-noise): ${rawText.slice(0, 60)}`);
68069
67911
  return true;
68070
67912
  }
68071
- const text = scrubSecrets(rawText);
67913
+ const text = scrubSecrets(cleanText);
68072
67914
  const body = text.length > TELEGRAM_TEXT_LIMIT ? `${text.slice(0, TELEGRAM_TEXT_LIMIT - 1)}…` : text;
68073
67915
  try {
68074
- assertTelegramLanguageReleased(body, void 0);
68075
67916
  const payload = await (await fetch(`https://api.telegram.org/bot${token}/sendMessage`, {
68076
67917
  method: "POST",
68077
67918
  headers: { "Content-Type": "application/json" },
@@ -1,9 +1,10 @@
1
1
  import { createRequire } from "node:module";
2
2
  import process$1 from "node:process";
3
- import { appendFileSync, chmodSync, mkdirSync, readFileSync, realpathSync, renameSync, statSync, writeFileSync } from "node:fs";
3
+ import { appendFileSync, chmodSync, closeSync, fstatSync, mkdirSync, openSync, readFileSync, readSync, realpathSync, renameSync, statSync, writeFileSync } from "node:fs";
4
4
  import { extname, join, sep } from "node:path";
5
5
  import { homedir } from "node:os";
6
6
  import { createHash, createHmac, timingSafeEqual } from "node:crypto";
7
+ import { StringDecoder } from "node:string_decoder";
7
8
  //#region \0rolldown/runtime.js
8
9
  var __create = Object.create;
9
10
  var __defProp = Object.defineProperty;
@@ -55633,10 +55634,10 @@ var require_internal = /* @__PURE__ */ __commonJSMin(((exports, module) => {
55633
55634
  }
55634
55635
  InternalCodec.prototype.encoder = InternalEncoder;
55635
55636
  InternalCodec.prototype.decoder = InternalDecoder;
55636
- var StringDecoder = __require("string_decoder").StringDecoder;
55637
- if (!StringDecoder.prototype.end) StringDecoder.prototype.end = function() {};
55637
+ var StringDecoder$1 = __require("string_decoder").StringDecoder;
55638
+ if (!StringDecoder$1.prototype.end) StringDecoder$1.prototype.end = function() {};
55638
55639
  function InternalDecoder(options, codec) {
55639
- this.decoder = new StringDecoder(codec.enc);
55640
+ this.decoder = new StringDecoder$1(codec.enc);
55640
55641
  }
55641
55642
  InternalDecoder.prototype.write = function(buf) {
55642
55643
  if (!Buffer.isBuffer(buf)) buf = Buffer.from(buf);
@@ -73449,6 +73450,10 @@ function assertTelegramLanguageReleased(text, releaseToken, now) {
73449
73450
  now
73450
73451
  });
73451
73452
  }
73453
+ function assertOptionalTelegramLanguageRelease(text, releaseToken, now) {
73454
+ if (releaseToken === void 0) return;
73455
+ assertTelegramLanguageReleased(text, releaseToken, now);
73456
+ }
73452
73457
  //#endregion
73453
73458
  //#region src/noise.ts
73454
73459
  /**
@@ -73465,6 +73470,13 @@ const SILENT_TURN_MARKER = "[[BLUN_TELEGRAM_SILENT]]";
73465
73470
  function isSilentTurnMarker(text) {
73466
73471
  return text.trim() === SILENT_TURN_MARKER;
73467
73472
  }
73473
+ function stripTrailingSilentTurnMarker(text) {
73474
+ const trimmed = text.trimEnd();
73475
+ if (!trimmed.endsWith("[[BLUN_TELEGRAM_SILENT]]")) return text;
73476
+ const prefix = trimmed.slice(0, -24);
73477
+ if (prefix.length > 0 && !/\s$/u.test(prefix)) return text;
73478
+ return prefix.trimEnd();
73479
+ }
73468
73480
  function normalize(s) {
73469
73481
  return s.toLowerCase().replace(/ä/g, "a").replace(/ö/g, "o").replace(/ü/g, "u").replace(/ß/g, "ss").replace(/[‐-―]/g, " ").replace(/[^\p{L}\p{N}\s]/gu, " ").replace(/\s+/g, " ").trim();
73470
73482
  }
@@ -73473,6 +73485,7 @@ const EMPTY_CLAUSES = [
73473
73485
  /^(bin da|bin hier|hier bin ich|ich bin da|was brauchst du|was gibt es|was liegt an|wie kann ich helfen|bereit|ich warte|warte auf|melde mich|freut mich|here|standing by|ready|awaiting|what do you need)$/,
73474
73486
  /^(keine (gruppen ?)?antwort|gar keine antwort|keine reaktion|kein wort|no ?action|no (response|reply|answer|comment))( (notig|erforderlich|needed|required))?$/,
73475
73487
  /^(ich )?(halte mich raus|(bin|bleibe) (jetzt )?still|schweige|habe nichts zu melden)$/,
73488
+ /^ich best(?:a|ae|ä)tige nichts und beende still$/u,
73476
73489
  /^(stille|still|silence|nichts (zu tun|zu melden|fur mich|fuer mich)|nicht meine lane)$/,
73477
73490
  /^(kein handlungsbedarf|es gibt keinen handlungsbedarf|hier besteht kein handlungsbedarf)( (fur|fuer) mich)?$/,
73478
73491
  /^(i (am |will be )?)?(staying (silent|out|quiet)|keeping (quiet|out)|holding back)$/,
@@ -73501,6 +73514,40 @@ function isGroupNoiseReply(text) {
73501
73514
  return text.split(/[.!?;,\r\n]+|\s+[-\u2013\u2014]\s+/u).map(normalize).filter(Boolean).every((clause) => EMPTY_CLAUSES.some((pattern) => pattern.test(clause)) || NOTIFICATION_CONTEXT.test(clause));
73502
73515
  }
73503
73516
  //#endregion
73517
+ //#region src/outbox-reader.ts
73518
+ /** Read a byte suffix from one outbox descriptor, never the preceding history. */
73519
+ function readOutboxSuffix(start) {
73520
+ if (typeof start === "number" && (!Number.isSafeInteger(start) || start < 0)) return void 0;
73521
+ let fd;
73522
+ try {
73523
+ fd = openSync(outboxLog(), "r");
73524
+ const stat = fstatSync(fd);
73525
+ if (!stat.isFile()) return void 0;
73526
+ const end = stat.size;
73527
+ const offset = typeof start === "number" ? start : start(end);
73528
+ if (!Number.isSafeInteger(offset) || offset < 0) return void 0;
73529
+ if (offset >= end) return "";
73530
+ const buffer = Buffer.allocUnsafe(Math.min(64 * 1024, end - offset));
73531
+ const decoder = new StringDecoder("utf8");
73532
+ const chunks = [];
73533
+ let position = offset;
73534
+ while (position < end) {
73535
+ const count = readSync(fd, buffer, 0, Math.min(buffer.length, end - position), position);
73536
+ if (count === 0) break;
73537
+ position += count;
73538
+ chunks.push(decoder.write(buffer.subarray(0, count)));
73539
+ }
73540
+ chunks.push(decoder.end());
73541
+ return chunks.join("");
73542
+ } catch {
73543
+ return;
73544
+ } finally {
73545
+ if (fd !== void 0) try {
73546
+ closeSync(fd);
73547
+ } catch {}
73548
+ }
73549
+ }
73550
+ //#endregion
73504
73551
  //#region src/inbound.ts
73505
73552
  function telegramThreadId(value) {
73506
73553
  if (value === void 0) return void 0;
@@ -73528,7 +73575,7 @@ const TOOL_DEFINITIONS = [
73528
73575
  text: { type: "string" },
73529
73576
  release_token: {
73530
73577
  type: "string",
73531
- description: "Language Guard receipt for this exact text. Required when the host enables the language release gate."
73578
+ description: "Optional Language Guard receipt for released text. When provided, it must match this exact text."
73532
73579
  },
73533
73580
  reply_to: {
73534
73581
  type: "string",
@@ -73592,7 +73639,7 @@ const TOOL_DEFINITIONS = [
73592
73639
  text: { type: "string" },
73593
73640
  release_token: {
73594
73641
  type: "string",
73595
- description: "Language Guard receipt for this exact text. Required when the host enables the language release gate."
73642
+ description: "Optional Language Guard receipt for released text. When provided, it must match this exact text."
73596
73643
  },
73597
73644
  format: {
73598
73645
  type: "string",
@@ -73621,8 +73668,8 @@ async function runTool(api, token, name, args) {
73621
73668
  case "download_attachment": return runDownload(api, token, args["file_id"]);
73622
73669
  case "edit_message": {
73623
73670
  const chatId = resolveAllowedChatId(args["chat_id"]);
73624
- if (isSilentTurnMarker(args["text"])) throw new Error("silent marker cannot be used to edit a Telegram message");
73625
- assertTelegramLanguageReleased(args["text"], args["release_token"]);
73671
+ if (stripTrailingSilentTurnMarker(args["text"]) !== args["text"] || isSilentTurnMarker(args["text"])) throw new Error("silent marker cannot be used to edit a Telegram message");
73672
+ assertOptionalTelegramLanguageRelease(args["text"], args["release_token"]);
73626
73673
  const parseMode = args["format"] === "markdownv2" ? "MarkdownV2" : void 0;
73627
73674
  const edited = await api.editMessageText(chatId, Number(args["message_id"]), args["text"], parseMode !== void 0 ? { parse_mode: parseMode } : void 0);
73628
73675
  const id = typeof edited === "object" ? edited.message_id : args["message_id"];
@@ -73646,13 +73693,10 @@ async function runTool(api, token, name, args) {
73646
73693
  * nicht weiter retryt. Datei-Anhänge sind nie betroffen.
73647
73694
  */
73648
73695
  const DUPLICATE_WINDOW_MS = 3e5;
73649
- const OUTBOX_TAIL_BYTES = 32768;
73696
+ const OUTBOX_TAIL_CHARACTERS = 32768;
73650
73697
  function isRecentDuplicate(chatId, text, threadId, replyTo) {
73651
73698
  try {
73652
- const file = outboxLog();
73653
- const size = statSync(file).size;
73654
- const raw = readFileSync(file, "utf8");
73655
- const tail = size > OUTBOX_TAIL_BYTES ? raw.slice(raw.length - OUTBOX_TAIL_BYTES) : raw;
73699
+ const tail = readOutboxSuffix((size) => Math.max(0, size - 4 * OUTBOX_TAIL_CHARACTERS))?.slice(-OUTBOX_TAIL_CHARACTERS) ?? "";
73656
73700
  const wanted = text.trim();
73657
73701
  const cutoff = Date.now() - DUPLICATE_WINDOW_MS;
73658
73702
  for (const line of tail.split("\n")) {
@@ -73681,6 +73725,10 @@ async function runReply(api, args) {
73681
73725
  if (files.length > 0) throw new Error("silent marker cannot accompany files");
73682
73726
  return "suppressed: silent marker - not sent";
73683
73727
  }
73728
+ if (stripTrailingSilentTurnMarker(text) !== text) {
73729
+ if (files.length === 0 && isGroupChat(chatId) && isGroupNoiseReply(stripTrailingSilentTurnMarker(text))) return "suppressed: group meta/acknowledgement noise - not sent";
73730
+ throw new Error("silent marker cannot accompany Telegram reply text or files; resend the reply without it");
73731
+ }
73684
73732
  if (files.length === 0 && isGroupChat(chatId) && isGroupNoiseReply(text)) {
73685
73733
  appendJsonl(outboxLog(), {
73686
73734
  direction: "out",
@@ -73701,7 +73749,7 @@ async function runReply(api, args) {
73701
73749
  });
73702
73750
  return "already delivered: an identical message was sent to this chat moments ago — do NOT send it again";
73703
73751
  }
73704
- assertTelegramLanguageReleased(text, args["release_token"]);
73752
+ assertOptionalTelegramLanguageRelease(text, args["release_token"]);
73705
73753
  for (const file of files) {
73706
73754
  assertSendable(file);
73707
73755
  const info = statSync(file);
@@ -13,6 +13,13 @@ const SILENT_TURN_MARKER = "[[BLUN_TELEGRAM_SILENT]]";
13
13
  function isSilentTurnMarker(text) {
14
14
  return text.trim() === SILENT_TURN_MARKER;
15
15
  }
16
+ function stripTrailingSilentTurnMarker(text) {
17
+ const trimmed = text.trimEnd();
18
+ if (!trimmed.endsWith("[[BLUN_TELEGRAM_SILENT]]")) return text;
19
+ const prefix = trimmed.slice(0, -24);
20
+ if (prefix.length > 0 && !/\s$/u.test(prefix)) return text;
21
+ return prefix.trimEnd();
22
+ }
16
23
  function normalize(s) {
17
24
  return s.toLowerCase().replace(/ä/g, "a").replace(/ö/g, "o").replace(/ü/g, "u").replace(/ß/g, "ss").replace(/[‐-―]/g, " ").replace(/[^\p{L}\p{N}\s]/gu, " ").replace(/\s+/g, " ").trim();
18
25
  }
@@ -21,6 +28,7 @@ const EMPTY_CLAUSES = [
21
28
  /^(bin da|bin hier|hier bin ich|ich bin da|was brauchst du|was gibt es|was liegt an|wie kann ich helfen|bereit|ich warte|warte auf|melde mich|freut mich|here|standing by|ready|awaiting|what do you need)$/,
22
29
  /^(keine (gruppen ?)?antwort|gar keine antwort|keine reaktion|kein wort|no ?action|no (response|reply|answer|comment))( (notig|erforderlich|needed|required))?$/,
23
30
  /^(ich )?(halte mich raus|(bin|bleibe) (jetzt )?still|schweige|habe nichts zu melden)$/,
31
+ /^ich best(?:a|ae|ä)tige nichts und beende still$/u,
24
32
  /^(stille|still|silence|nichts (zu tun|zu melden|fur mich|fuer mich)|nicht meine lane)$/,
25
33
  /^(kein handlungsbedarf|es gibt keinen handlungsbedarf|hier besteht kein handlungsbedarf)( (fur|fuer) mich)?$/,
26
34
  /^(i (am |will be )?)?(staying (silent|out|quiet)|keeping (quiet|out)|holding back)$/,
@@ -49,4 +57,4 @@ function isGroupNoiseReply(text) {
49
57
  return text.split(/[.!?;,\r\n]+|\s+[-\u2013\u2014]\s+/u).map(normalize).filter(Boolean).every((clause) => EMPTY_CLAUSES.some((pattern) => pattern.test(clause)) || NOTIFICATION_CONTEXT.test(clause));
50
58
  }
51
59
  //#endregion
52
- export { SILENT_TURN_MARKER, isGroupChat, isGroupNoiseReply, isSilentTurnMarker };
60
+ export { SILENT_TURN_MARKER, isGroupChat, isGroupNoiseReply, isSilentTurnMarker, stripTrailingSilentTurnMarker };