blun-king-cli 9.1.76 → 9.1.78

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/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.69
12
+ npm install -g blun-king-cli@9.1.77
13
13
 
14
14
  Start
15
15
  -----
@@ -72,6 +72,20 @@ parallel geprüft und eingelesen. Die Skills werden weiterhin in der
72
72
  ursprünglichen, festen Reihenfolge registriert; Priorität und Verhalten bei
73
73
  doppelten Namen bleiben unverändert.
74
74
 
75
+ Kontextentlastung bei langen Sitzungen
76
+ --------------------------------------
77
+ King bewahrt den vollständigen Verlauf weiterhin im Sitzungs-Wire auf. Erreicht
78
+ der aktive Verlauf 75 Prozent des Modellfensters, ersetzt die Modellprojektion
79
+ ältere große Werkzeugergebnisse durch einen kurzen Platzhalter. Die letzten 20
80
+ Nachrichten bleiben unverändert. Solange der Prefix-Cache warm ist, wird der
81
+ Schnitt höchstens nach jeweils 20 weiteren Nachrichten verschoben. Nach einer
82
+ Stunde ohne Modellantwort darf er sofort nachziehen.
83
+
84
+ Dabei werden keine gespeicherten Nachrichten geändert oder gelöscht. Fortsetzen,
85
+ Exportieren und die sichtbare Historie behalten die ursprünglichen
86
+ Werkzeugergebnisse. Das Telemetrieereignis `micro_compaction_finished` nennt den
87
+ Auslöser, den Schnitt und die geschätzte Tokenzahl vor und nach der Entlastung.
88
+
75
89
  Zug-Wächter
76
90
  -----------
77
91
  Läuft ein Zug 20 Minuten ohne neues Werkzeugergebnis, meldet die Konsole den
@@ -199,6 +213,21 @@ sodass auch kurze Schritte wie Rendern und Speichern in der Produktionskette
199
213
  sichtbar sind. Aktualisierte Vorschaubilder werden erneuert; Medienpfade und
200
214
  Webadressen sind als Terminalverweise anklickbar.
201
215
 
216
+ Wiederauffindbarer Verdichtungsverlauf
217
+ --------------------------------------
218
+
219
+ Bevor eine erfolgreiche Vollverdichtung ältere Nachrichten ersetzt, schreibt
220
+ King den verdrängten Verlauf in ein privates Markdown-Archiv unter
221
+ ~/.blun/conversation-history/. Die Verdichtungszusammenfassung enthält den
222
+ genauen history_path, sodass der Agent mit Read Einzelheiten wiederfinden kann,
223
+ die nicht in der Zusammenfassung stehen.
224
+
225
+ Eingebettete Bild-, Audio- und Videodaten werden im Archiv nicht doppelt
226
+ gespeichert. Der ursprüngliche Sitzungs-Wire bleibt maßgeblich. Kann das Archiv
227
+ nicht geschrieben werden, protokolliert King compaction_history_archive_failed
228
+ und lässt den normalen Verdichtungsweg verfügbar; ein Archivfehler löscht
229
+ niemals den gespeicherten Sitzungsverlauf.
230
+
202
231
  Aktualisieren
203
232
  -------------
204
233
 
package/README.md CHANGED
@@ -94,6 +94,20 @@ parallel geprüft und eingelesen. Die Skills werden weiterhin in der
94
94
  ursprünglichen, festen Reihenfolge registriert; Priorität und Verhalten bei
95
95
  doppelten Namen bleiben unverändert.
96
96
 
97
+ ## Kontextentlastung bei langen Sitzungen
98
+
99
+ King bewahrt den vollständigen Verlauf weiterhin im Sitzungs-Wire auf. Erreicht
100
+ der aktive Verlauf 75 Prozent des Modellfensters, ersetzt die Modellprojektion
101
+ ältere große Werkzeugergebnisse durch einen kurzen Platzhalter. Die letzten 20
102
+ Nachrichten bleiben unverändert. Solange der Prefix-Cache warm ist, wird der
103
+ Schnitt höchstens nach jeweils 20 weiteren Nachrichten verschoben. Nach einer
104
+ Stunde ohne Modellantwort darf er sofort nachziehen.
105
+
106
+ Dabei werden keine gespeicherten Nachrichten geändert oder gelöscht. Fortsetzen,
107
+ Exportieren und die sichtbare Historie behalten die ursprünglichen
108
+ Werkzeugergebnisse. Das Telemetrieereignis `micro_compaction_finished` nennt den
109
+ Auslöser, den Schnitt und die geschätzte Tokenzahl vor und nach der Entlastung.
110
+
97
111
  ## Zug-Wächter
98
112
 
99
113
  Läuft ein Zug 20 Minuten ohne neues Werkzeugergebnis, meldet die Konsole den
@@ -226,6 +240,12 @@ sodass auch kurze Schritte wie Rendern und Speichern in der Produktionskette
226
240
  sichtbar sind. Aktualisierte Vorschaubilder werden erneuert; Medienpfade und
227
241
  Webadressen sind als Terminalverweise anklickbar.
228
242
 
243
+ ## Wiederauffindbarer Verdichtungsverlauf
244
+
245
+ Bevor eine erfolgreiche Vollverdichtung ältere Nachrichten ersetzt, schreibt King den verdrängten Verlauf in ein privates Markdown-Archiv unter `~/.blun/conversation-history/`. Die Verdichtungszusammenfassung enthält den genauen `history_path`, sodass der Agent mit `Read` Einzelheiten wiederfinden kann, die nicht in der Zusammenfassung stehen.
246
+
247
+ Eingebettete Bild-, Audio- und Videodaten werden im Archiv nicht doppelt gespeichert. Der ursprüngliche Sitzungs-Wire bleibt maßgeblich. Kann das Archiv nicht geschrieben werden, protokolliert King `compaction_history_archive_failed` und lässt den normalen Verdichtungsweg verfügbar; ein Archivfehler löscht niemals den gespeicherten Sitzungsverlauf.
248
+
229
249
  ## Aktualisieren
230
250
 
231
251
  `blun update`, `king update` und die jeweilige Variante `upgrade` verwenden
@@ -0,0 +1,112 @@
1
+ 'use strict';
2
+
3
+ const { randomUUID } = require('node:crypto');
4
+ const { mkdir, writeFile } = require('node:fs/promises');
5
+ const path = require('node:path');
6
+
7
+ const COMPACTION_HISTORY_DIR = 'conversation-history';
8
+ const INLINE_MEDIA_NOTICE = '[inline media payload omitted; original remains in session wire]';
9
+
10
+ async function archiveCompactionHistory(options = {}) {
11
+ const homedir = requiredString(options.homedir, 'homedir');
12
+ const messages = Array.isArray(options.messages) ? options.messages : [];
13
+ const now = finiteNumber(options.now, Date.now());
14
+ const id = safeFilePart(options.id || randomUUID());
15
+ const dir = path.join(homedir, COMPACTION_HISTORY_DIR);
16
+ const timestamp = new Date(now).toISOString().replace(/[:.]/g, '-');
17
+ const outputPath = path.join(dir, `compaction-${timestamp}-${id}.md`);
18
+
19
+ await mkdir(dir, { recursive: true, mode: 0o700 });
20
+ await writeFile(outputPath, serializeCompactionHistory(messages, { now }), {
21
+ encoding: 'utf8',
22
+ flag: 'wx',
23
+ mode: 0o600,
24
+ });
25
+ return outputPath;
26
+ }
27
+
28
+ function serializeCompactionHistory(messages, options = {}) {
29
+ const now = finiteNumber(options.now, Date.now());
30
+ const lines = [
31
+ '# BLUN conversation history archive',
32
+ '',
33
+ `Archived at: ${new Date(now).toISOString()}`,
34
+ `Messages: ${messages.length}`,
35
+ '',
36
+ 'This file contains the messages displaced by a full context compaction.',
37
+ 'Inline media payloads are omitted here; the original session wire remains authoritative.',
38
+ ];
39
+
40
+ for (let index = 0; index < messages.length; index += 1) {
41
+ const message = sanitizeMessage(messages[index]);
42
+ lines.push(
43
+ '',
44
+ `## Message ${index + 1}: ${typeof message?.role === 'string' ? message.role : 'unknown'}`,
45
+ '',
46
+ '```json',
47
+ JSON.stringify(message, null, 2),
48
+ '```',
49
+ );
50
+ }
51
+ lines.push('');
52
+ return lines.join('\n');
53
+ }
54
+
55
+ function buildCompactionArchiveNotice(historyPath) {
56
+ return [
57
+ '<system-reminder>',
58
+ 'The complete messages displaced by this compaction were archived locally.',
59
+ `history_path: ${historyPath}`,
60
+ 'next_step: Use Read with history_path to recover exact details that the summary omitted.',
61
+ '</system-reminder>',
62
+ ].join('\n');
63
+ }
64
+
65
+ function sanitizeMessage(message) {
66
+ if (message === null || typeof message !== 'object') return message;
67
+ if (!Array.isArray(message.content)) return message;
68
+ return {
69
+ ...message,
70
+ content: message.content.map(sanitizeContentPart),
71
+ };
72
+ }
73
+
74
+ function sanitizeContentPart(part) {
75
+ if (part === null || typeof part !== 'object') return part;
76
+ if (part.type === 'image_url' && isInlineMediaUrl(part.imageUrl?.url)) {
77
+ return { type: 'text', text: INLINE_MEDIA_NOTICE };
78
+ }
79
+ if (part.type === 'audio_url' && isInlineMediaUrl(part.audioUrl?.url)) {
80
+ return { type: 'text', text: INLINE_MEDIA_NOTICE };
81
+ }
82
+ if (part.type === 'video_url' && isInlineMediaUrl(part.videoUrl?.url)) {
83
+ return { type: 'text', text: INLINE_MEDIA_NOTICE };
84
+ }
85
+ return part;
86
+ }
87
+
88
+ function isInlineMediaUrl(value) {
89
+ return typeof value === 'string' && /^data:(?:image|audio|video)\//i.test(value);
90
+ }
91
+
92
+ function requiredString(value, name) {
93
+ if (typeof value === 'string' && value.length > 0) return value;
94
+ throw new TypeError(`${name} must be a non-empty string`);
95
+ }
96
+
97
+ function safeFilePart(value) {
98
+ const cleaned = String(value).replace(/[^a-zA-Z0-9._-]+/g, '_').replace(/^_+|_+$/g, '').slice(0, 96);
99
+ return cleaned || randomUUID();
100
+ }
101
+
102
+ function finiteNumber(value, fallback) {
103
+ const number = Number(value);
104
+ return Number.isFinite(number) ? number : fallback;
105
+ }
106
+
107
+ module.exports = {
108
+ COMPACTION_HISTORY_DIR,
109
+ archiveCompactionHistory,
110
+ buildCompactionArchiveNotice,
111
+ serializeCompactionHistory,
112
+ };
@@ -0,0 +1,75 @@
1
+ 'use strict';
2
+
3
+ const MICRO_COMPACTION_PRESSURE_RATIO = 0.75;
4
+ const MICRO_COMPACTION_MIN_ADVANCE_MESSAGES = 20;
5
+
6
+ function selectMicroCompactionCutoff(options = {}) {
7
+ const historyLength = nonNegativeInteger(options.historyLength);
8
+ const currentCutoff = nonNegativeInteger(options.currentCutoff);
9
+ const keepRecentMessages = nonNegativeInteger(options.keepRecentMessages);
10
+ const nextCutoff = Math.max(0, historyLength - keepRecentMessages);
11
+ if (nextCutoff <= currentCutoff) return null;
12
+
13
+ const maxContextTokens = positiveFinite(options.maxContextTokens);
14
+ const contextTokens = nonNegativeFinite(options.contextTokens);
15
+ const contextUsageRatio = maxContextTokens === null
16
+ ? 1
17
+ : contextTokens / maxContextTokens;
18
+ const minContextUsageRatio = nonNegativeFinite(options.minContextUsageRatio);
19
+ if (contextUsageRatio < minContextUsageRatio) return null;
20
+
21
+ const now = finiteOr(options.now, Date.now());
22
+ const lastAssistantAt = finiteOrNull(options.lastAssistantAt);
23
+ const cacheAgeMs = lastAssistantAt === null ? null : Math.max(0, now - lastAssistantAt);
24
+ const cacheMissedThresholdMs = nonNegativeFinite(options.cacheMissedThresholdMs);
25
+ const cacheMissed = cacheAgeMs !== null && cacheAgeMs >= cacheMissedThresholdMs;
26
+ const underContextPressure = maxContextTokens !== null
27
+ && contextUsageRatio >= MICRO_COMPACTION_PRESSURE_RATIO;
28
+ if (!cacheMissed && !underContextPressure) return null;
29
+
30
+ if (
31
+ !cacheMissed
32
+ && currentCutoff > 0
33
+ && nextCutoff - currentCutoff < MICRO_COMPACTION_MIN_ADVANCE_MESSAGES
34
+ ) {
35
+ return null;
36
+ }
37
+
38
+ return {
39
+ cacheAgeMs,
40
+ contextUsageRatio,
41
+ cutoff: nextCutoff,
42
+ trigger: cacheMissed ? 'cache_miss' : 'context_pressure',
43
+ };
44
+ }
45
+
46
+ function finiteOr(value, fallback) {
47
+ const number = Number(value);
48
+ return Number.isFinite(number) ? number : fallback;
49
+ }
50
+
51
+ function finiteOrNull(value) {
52
+ if (value === null || value === undefined) return null;
53
+ const number = Number(value);
54
+ return Number.isFinite(number) ? number : null;
55
+ }
56
+
57
+ function nonNegativeFinite(value) {
58
+ const number = Number(value);
59
+ return Number.isFinite(number) && number >= 0 ? number : 0;
60
+ }
61
+
62
+ function nonNegativeInteger(value) {
63
+ return Math.floor(nonNegativeFinite(value));
64
+ }
65
+
66
+ function positiveFinite(value) {
67
+ const number = Number(value);
68
+ return Number.isFinite(number) && number > 0 ? number : null;
69
+ }
70
+
71
+ module.exports = {
72
+ MICRO_COMPACTION_MIN_ADVANCE_MESSAGES,
73
+ MICRO_COMPACTION_PRESSURE_RATIO,
74
+ selectMicroCompactionCutoff,
75
+ };
package/blun.mjs CHANGED
@@ -75237,8 +75237,9 @@ function extractCompactionSummary(response) {
75237
75237
  if (summary.trim().length === 0) throw new APIEmptyResponseError("The compaction response did not contain a non-empty summary.");
75238
75238
  return summary;
75239
75239
  }
75240
- var DEFAULT_COMPACTION_MAX_COMPLETION_TOKENS, COMPACTION_THINKING_EFFORT, COMPACTION_SUMMARY_RESERVE_RATIO, MAX_HIERARCHICAL_COMPACTION_PASSES, HIERARCHICAL_COMPACTION_PREFIX, CompactionTruncatedError, CompactionStallError, COMPACTION_STALL_MEASUREMENT_MULTIPLIER, REBUILT_AFTER_COMPACTION_INJECTION_VARIANTS, FullCompaction, MAX_COMPACTION_OVERFLOW_SHRINK_ATTEMPTS, COMPACTION_OVERFLOW_SHRINK_RATIOS;
75240
+ var archiveCompactionHistory, buildCompactionArchiveNotice, DEFAULT_COMPACTION_MAX_COMPLETION_TOKENS, COMPACTION_THINKING_EFFORT, COMPACTION_SUMMARY_RESERVE_RATIO, MAX_HIERARCHICAL_COMPACTION_PASSES, HIERARCHICAL_COMPACTION_PREFIX, CompactionTruncatedError, CompactionStallError, COMPACTION_STALL_MEASUREMENT_MULTIPLIER, REBUILT_AFTER_COMPACTION_INJECTION_VARIANTS, FullCompaction, MAX_COMPACTION_OVERFLOW_SHRINK_ATTEMPTS, COMPACTION_OVERFLOW_SHRINK_RATIOS;
75241
75241
  var init_full = __esmMin((() => {
75242
+ ({ archiveCompactionHistory, buildCompactionArchiveNotice } = createRequire(import.meta.url)("./bin/compaction-history-archive.cjs"));
75242
75243
  init_errors$8();
75243
75244
  init_src$4();
75244
75245
  init_errors$4();
@@ -75773,7 +75774,24 @@ var init_full = __esmMin((() => {
75773
75774
  return;
75774
75775
  }
75775
75776
  const rawSummary = this.postProcessSummary(summary ?? "");
75776
- const contextSummary = buildCompactionSummaryText(rawSummary);
75777
+ let contextSummary = buildCompactionSummaryText(rawSummary);
75778
+ let historyPath;
75779
+ try {
75780
+ historyPath = await archiveCompactionHistory({
75781
+ homedir: this.agent.blunHomeDir,
75782
+ messages: originalHistory
75783
+ });
75784
+ contextSummary = `${contextSummary}\n\n${buildCompactionArchiveNotice(historyPath)}`;
75785
+ this.agent.telemetry.track("compaction_history_archived", {
75786
+ history_path: historyPath,
75787
+ message_count: originalHistory.length
75788
+ });
75789
+ } catch (error) {
75790
+ this.agent.telemetry.track("compaction_history_archive_failed", {
75791
+ message_count: originalHistory.length,
75792
+ error_type: error instanceof Error ? error.name : "Unknown"
75793
+ });
75794
+ }
75777
75795
  const result = this.agent.context.applyCompaction({
75778
75796
  summary: rawSummary,
75779
75797
  contextSummary,
@@ -75889,8 +75907,9 @@ var init_full = __esmMin((() => {
75889
75907
  }));
75890
75908
  //#endregion
75891
75909
  //#region ../../packages/agent-core/src/agent/compaction/micro.ts
75892
- var DEFAULT_CONFIG, MicroCompaction;
75910
+ var selectMicroCompactionCutoff, DEFAULT_CONFIG, MicroCompaction;
75893
75911
  var init_micro = __esmMin((() => {
75912
+ ({ selectMicroCompactionCutoff } = createRequire(import.meta.url)("./bin/micro-compaction-policy.cjs"));
75894
75913
  init_tokens();
75895
75914
  DEFAULT_CONFIG = {
75896
75915
  keepRecentMessages: 20,
@@ -75920,9 +75939,67 @@ var init_micro = __esmMin((() => {
75920
75939
  });
75921
75940
  this.cutoff = cutoff;
75922
75941
  }
75923
- detect() {}
75942
+ detect() {
75943
+ const config = this.config;
75944
+ const { history, lastAssistantAt } = this.agent.context;
75945
+ const maxContextTokens = this.agent.config.modelCapabilities.max_context_tokens;
75946
+ const contextTokens = this.agent.context.tokenCountWithPending;
75947
+ const selection = selectMicroCompactionCutoff({
75948
+ historyLength: history.length,
75949
+ currentCutoff: this.cutoff,
75950
+ keepRecentMessages: config.keepRecentMessages,
75951
+ contextTokens,
75952
+ maxContextTokens,
75953
+ lastAssistantAt,
75954
+ now: Date.now(),
75955
+ cacheMissedThresholdMs: config.cacheMissedThresholdMs,
75956
+ minContextUsageRatio: config.minContextUsageRatio
75957
+ });
75958
+ if (selection === null) return;
75959
+ const previousCutoff = this.cutoff;
75960
+ const effect = this.measureEffect(history, selection.cutoff);
75961
+ const previousEffect = this.measureEffect(history, previousCutoff);
75962
+ if (effect.truncatedToolResultCount <= previousEffect.truncatedToolResultCount) return;
75963
+ this.apply(selection.cutoff);
75964
+ const rawContextTokens = estimateTokensForMessages(history);
75965
+ const tokensBefore = rawContextTokens - previousEffect.truncatedToolResultTokensBefore + previousEffect.truncatedToolResultTokensAfter;
75966
+ const tokensAfter = rawContextTokens - effect.truncatedToolResultTokensBefore + effect.truncatedToolResultTokensAfter;
75967
+ this.agent.telemetry.track("micro_compaction_finished", {
75968
+ trigger: selection.trigger,
75969
+ keep_recent_messages: config.keepRecentMessages,
75970
+ min_content_tokens: config.minContentTokens,
75971
+ cache_missed_threshold_ms: config.cacheMissedThresholdMs,
75972
+ truncated_marker: config.truncatedMarker,
75973
+ min_context_usage_ratio: config.minContextUsageRatio,
75974
+ context_usage_ratio: selection.contextUsageRatio,
75975
+ truncated_tool_result_count: effect.truncatedToolResultCount,
75976
+ truncated_tool_result_tokens_before: effect.truncatedToolResultTokensBefore,
75977
+ truncated_tool_result_tokens_after: effect.truncatedToolResultTokensAfter,
75978
+ tokens_before: tokensBefore,
75979
+ tokens_after: tokensAfter,
75980
+ previous_cutoff: previousCutoff,
75981
+ cutoff: selection.cutoff,
75982
+ message_count: history.length,
75983
+ cache_age_ms: selection.cacheAgeMs,
75984
+ thinking_effort: this.agent.config.thinkingEffort
75985
+ });
75986
+ }
75924
75987
  compact(messages) {
75925
- return messages;
75988
+ const config = this.config;
75989
+ const result = [];
75990
+ let i = 0;
75991
+ for (const msg of messages) {
75992
+ if (i < this.cutoff && msg.role === "tool" && msg.toolCallId !== void 0 && estimateTokensForContentParts(msg.content) >= config.minContentTokens) result.push({
75993
+ ...msg,
75994
+ content: [{
75995
+ type: "text",
75996
+ text: config.truncatedMarker
75997
+ }]
75998
+ });
75999
+ else result.push(msg);
76000
+ i++;
76001
+ }
76002
+ return result;
75926
76003
  }
75927
76004
  measureEffect(messages, cutoff) {
75928
76005
  let markerTokenCount;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blun-king-cli",
3
- "version": "9.1.76",
3
+ "version": "9.1.78",
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": {