blun-king-cli 9.1.497 → 9.1.499

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,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## 9.1.499 - 2026-08-30
4
+
5
+ - Replaces image, audio, and video payloads older than the newest twelve conversation messages with short model-facing references while preserving the complete original session wire and every recent media attachment.
6
+ - Reduces Fredrik's measured repeated model input by 85,748 characters, approximately 21,437 estimated tokens per subsequent work step, without changing the current image, message text, or persisted history.
7
+ - Runs overdue TodoList maintenance as its own low-effort model step before the ninth work batch and exposes only TodoList during that step, preventing the normal workflow from hitting a visible red policy error first.
8
+ - Keeps fresh user steers ahead of TodoList housekeeping while preserving the hard policy gate as a fallback for an actually invalid update.
9
+
10
+ ## 9.1.498 - 2026-08-30
11
+
12
+ - Shows prompt-cache hit rate plus cache-read and cache-write tokens per model in `/usage`.
13
+ - Adds `/tokens` as a short alias for the existing detailed context doctor without introducing another model call.
14
+ - Adds `/offload` as an alias for the existing archive-backed context compaction command.
15
+ - Preserves every distinct authorized Telegram bot message instead of dropping same-sender messages that arrive within a fixed 15-second window.
16
+ - Preserves the exact TodoList through full compaction and adds a one-shot recovery reminder to read the archived history path before broad searches when an exact path, command, hash, or requirement is uncertain.
17
+ - Keeps completed TodoList evidence append-only: later uncertainty becomes a separate in-progress step instead of silently reverting or removing completed work.
18
+ - Defers recurring session-loop wakeups while the maintained TodoList still contains active or pending work, coalescing missed intervals until the assignment is genuinely idle without delaying unrelated scheduled reminders.
19
+
3
20
  ## 9.1.497 - 2026-08-30
4
21
 
5
22
  - Prioritizes Telegram bot messages from shared groups behind direct and explicitly addressed traffic but ahead of ordinary queued context, without forcing a reply.
package/LIESMICH.txt CHANGED
@@ -9,7 +9,7 @@ Installation
9
9
  ------------
10
10
  Die geprüfte Version exakt global installieren:
11
11
 
12
- npm install -g blun-king-cli@9.1.497
12
+ npm install -g blun-king-cli@9.1.499
13
13
 
14
14
  Start
15
15
  -----
@@ -65,6 +65,36 @@ geschriebenen Entwurf zu löschen. Das gilt auch bei
65
65
  Autovervollständigung, Geistervorschlägen, Bash-Eingabe und einer offenen
66
66
  Mehrzeileneingabe.
67
67
 
68
+ Version 9.1.499 ersetzt Bild-, Audio- und Videodaten ausserhalb der neuesten zwoelf
69
+ Gespraechsnachrichten ausschliesslich in der wiederholten Modellprojektion durch
70
+ kurze Verweise. Aktuelle Medien, Nachrichtentext und vollstaendiger Sitzungsrohverlauf
71
+ bleiben unveraendert. In Fredriks gemessenem Wiederaufnahme-Checkpoint spart der
72
+ Schnitt 85.748 Zeichen beziehungsweise rund 21.437 geschaetzte Eingabetoken je
73
+ weiterem Werkzeugschritt.
74
+
75
+ Faellige Todo-Pflege laeuft in 9.1.499 vor dem naechsten Sachwerkzeug als eigener
76
+ kleiner Modellschritt. In diesem Schritt bietet die Laufzeit ausschliesslich
77
+ TodoList an; erst nach einer gueltigen Aktualisierung werden Datei-, Such- und
78
+ Shell-Werkzeuge wieder freigegeben. Dadurch entsteht im normalen Ablauf kein
79
+ roter Zwischenfehler. Frische Benutzernachrichten behalten Vorrang.
80
+
81
+ Version 9.1.498 zeigt in /usage die gemessene Prompt-Cache-Trefferquote sowie
82
+ gelesene und geschriebene Cache-Token je Modell. /tokens ist ein kurzer Alias
83
+ fuer die bereits vorhandene detaillierte Kontextdiagnose; /offload ruft die
84
+ vorhandene archivierende Kontextverdichtung auf. Unterschiedliche autorisierte
85
+ Botnachrichten werden nicht mehr durch ein starres 15-Sekunden-Fenster verworfen.
86
+ Nach einer vollständigen Verdichtung bleibt die exakte Todo-Liste erhalten. Ein
87
+ einmaliger Wiederherstellungshinweis verlangt bei unsicheren Pfaden, Befehlen,
88
+ Hashes oder Anforderungen zuerst das Lesen des archivierten Verlaufs, bevor eine
89
+ breite Dateisuche oder ein Neubeginn zulässig ist.
90
+ Erledigte Todo-Schritte bleiben als Nachweis erhalten. Neue widersprüchliche
91
+ Erkenntnisse werden als eigener laufender Schritt mit Begründung ergänzt, statt
92
+ einen bereits belegten Abschluss still zurückzustufen oder zu entfernen.
93
+ Wiederkehrende Session-Loops warten, solange die gepflegte Todo-Liste noch
94
+ laufende oder ausstehende Arbeit enthält. Verpasste Intervalle werden bis zum
95
+ echten Leerlauf zusammengefasst; andere geplante Erinnerungen bleiben davon
96
+ unberührt.
97
+
68
98
  Version 9.1.497 priorisiert Telegram-Botnachrichten aus gemeinsamen Gruppen
69
99
  hinter privaten und ausdrücklich adressierten Nachrichten, aber vor normalem
70
100
  Kontext. Die Priorisierung erzwingt keine Antwort. Eine angenommene
package/README.md CHANGED
@@ -9,7 +9,7 @@ Voraussetzung ist Node.js 24.15 oder neuer. Die geprüfte Version wird exakt
9
9
  installiert:
10
10
 
11
11
  ```powershell
12
- npm install -g blun-king-cli@9.1.497
12
+ npm install -g blun-king-cli@9.1.499
13
13
  ```
14
14
 
15
15
  ## Reproduzierbares Staging und Packen
@@ -81,6 +81,36 @@ konkurrierenden Start zurückgewiesen und bleiben an der Spitze der
81
81
  FIFO-Warteschlange. Für diesen normalen Wartestatus erscheint kein
82
82
  `turn.agent_busy`-Fehler.
83
83
 
84
+ Version 9.1.499 ersetzt Bild-, Audio- und Videodaten außerhalb der neuesten zwölf
85
+ Gesprächsnachrichten ausschließlich in der wiederholten Modellprojektion durch
86
+ kurze Verweise. Aktuelle Medien, Nachrichtentext und vollständiger Sitzungsrohverlauf
87
+ bleiben unverändert. In Fredriks gemessenem Wiederaufnahme-Checkpoint spart der
88
+ Schnitt 85.748 Zeichen beziehungsweise rund 21.437 geschätzte Eingabetoken je
89
+ weiterem Werkzeugschritt.
90
+
91
+ Faellige Todo-Pflege laeuft in 9.1.499 vor dem naechsten Sachwerkzeug als eigener
92
+ kleiner Modellschritt. In diesem Schritt bietet die Laufzeit ausschliesslich
93
+ `TodoList` an; erst nach einer gueltigen Aktualisierung werden Datei-, Such- und
94
+ Shell-Werkzeuge wieder freigegeben. Dadurch entsteht im normalen Ablauf kein
95
+ roter Zwischenfehler. Frische Benutzernachrichten behalten Vorrang.
96
+
97
+ Version 9.1.498 zeigt in `/usage` die gemessene Prompt-Cache-Trefferquote sowie
98
+ gelesene und geschriebene Cache-Token je Modell. `/tokens` ist ein kurzer Alias
99
+ fuer die bereits vorhandene detaillierte Kontextdiagnose; `/offload` ruft die
100
+ vorhandene archivierende Kontextverdichtung auf. Unterschiedliche autorisierte
101
+ Botnachrichten werden nicht mehr durch ein starres 15-Sekunden-Fenster verworfen.
102
+ Nach einer vollständigen Verdichtung bleibt die exakte Todo-Liste erhalten. Ein
103
+ einmaliger Wiederherstellungshinweis verlangt bei unsicheren Pfaden, Befehlen,
104
+ Hashes oder Anforderungen zuerst das Lesen des archivierten Verlaufs, bevor eine
105
+ breite Dateisuche oder ein Neubeginn zulässig ist.
106
+ Erledigte Todo-Schritte bleiben als Nachweis erhalten. Neue widersprüchliche
107
+ Erkenntnisse werden als eigener laufender Schritt mit Begründung ergänzt, statt
108
+ einen bereits belegten Abschluss still zurückzustufen oder zu entfernen.
109
+ Wiederkehrende Session-Loops warten außerdem, solange die gepflegte Todo-Liste
110
+ noch laufende oder ausstehende Arbeit enthält. Verpasste Intervalle werden bis
111
+ zum echten Leerlauf zusammengefasst; andere geplante Erinnerungen bleiben davon
112
+ unberührt.
113
+
84
114
  Version 9.1.497 priorisiert Telegram-Botnachrichten aus gemeinsamen Gruppen
85
115
  hinter privaten und ausdrücklich adressierten Nachrichten, aber vor normalem
86
116
  Kontext. Die Priorisierung erzwingt keine Antwort. Eine angenommene
@@ -0,0 +1,48 @@
1
+ 'use strict';
2
+
3
+ const HISTORICAL_MEDIA_KEEP_RECENT_MESSAGES = 12;
4
+ const HISTORICAL_MEDIA_PLACEHOLDERS = Object.freeze({
5
+ image_url: '[historical image omitted from active model context; original remains in session wire]',
6
+ audio_url: '[historical audio omitted from active model context; original remains in session wire]',
7
+ video_url: '[historical video omitted from active model context; original remains in session wire]',
8
+ });
9
+
10
+ function projectHistoricalMediaParts(messages, options = {}) {
11
+ if (!Array.isArray(messages) || messages.length === 0) return messages;
12
+ const keepRecentMessages = nonNegativeInteger(
13
+ options.keepRecentMessages,
14
+ HISTORICAL_MEDIA_KEEP_RECENT_MESSAGES,
15
+ );
16
+ const historicalEnd = Math.max(0, messages.length - keepRecentMessages);
17
+ let changed = false;
18
+
19
+ const projected = messages.map((message, messageIndex) => {
20
+ if (messageIndex >= historicalEnd || !Array.isArray(message?.content)) return message;
21
+ let nextContent;
22
+ for (let partIndex = 0; partIndex < message.content.length; partIndex += 1) {
23
+ const part = message.content[partIndex];
24
+ const placeholder = HISTORICAL_MEDIA_PLACEHOLDERS[part?.type];
25
+ if (placeholder === undefined) {
26
+ nextContent?.push(part);
27
+ continue;
28
+ }
29
+ nextContent ??= message.content.slice(0, partIndex);
30
+ nextContent.push({ type: 'text', text: placeholder });
31
+ changed = true;
32
+ }
33
+ return nextContent === undefined ? message : { ...message, content: nextContent };
34
+ });
35
+
36
+ return changed ? projected : messages;
37
+ }
38
+
39
+ function nonNegativeInteger(value, fallback) {
40
+ const number = Number(value);
41
+ return Number.isInteger(number) && number >= 0 ? number : fallback;
42
+ }
43
+
44
+ module.exports = {
45
+ HISTORICAL_MEDIA_KEEP_RECENT_MESSAGES,
46
+ HISTORICAL_MEDIA_PLACEHOLDERS,
47
+ projectHistoricalMediaParts,
48
+ };
@@ -0,0 +1,26 @@
1
+ 'use strict';
2
+
3
+ function wholeNonNegative(value) {
4
+ if (!Number.isFinite(value) || value <= 0) return 0;
5
+ return Math.floor(value);
6
+ }
7
+
8
+ function buildCacheEfficiency(usage = {}) {
9
+ const uncachedTokens = wholeNonNegative(usage.inputOther);
10
+ const readTokens = wholeNonNegative(usage.inputCacheRead);
11
+ const writeTokens = wholeNonNegative(usage.inputCacheCreation);
12
+ const inputTokens = uncachedTokens + readTokens + writeTokens;
13
+
14
+ return {
15
+ hasActivity: readTokens > 0 || writeTokens > 0,
16
+ hitRate: inputTokens > 0 ? Math.min(1, readTokens / inputTokens) : 0,
17
+ inputTokens,
18
+ readTokens,
19
+ writeTokens,
20
+ };
21
+ }
22
+
23
+ module.exports = {
24
+ buildCacheEfficiency,
25
+ wholeNonNegative,
26
+ };
package/blun.mjs CHANGED
@@ -75375,6 +75375,7 @@ var init_full = __esmMin((() => {
75375
75375
  "permission_mode",
75376
75376
  "plan_mode",
75377
75377
  "plugin_session_start",
75378
+ "compaction_recovery",
75378
75379
  "todo_list_reminder",
75379
75380
  "tool-awareness"
75380
75381
  ]);
@@ -77053,6 +77054,7 @@ function createCronScheduler(opts) {
77053
77054
  const now = clocks.wallNow();
77054
77055
  try {
77055
77056
  for (const task of tasks) try {
77057
+ if (!isIdle(task)) continue;
77056
77058
  if (inFlight.has(task.id)) continue;
77057
77059
  if (task.paused === true) continue;
77058
77060
  const parsed = getParsed(task.cron);
@@ -77180,6 +77182,10 @@ var init_telemetry_events = __esmMin((() => {
77180
77182
  }));
77181
77183
  //#endregion
77182
77184
  //#region ../../packages/agent-core/src/agent/cron/manager.ts
77185
+ function hasUnfinishedTodoWork(agent) {
77186
+ const todos = agent.tools.storeData()["todo"];
77187
+ return Array.isArray(todos) && todos.some((todo) => todo?.status === "in_progress" || todo?.status === "pending");
77188
+ }
77183
77189
  var STALE_THRESHOLD_MS, CronManager;
77184
77190
  var init_manager$3 = __esmMin((() => {
77185
77191
  init_clock();
@@ -77239,7 +77245,7 @@ var init_manager$3 = __esmMin((() => {
77239
77245
  this.scheduler = createCronScheduler({
77240
77246
  clocks: this.clocks,
77241
77247
  source: () => this.store.list(),
77242
- isIdle: () => !agent.turn.hasActiveTurn,
77248
+ isIdle: (task) => !agent.turn.hasActiveTurn && (task?.owner !== "session-loop" || !hasUnfinishedTodoWork(agent)),
77243
77249
  isKilled: () => process.env["BLUN_DISABLE_CRON"] === "1",
77244
77250
  onFire: (task, ctx) => {
77245
77251
  this.handleFire(task, ctx);
@@ -79043,7 +79049,8 @@ var init_context$2 = __esmMin((() => {
79043
79049
  project(messages, options) {
79044
79050
  const anomalies = [];
79045
79051
  const userOffloaded = this.agent.userMessageOffload.compact(messages);
79046
- const historicalUserReferencesProjected = compactHistoricalPersistedUserMessageReferences(userOffloaded);
79052
+ const historicalMediaProjected = projectHistoricalMediaParts(userOffloaded);
79053
+ const historicalUserReferencesProjected = compactHistoricalPersistedUserMessageReferences(historicalMediaProjected);
79047
79054
  const duplicateUserMessagesProjected = dedupeRepeatedUserMessages(historicalUserReferencesProjected);
79048
79055
  const historicalTelegramProjected = projectHistoricalUnaddressedTelegramMessages(duplicateUserMessagesProjected);
79049
79056
  const assistantOffloaded = this.agent.assistantMessageOffload.compact(historicalTelegramProjected);
@@ -233121,7 +233128,7 @@ var init_mission_contract_bridge = __esmMin((() => {
233121
233128
  }));
233122
233129
  //#endregion
233123
233130
  //#region ../../packages/agent-core/src/agent/injection/manager.ts
233124
- var ACTIVE_BACKGROUND_TASK_GUIDANCE, InjectionManager;
233131
+ var ACTIVE_BACKGROUND_TASK_GUIDANCE, POST_COMPACTION_RECOVERY_GUIDANCE, InjectionManager;
233125
233132
  var init_manager$2 = __esmMin((() => {
233126
233133
  init_task_list();
233127
233134
  init_action_style();
@@ -233136,6 +233143,7 @@ var init_manager$2 = __esmMin((() => {
233136
233143
  init_validated_learning_signal();
233137
233144
  init_tool_awareness();
233138
233145
  ACTIVE_BACKGROUND_TASK_GUIDANCE = "The conversation was compacted, so the earlier messages that started these background tasks are gone — but the tasks are still running from before. Do not start duplicates. Use TaskOutput to fetch a task’s result, TaskList to list them, TaskUpdate to send additional instructions to a running agent task, and TaskStop to cancel one.";
233146
+ POST_COMPACTION_RECOVERY_GUIDANCE = "The conversation was compacted. Continue from the persisted TodoList and summary. If an exact file path, command, hash, requirement, or other working detail is missing or uncertain, use Read on the summary's history_path before broad filesystem searches, guessing, or starting work over.";
233139
233147
  InjectionManager = class {
233140
233148
  agent;
233141
233149
  injectors;
@@ -233193,9 +233201,16 @@ var init_manager$2 = __esmMin((() => {
233193
233201
  }
233194
233202
  async injectAfterCompaction() {
233195
233203
  await this.injectGoal();
233204
+ this.injectCompactionRecoveryGuidance();
233196
233205
  this.injectActiveBackgroundTasks();
233197
233206
  await this.inject();
233198
233207
  }
233208
+ injectCompactionRecoveryGuidance() {
233209
+ this.agent.context.appendSystemReminder(POST_COMPACTION_RECOVERY_GUIDANCE, {
233210
+ kind: "injection",
233211
+ variant: "compaction_recovery"
233212
+ });
233213
+ }
233199
233214
  /**
233200
233215
  * Post-compaction only: re-surface still-running background tasks. Folding the
233201
233216
  * live context to [recent user prompts, summary] drops the messages that
@@ -261265,7 +261280,7 @@ function renderPersistedUserMessage(text, outputPath) {
261265
261280
  createUserMessagePreview(text)
261266
261281
  ].join("\n");
261267
261282
  }
261268
- var USER_MESSAGE_MAX_CHARS, USER_MESSAGE_OFFLOAD_MARKER, shouldOffloadUserMessage, shouldOffloadHistoricalUserMessage, createUserMessagePreview, compactHistoricalPersistedUserMessageReferences;
261283
+ var USER_MESSAGE_MAX_CHARS, USER_MESSAGE_OFFLOAD_MARKER, shouldOffloadUserMessage, shouldOffloadHistoricalUserMessage, createUserMessagePreview, compactHistoricalPersistedUserMessageReferences, projectHistoricalMediaParts;
261269
261284
  var init_user_message_offload = __esmMin((() => {
261270
261285
  const policy = createRequire(import.meta.url)("./bin/user-message-offload-policy.cjs");
261271
261286
  USER_MESSAGE_MAX_CHARS = policy.USER_MESSAGE_MAX_CHARS;
@@ -261273,8 +261288,9 @@ var init_user_message_offload = __esmMin((() => {
261273
261288
  shouldOffloadUserMessage = policy.shouldOffloadUserMessage;
261274
261289
  shouldOffloadHistoricalUserMessage = policy.shouldOffloadHistoricalUserMessage;
261275
261290
  createUserMessagePreview = policy.createUserMessagePreview;
261276
- compactHistoricalPersistedUserMessageReferences = policy.compactHistoricalPersistedUserMessageReferences;
261277
- }));
261291
+ compactHistoricalPersistedUserMessageReferences = policy.compactHistoricalPersistedUserMessageReferences;
261292
+ ({ projectHistoricalMediaParts } = createRequire(import.meta.url)("./bin/historical-media-projection-policy.cjs"));
261293
+ }));
261278
261294
  var UserMessageOffload = class {
261279
261295
  agent;
261280
261296
  replacements = /* @__PURE__ */ new Map();
@@ -262770,6 +262786,24 @@ var init_turn = __esmMin((() => {
262770
262786
  currentStepHadFailure = false;
262771
262787
  const pendingSteers = this.matchingSteers(turnId);
262772
262788
  if (pendingSteers.length === 0) {
262789
+ const todoMaintenanceMode = goalTodoMaintenanceMode(this.agent);
262790
+ if (todoMaintenanceMode !== null) {
262791
+ const todoTool = eligibleTools.find((tool) => tool.name === "TodoList");
262792
+ if (todoTool === void 0) return {
262793
+ block: true,
262794
+ reason: "TodoList maintenance is due, but the TodoList tool is unavailable."
262795
+ };
262796
+ this.agent.log.info("proactive TodoList maintenance step", {
262797
+ stepNumber,
262798
+ mode: todoMaintenanceMode,
262799
+ workCallsSinceRefresh: this.agent.goalTodoPolicyState?.workCallsSinceRefresh ?? 0
262800
+ });
262801
+ const todoSystemPrompt = buildGoalTodoMaintenanceSystemPrompt(this.agent, turnSystemPrompt ?? this.agent.effectiveSystemPrompt, todoMaintenanceMode);
262802
+ return {
262803
+ llm: this.agent.llmForTurn("low", todoSystemPrompt),
262804
+ tools: [todoTool]
262805
+ };
262806
+ }
262773
262807
  if (workStepThinkingEffort === void 0) return;
262774
262808
  this.agent.log.info("work step thinking effort", {
262775
262809
  stepNumber,
@@ -403833,7 +403867,7 @@ const BUILTIN_SLASH_COMMAND_DEFINITIONS = [
403833
403867
  },
403834
403868
  {
403835
403869
  name: "compact",
403836
- aliases: [],
403870
+ aliases: ["offload"],
403837
403871
  descriptionKey: "command.compact.description",
403838
403872
  priority: 80,
403839
403873
  argumentHint: "<instruction>"
@@ -403976,7 +404010,7 @@ const BUILTIN_SLASH_COMMAND_DEFINITIONS = [
403976
404010
  },
403977
404011
  {
403978
404012
  name: "context-doctor",
403979
- aliases: [],
404013
+ aliases: ["tokens"],
403980
404014
  descriptionKey: "contextDoctor.description",
403981
404015
  priority: 66,
403982
404016
  availability: "always"
@@ -417895,9 +417929,11 @@ function buildSessionUsageSection(usage, error, value, muted, errorStyle) {
417895
417929
  for (const [model, row] of entries) {
417896
417930
  const input = usageInputTotal$2(row);
417897
417931
  const output = usageNumber(row.output);
417932
+ const cache = buildCacheEfficiency(row);
417898
417933
  totalInput += input;
417899
417934
  totalOutput += output;
417900
417935
  lines.push(` ${muted(sanitizeTerminalText(model))} ${uiText("usage.input")} ${value(formatExactTokenCount(input, locale))} ${uiText("usage.output")} ${value(formatExactTokenCount(output, locale))} ${uiText("usage.total")} ${value(formatExactTokenCount(input + output, locale))}`);
417936
+ if (cache.hasActivity) lines.push(` ${muted("CACHE")} ${value(`${Math.round(cache.hitRate * 100)}%`)} | R ${value(formatExactTokenCount(cache.readTokens, locale))} | W ${value(formatExactTokenCount(cache.writeTokens, locale))}`);
417901
417937
  }
417902
417938
  if (entries.length > 1) lines.push(` ${muted(uiText("usage.total"))} ${uiText("usage.input")} ${value(formatExactTokenCount(totalInput, locale))} ${uiText("usage.output")} ${value(formatExactTokenCount(totalOutput, locale))} ${uiText("usage.total")} ${value(formatExactTokenCount(totalInput + totalOutput, locale))}`);
417903
417939
  return lines;
@@ -417964,6 +418000,7 @@ var { buildApprovalRejectionStop } = createRequire(import.meta.url)("./bin/appro
417964
418000
  var { reconcileContextBudget } = createRequire(import.meta.url)("./bin/context-budget-ledger.cjs");
417965
418001
  var { buildContextInsight } = createRequire(import.meta.url)("./bin/context-insight-policy.cjs");
417966
418002
  var { buildContextDoctor } = createRequire(import.meta.url)("./bin/context-doctor-policy.cjs");
418003
+ var { buildCacheEfficiency } = createRequire(import.meta.url)("./bin/usage-cache-efficiency-policy.cjs");
417967
418004
  var { buildValidatedLearningInsight } = createRequire(import.meta.url)("./bin/validated-learning-insight-policy.cjs");
417968
418005
  var { readValidatedLearningCandidates } = createRequire(import.meta.url)("./bin/validated-learning-signal.cjs");
417969
418006
  function buildUsageReportLines(options) {
@@ -423177,6 +423214,7 @@ async function handleGoalCommand(host, args) {
423177
423214
  }
423178
423215
  const IDEA_CONTRACT_MARKER = "Work as a self-directing employee:";
423179
423216
  const IDEA_ALLOWED_CHANNELS = Object.freeze([]);
423217
+ const GOAL_TODO_REFRESH_WORK_CALL_LIMIT = 8;
423180
423218
  const IDEA_TERMINAL_TODO_STATUSES = Object.freeze([
423181
423219
  "done",
423182
423220
  "blocked",
@@ -423204,6 +423242,30 @@ function validInitialIdeaPlan(value) {
423204
423242
  }
423205
423243
  return active === 1;
423206
423244
  }
423245
+ function goalTodoMaintenanceMode(agent) {
423246
+ if (isIdeaGoal(agent)) return null;
423247
+ const todos = ideaTodos(agent);
423248
+ if (todos.length === 0) return agent.goal.getActiveGoal() === null ? null : "initial";
423249
+ const progress = agent.goalTodoPolicyState ??= {
423250
+ workCallsSinceRefresh: 0,
423251
+ refreshRequired: false
423252
+ };
423253
+ return progress.refreshRequired || progress.workCallsSinceRefresh >= GOAL_TODO_REFRESH_WORK_CALL_LIMIT ? "refresh" : null;
423254
+ }
423255
+ function buildGoalTodoMaintenanceSystemPrompt(agent, basePrompt, mode) {
423256
+ const todos = ideaTodos(agent);
423257
+ const visibleTodoList = todos.length === 0 ? "The visible TodoList is empty." : ["Current visible TodoList:", ...todos.map((todo) => `- [${String(todo?.status ?? "pending")}] ${String(todo?.title ?? "").trim()}`)].join("\n");
423258
+ const action = mode === "initial" ? "Create the visible task-specific TodoList now. Keep exactly one item in_progress and every later item pending." : "Update the visible TodoList now, before any more task work. Mark only evidenced steps done, keep the actual current step in_progress, and leave later steps pending. If the current title is stale, refine it to the concrete verified substep.";
423259
+ return `<todo-maintenance>
423260
+ TODO MAINTENANCE IS THE ONLY ACTION FOR THIS STEP.
423261
+ ${action}
423262
+ Call TodoList as the first and only tool. Do not call a work, search, file, shell, web, messaging, or goal tool in this step. Do not merely describe the update.
423263
+
423264
+ ${visibleTodoList}
423265
+ </todo-maintenance>
423266
+
423267
+ ${basePrompt}`;
423268
+ }
423207
423269
  function enforceGoalTodoPolicy(agent, context) {
423208
423270
  if (isIdeaGoal(agent)) return;
423209
423271
  if (TELEGRAM_DELIVERY_TOOL_RE.test(String(context.toolCall.name ?? ""))) return;
@@ -423231,6 +423293,12 @@ function enforceGoalTodoPolicy(agent, context) {
423231
423293
  if (context.toolCall.name === "TodoList") {
423232
423294
  const nextTodos = context.args?.todos;
423233
423295
  if (!Array.isArray(nextTodos)) return;
423296
+ const normalizeTitle = (value) => String(value ?? "").trim().toLowerCase().replace(/\s+/gu, " ");
423297
+ const missingCompleted = todos.filter((todo, index) => todo?.status === "done" && !nextTodos.some((nextTodo) => nextTodo?.status === "done" && normalizeTitle(nextTodo.title) === normalizeTitle(todo.title)) && nextTodos[index]?.status !== "done");
423298
+ if (missingCompleted.length > 0) return {
423299
+ block: true,
423300
+ reason: `Completed TodoList evidence is append-only. Keep each completed item present and done. If later evidence reopens work, retain the completed item and add a separate in_progress item with the reason. Missing completed item: ${String(missingCompleted[0]?.title ?? "").trim()}`
423301
+ };
423234
423302
  if (nextTodos.length === 0) {
423235
423303
  if (activeGoal !== null) return {
423236
423304
  block: true,
@@ -423272,7 +423340,7 @@ function enforceGoalTodoPolicy(agent, context) {
423272
423340
  }
423273
423341
  if (context.toolCall.id === context.toolCalls[0]?.id) {
423274
423342
  progress.workCallsSinceRefresh += 1;
423275
- if (progress.workCallsSinceRefresh > 8) progress.refreshRequired = true;
423343
+ if (progress.workCallsSinceRefresh > GOAL_TODO_REFRESH_WORK_CALL_LIMIT) progress.refreshRequired = true;
423276
423344
  }
423277
423345
  if (!progress.refreshRequired) return;
423278
423346
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blun-king-cli",
3
- "version": "9.1.497",
3
+ "version": "9.1.499",
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": {
@@ -130,25 +130,6 @@ function matchesAny(text, patterns) {
130
130
  } catch {}
131
131
  return false;
132
132
  }
133
- /**
134
- * Bot-to-bot loop guard (Telegram's own safety requirement for Bot API 10.0
135
- * bot-to-bot mode): once King can receive other bots' messages, an unthrottled
136
- * exchange could ping-pong forever. We throttle deliveries FROM a given bot in
137
- * a given group to at most one per BOT_COOLDOWN_MS, so two bots can never fast-
138
- * loop. In-memory per bridge process — resets on restart, which is fine.
139
- */
140
- const BOT_COOLDOWN_MS = 15e3;
141
- const lastBotDelivery = /* @__PURE__ */ new Map();
142
- /** Exposed for tests; drops stale entries so the map can't grow unbounded. */
143
- function botLoopGuardAllows(senderId, now = Date.now(), cooldownMs = BOT_COOLDOWN_MS) {
144
- const last = lastBotDelivery.get(senderId);
145
- if (last !== void 0 && now - last < cooldownMs) return false;
146
- lastBotDelivery.set(senderId, now);
147
- if (lastBotDelivery.size > 200) {
148
- for (const [key, ts] of lastBotDelivery) if (now - ts > cooldownMs) lastBotDelivery.delete(key);
149
- }
150
- return true;
151
- }
152
133
  function gate(ctx, botUsername) {
153
134
  const access = readAccess();
154
135
  if (pruneExpired(access)) saveAccess(access);
@@ -227,7 +208,6 @@ function gate(ctx, botUsername) {
227
208
  if (groupAllowFrom.length > 0 && !groupAllowFrom.includes(senderId)) return { action: "drop" };
228
209
  const mentionBypass = from.isBot !== true && (policy.alwaysAllowFrom ?? []).includes(senderId);
229
210
  const addressed = !matchesAny(ctx.text, access.ignorePatterns) && (mentionBypass || isMentioned(ctx, botUsername, access.mentionPatterns));
230
- if (!addressed && from.isBot === true && !botLoopGuardAllows(senderId)) return { action: "drop" };
231
211
  return {
232
212
  action: "deliver",
233
213
  access,