blun-king-cli 9.1.585 → 9.1.587

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,16 @@
1
+ ## 9.1.587 - 2026-09-10
2
+
3
+ - Preserve the active tool catalog across buffered instructions and the following work step. Required tools remain available; retained optional schemas are reselected when the existing schema budget is exceeded.
4
+ - Restore earlier automatic context compaction and allow another automatic compaction in the same turn after a successful shrinking summary. Smaller model limits and failure limits remain in force.
5
+ - After a thinking-only timeout, use the existing single recovery attempt with thinking disabled so the model can answer or call a tool. Subsequent ordinary steps retain the chosen thinking setting; cancellation, approval checks and timeout bounds remain in force.
6
+ - Set BLUN_STABLE_STEER_TOOLS=0 or BLUN_EARLY_COMPACTION=0 before starting King to restore the previous behavior for the respective feature. No existing sessions or configuration files are rewritten by this update.
7
+
8
+ ## 9.1.586 - 2026-09-09
9
+
10
+ - Fix Telegram receiver startup on Windows when an old bridge PID has been reused by an unrelated process. Preserve uncertain process ownership and never stop that unrelated process.
11
+ - Keep context compaction, model settings, conversation history, and existing Telegram delivery behavior unchanged.
12
+
13
+
1
14
  ## 9.1.585 - 2026-09-08
2
15
 
3
16
  - Managed WebSearch validates result envelopes and source rows even when optional search status metadata is absent.
@@ -3,8 +3,12 @@
3
3
  const DEFAULT_COMPACTION_TRIGGER_MAX_TOKENS = 256_000;
4
4
  const DEFAULT_COMPACTION_BLOCK_MAX_TOKENS = 288_000;
5
5
 
6
- function capCompactionThreshold(threshold, _phase) {
6
+ function capCompactionThreshold(threshold, phase) {
7
7
  if (threshold === undefined) return undefined;
8
+ if (process.env.BLUN_EARLY_COMPACTION !== "0") {
9
+ if (phase === "trigger") return Math.min(threshold, DEFAULT_COMPACTION_TRIGGER_MAX_TOKENS);
10
+ if (phase === "block") return Math.min(threshold, DEFAULT_COMPACTION_BLOCK_MAX_TOKENS);
11
+ }
8
12
  return threshold;
9
13
  }
10
14
 
@@ -65,6 +65,7 @@ function forceAnswerParams(params) {
65
65
  };
66
66
  return {
67
67
  ...params,
68
+ thinkingOnlyRecovery: true,
68
69
  messages: [...params.messages, forceAnswerMessage],
69
70
  boundaryMessageSuffix: [...(params.boundaryMessageSuffix ?? []), forceAnswerMessage],
70
71
  };
package/blun.mjs CHANGED
@@ -75058,7 +75058,8 @@ var init_kosong_llm = __esmMin((() => {
75058
75058
  this.notifyMediaDropped(dropped);
75059
75059
  });
75060
75060
  completionBudget = this.describeRequest(outgoingMessages, tools, params.completionBudgetRetry).details;
75061
- result = await this.generate(completionBudget.provider, this.systemPrompt, tools, outgoingMessages, callbacks, options);
75061
+ const requestProvider = params.thinkingOnlyRecovery === true ? completionBudget.provider.withThinking("off") : completionBudget.provider;
75062
+ result = await this.generate(requestProvider, this.systemPrompt, tools, outgoingMessages, callbacks, options);
75062
75063
  } catch (error) {
75063
75064
  if (error instanceof APIEmptyResponseError) throw error.withMetadata({ maxCompletionTokens: completionBudget === void 0 ? null : completionBudget.maxCompletionTokens ?? null });
75064
75065
  if (error instanceof APIPaymentRequiredError) {
@@ -75843,6 +75844,8 @@ var init_full = __esmMin((() => {
75843
75844
  }
75844
75845
  markCompleted() {
75845
75846
  this.agent.records.logRecord({ type: "full_compaction.complete" });
75847
+ const completedTransaction = this.compacting?.transaction;
75848
+ if (process.env.BLUN_EARLY_COMPACTION !== "0" && completedTransaction?.state === "summarized" && completedTransaction.tokensAfter < completedTransaction.tokensBefore) this.compactionCountInTurn = 0;
75846
75849
  this.closeTransaction("success");
75847
75850
  this.compacting = null;
75848
75851
  this.sessionCompactionState = resetCompactionFailures();
@@ -263255,6 +263258,52 @@ function blunSelectTurnTools(tools, maxContextTokens, loadedToolNames, requiredT
263255
263258
  deferredToolCount: deferred.length
263256
263259
  };
263257
263260
  }
263261
+ function blunSelectStableSteerTools(selectedTools, eligibleTools, loadedToolNames, requiredToolNames, maxContextTokens, log) {
263262
+ const selectedNames = new Set(selectedTools.map((tool) => tool.name));
263263
+ const additions = [];
263264
+ for (const tool of eligibleTools) {
263265
+ if (!requiredToolNames.has(tool.name) || selectedNames.has(tool.name)) continue;
263266
+ additions.push(tool);
263267
+ selectedNames.add(tool.name);
263268
+ }
263269
+ const proposed = [...selectedTools, ...additions];
263270
+ let deferred = eligibleTools.filter((tool) => !selectedNames.has(tool.name));
263271
+ const recoveryLoader = deferred.length > 0 && !selectedNames.has("ToolSearch") ? createDeferredToolLoader(selectedTools, deferred, loadedToolNames) : void 0;
263272
+ if (recoveryLoader) proposed.push(recoveryLoader);
263273
+ const schemaBudgetTokens = toolSchemaBudgetTokens(maxContextTokens);
263274
+ const proposedSchemaTokens = estimateTokensForTools(proposed);
263275
+ // A full-catalog alias must never shrink the eligible catalog itself.
263276
+ const budgetFallback = proposedSchemaTokens > schemaBudgetTokens && selectedTools !== eligibleTools;
263277
+ if (budgetFallback) {
263278
+ const selection = blunSelectTurnTools(eligibleTools, maxContextTokens, loadedToolNames, requiredToolNames);
263279
+ selectedTools.length = 0;
263280
+ for (const tool of selection.tools) if (tool.name !== "ToolSearch") selectedTools.push(tool);
263281
+ const retainedNames = new Set(selectedTools.map((tool) => tool.name));
263282
+ deferred = eligibleTools.filter((tool) => !retainedNames.has(tool.name));
263283
+ // Rebind discovery to the shared array, including former residents.
263284
+ if (deferred.length > 0) selectedTools.push(createDeferredToolLoader(selectedTools, deferred, loadedToolNames));
263285
+ } else {
263286
+ for (const tool of additions) selectedTools.push(tool);
263287
+ if (recoveryLoader) selectedTools.push(recoveryLoader);
263288
+ }
263289
+ const schemaTokenEstimate = estimateTokensForTools(selectedTools);
263290
+ const schemaBudgetExceeded = schemaTokenEstimate > schemaBudgetTokens;
263291
+ if (budgetFallback || schemaBudgetExceeded) log?.info("stable steer budget fallback", {
263292
+ budgetFallback,
263293
+ proposedSchemaTokens,
263294
+ schemaTokenEstimate,
263295
+ schemaBudgetTokens,
263296
+ schemaBudgetExceeded
263297
+ });
263298
+ return {
263299
+ tools: selectedTools,
263300
+ deferredToolCount: deferred.length,
263301
+ budgetFallback,
263302
+ schemaTokenEstimate,
263303
+ schemaBudgetTokens,
263304
+ schemaBudgetExceeded
263305
+ };
263306
+ }
263258
263307
  function blunToolSuppressionReason(turnNeedsTools, eligibleTools, deferredToolCount, fastConversation) {
263259
263308
  if (fastConversation) return "fast_conversation";
263260
263309
  if (!turnNeedsTools) return "greeting_optimization";
@@ -264496,7 +264545,7 @@ var init_turn = __esmMin((() => {
264496
264545
  const steerRequiredToolNames = mediaToolNamesForTurnText(steerText);
264497
264546
  for (const name of rankedToolNamesForTurnText(eligibleTools, steerText)) steerRequiredToolNames.add(name);
264498
264547
  if (steerEfforts.some((effort) => effort === void 0)) {
264499
- const prioritizedSelection = blunSelectTurnTools(eligibleTools, this.agent.config.modelCapabilities?.max_context_tokens, this.loadedToolNames, steerRequiredToolNames);
264548
+ const prioritizedSelection = process.env.BLUN_STABLE_STEER_TOOLS !== "0" ? blunSelectStableSteerTools(selectedTools, eligibleTools, this.loadedToolNames, steerRequiredToolNames, this.agent.config.modelCapabilities?.max_context_tokens, this.agent.log) : blunSelectTurnTools(eligibleTools, this.agent.config.modelCapabilities?.max_context_tokens, this.loadedToolNames, steerRequiredToolNames);
264500
264549
  this.agent.context.appendSystemReminder(ACTIVE_STEER_PRIORITY_REMINDER, {
264501
264550
  kind: "injection",
264502
264551
  variant: "active_steer_priority"
@@ -422277,7 +422326,41 @@ function resolveTelegramMcpEntry() {
422277
422326
  ];
422278
422327
  for (const candidate of candidates) if (candidate !== void 0 && existsSync(candidate)) return resolveKnownTelegramMcp(candidate, { explicitOverride: candidate === process.env["BLUN_TELEGRAM_MCP_SERVER"] });
422279
422328
  }
422329
+ import { fstatSync as __blunTelegramPidFstatSync } from "node:fs";
422330
+ "use strict";
422331
+ const PROCESS_START_LOOKUP_TIMEOUT_MS = 1500;
422332
+ const PID_CLAIM_TIME_TOLERANCE_MS = 1000;
422333
+ function readNonBridgeProcessStartedAt(pid) {
422334
+ if (process.platform !== 'win32' || !Number.isSafeInteger(pid) || pid <= 1)
422335
+ return undefined;
422336
+ try {
422337
+ const output = execFileSync('powershell.exe', [
422338
+ '-NoProfile', '-NonInteractive', '-Command',
422339
+ `$ErrorActionPreference='Stop'; $p=Get-CimInstance Win32_Process -Filter 'ProcessId=${pid}'; ` +
422340
+ // A recognizable bridge is preserved even after a wall-clock correction.
422341
+ `if($null -ne $p -and -not [string]::IsNullOrWhiteSpace($p.CommandLine) -and $p.CommandLine -notmatch 'bridge\\.mjs'){` +
422342
+ `([DateTimeOffset]$p.CreationDate.ToUniversalTime()).ToUnixTimeMilliseconds()}`,
422343
+ ], {
422344
+ encoding: 'utf8',
422345
+ windowsHide: true,
422346
+ timeout: PROCESS_START_LOOKUP_TIMEOUT_MS,
422347
+ maxBuffer: 4096,
422348
+ stdio: ['ignore', 'pipe', 'ignore'],
422349
+ }).trim();
422350
+ if (!/^\d+$/.test(output))
422351
+ return undefined;
422352
+ const startedAt = Number(output);
422353
+ return Number.isSafeInteger(startedAt) && startedAt > 0 ? startedAt : undefined;
422354
+ }
422355
+ catch {
422356
+ return undefined;
422357
+ }
422358
+ }
422359
+
422280
422360
  var TelegramChannelController = class {
422361
+ bridgeClaimUncertain = false;
422362
+ readProcessStartedAt;
422363
+
422281
422364
  host;
422282
422365
  dir;
422283
422366
  heartbeatTimer;
@@ -422343,7 +422426,9 @@ var TelegramChannelController = class {
422343
422426
  return false;
422344
422427
  }
422345
422428
  });
422346
- }
422429
+
422430
+ this.readProcessStartedAt = options.readProcessStartedAt ?? readNonBridgeProcessStartedAt;
422431
+ }
422347
422432
  get leaseFile() {
422348
422433
  return join(this.dir, "tui.pid");
422349
422434
  }
@@ -422466,11 +422551,68 @@ var TelegramChannelController = class {
422466
422551
  this.handoffTimer.unref();
422467
422552
  }
422468
422553
  liveBridgePid() {
422469
- try {
422470
- const pid = parseInt(readFileSync(this.botPidFile, "utf8"), 10);
422471
- if (Number.isInteger(pid) && pid > 1 && this.isProcessAlive(pid)) return pid;
422472
- } catch {}
422473
- }
422554
+ if (this.bridgeClaimUncertain)
422555
+ return null;
422556
+ let pid;
422557
+ let claim;
422558
+ let stats;
422559
+ try {
422560
+ const fd = openSync(this.botPidFile, 'r');
422561
+ try {
422562
+ claim = readFileSync(fd, 'utf8');
422563
+ stats = __blunTelegramPidFstatSync(fd);
422564
+ }
422565
+ finally {
422566
+ closeSync(fd);
422567
+ }
422568
+ pid = parseInt(claim, 10);
422569
+ if (!Number.isInteger(pid) || pid <= 1)
422570
+ return null;
422571
+ if (!this.isProcessAlive(pid))
422572
+ return undefined;
422573
+ }
422574
+ catch (error) {
422575
+ // Only an absent claim proves that reading it did not hide an owner.
422576
+ return error.code === 'ENOENT' ? undefined : null;
422577
+ }
422578
+ try {
422579
+ const startedAt = this.readProcessStartedAt(pid);
422580
+ if (startedAt !== undefined && Number.isFinite(startedAt)
422581
+ && startedAt > stats.mtimeMs + PID_CLAIM_TIME_TOLERANCE_MS
422582
+ && this.ownsChannel() && this.retireReusedBridgePid(claim, stats))
422583
+ return undefined;
422584
+ }
422585
+ catch {
422586
+ // A failed identity or archive lookup must not launch a second poller.
422587
+ }
422588
+ return pid;
422589
+ }
422590
+ retireReusedBridgePid(claim, original) {
422591
+ const matches = (file) => {
422592
+ const current = lstatSync(file);
422593
+ return current.isFile() && current.dev === original.dev && current.ino === original.ino
422594
+ && current.size === original.size && current.mtimeMs === original.mtimeMs
422595
+ && readFileSync(file, 'utf8') === claim;
422596
+ };
422597
+ if (!matches(this.botPidFile))
422598
+ return false;
422599
+ const retired = join(this.dir, `bot.pid.stale-${randomUUID()}`);
422600
+ renameSync(this.botPidFile, retired);
422601
+ try {
422602
+ if (matches(retired))
422603
+ return true;
422604
+ }
422605
+ catch {
422606
+ // Restore the name if the moved claim cannot be verified.
422607
+ }
422608
+ this.bridgeClaimUncertain = true;
422609
+ // Do not overwrite a newer claim if another writer won the rename window.
422610
+ try {
422611
+ linkSync(retired, this.botPidFile);
422612
+ }
422613
+ catch { /* A current claim wins. */ }
422614
+ return false;
422615
+ }
422474
422616
  releaseOwnership() {
422475
422617
  try {
422476
422618
  if (this.readOwner()?.ownerId === this.ownerId) rmSync(this.ownerFile);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blun-king-cli",
3
- "version": "9.1.585",
3
+ "version": "9.1.587",
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": {