blun-king-cli 9.1.99 → 9.1.101

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.99
12
+ npm install -g blun-king-cli@9.1.100
13
13
 
14
14
  Start
15
15
  -----
@@ -93,6 +93,8 @@ Große textbasierte Werkzeugergebnisse bleiben nicht mehr vollständig im Modell
93
93
 
94
94
  Ab BLUN King 9.1.99 werden auch direkt aufeinanderfolgende reine Textergebnisse als Stapel betrachtet. Enthalten sie zusammen mehr als 12.000 Zeichen, obwohl kein einzelnes Ergebnis diese Grenze überschreitet, speichert King so viele der größten geeigneten Ergebnisse wie nötig in privaten Dateien. In der Modellprojektion verbleiben lesbare Verweise. Der unveränderte Sitzungsrohverlauf bleibt vollständig erhalten. Gemischte Medienergebnisse, einzelne Ergebnisse unter 3.000 Zeichen und Stapel bis einschließlich 12.000 Zeichen bleiben unverändert. Beispiel: Bei zwei Suchergebnissen mit 7.000 und 6.000 Zeichen wird das größere Ergebnis privat gespeichert; Anfang, Ende, ausgelassene Zeichenzahl und `output_path` bleiben für den Agenten sichtbar.
95
95
 
96
+ Ab BLUN King 9.1.100 beendet eine begrenzte Grep-Inhaltssuche ripgrep, sobald der Versatz, die angeforderten Zeilen und eine zusätzliche Vorschauzeile vollständig vorliegen. Die Vorschauzeile belegt, ob eine weitere Seite existiert, ohne vorher bis zu 10 MB einzulesen. Unbegrenzte Suchen, Trefferzählungen und nach Änderungszeit sortierte Dateilisten laufen weiterhin vollständig durch.
97
+
96
98
  Große Dateien werden weiterhin seitenweise gelesen. Erreicht `Read` seine interne Grenze von 1.000 Zeilen und enthält die Datei weitere Zeilen, nennt das Ergebnis jetzt den exakten nächsten `line_offset`. Beispiel: Ein Lesevorgang ab Zeile 2001 wird mit `line_offset=3001` fortgesetzt. Am Dateiende und bei einem bewusst kleineren Leseausschnitt erscheint kein Fortsetzungshinweis. Dadurch zieht der Agent keine Schlüsse aus einem unvollständigen Ausschnitt und liest die Datei nicht erneut ab der ersten Zeile.
97
99
 
98
100
  Das Telemetrieereignis `tool_result_offloaded` enthält ausschließlich `tool_name`, `output_size_chars`, `output_size_bytes` und `preview_size_chars`. Es enthält weder den Inhalt noch den Speicherpfad. Beispiel: Eine Ausgabe mit 80.000 Zeichen erzeugt eine Vorschau mit 2.000 Zeichen; die Telemetrie zeigt die Größenersparnis, ohne Nutzdaten zu protokollieren.
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.99
12
+ npm install -g blun-king-cli@9.1.100
13
13
  ```
14
14
 
15
15
  ## Reproduzierbares Staging und Packen
@@ -113,6 +113,8 @@ Große textbasierte Werkzeugergebnisse bleiben nicht mehr vollständig im Modell
113
113
 
114
114
  Ab BLUN King 9.1.99 werden auch direkt aufeinanderfolgende reine Textergebnisse als Stapel betrachtet. Enthalten sie zusammen mehr als 12.000 Zeichen, obwohl kein einzelnes Ergebnis diese Grenze überschreitet, speichert King so viele der größten geeigneten Ergebnisse wie nötig in privaten Dateien. In der Modellprojektion verbleiben lesbare Verweise. Der unveränderte Sitzungsrohverlauf bleibt vollständig erhalten. Gemischte Medienergebnisse, einzelne Ergebnisse unter 3.000 Zeichen und Stapel bis einschließlich 12.000 Zeichen bleiben unverändert. Beispiel: Bei zwei Suchergebnissen mit 7.000 und 6.000 Zeichen wird das größere Ergebnis privat gespeichert; Anfang, Ende, ausgelassene Zeichenzahl und `output_path` bleiben für den Agenten sichtbar.
115
115
 
116
+ Ab BLUN King 9.1.100 beendet eine begrenzte Grep-Inhaltssuche ripgrep, sobald der Versatz, die angeforderten Zeilen und eine zusätzliche Vorschauzeile vollständig vorliegen. Die Vorschauzeile belegt, ob eine weitere Seite existiert, ohne vorher bis zu 10 MB einzulesen. Unbegrenzte Suchen, Trefferzählungen und nach Änderungszeit sortierte Dateilisten laufen weiterhin vollständig durch.
117
+
116
118
  Große Dateien werden weiterhin seitenweise gelesen. Erreicht `Read` seine interne Grenze von 1.000 Zeilen und enthält die Datei weitere Zeilen, nennt das Ergebnis jetzt den exakten nächsten `line_offset`. Beispiel: Ein Lesevorgang ab Zeile 2001 wird mit `line_offset=3001` fortgesetzt. Am Dateiende und bei einem bewusst kleineren Leseausschnitt erscheint kein Fortsetzungshinweis. Dadurch zieht der Agent keine Schlüsse aus einem unvollständigen Ausschnitt und liest die Datei nicht erneut ab der ersten Zeile.
117
119
 
118
120
  Das Telemetrieereignis `tool_result_offloaded` enthält ausschließlich `tool_name`, `output_size_chars`, `output_size_bytes` und `preview_size_chars`. Es enthält weder den Inhalt noch den Speicherpfad. Beispiel: Eine Ausgabe mit 80.000 Zeichen erzeugt eine Vorschau mit 2.000 Zeichen; die Telemetrie zeigt die Größenersparnis, ohne Nutzdaten zu protokollieren.
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+
3
+ function grepContentRecordLimit(args = {}) {
4
+ const mode = args.output_mode ?? "files_with_matches";
5
+ const headLimit = args.head_limit ?? 250;
6
+ const offset = args.offset ?? 0;
7
+ if (mode !== "content" || !Number.isSafeInteger(headLimit) || headLimit <= 0) return undefined;
8
+ if (!Number.isSafeInteger(offset) || offset < 0) return undefined;
9
+ const limit = offset + headLimit + 1;
10
+ return Number.isSafeInteger(limit) ? limit : undefined;
11
+ }
12
+
13
+ function grepPaginationNotice({ headLimit, offset, afterOffsetLength, earlyStopped = false }) {
14
+ const nextOffset = offset + headLimit;
15
+ const observedTotal = afterOffsetLength + offset;
16
+ const total = earlyStopped ? `at least ${String(observedTotal)}` : String(observedTotal);
17
+ return `Results truncated to ${String(headLimit)} lines (total: ${total}). Use offset=${String(nextOffset)} to see more.`;
18
+ }
19
+
20
+ function sliceThroughNewlineLimit(buffer, remainingNewlines) {
21
+ if (!Number.isSafeInteger(remainingNewlines) || remainingNewlines <= 0) {
22
+ return { accepted: buffer, newlineCount: 0, stopped: false };
23
+ }
24
+ let newlineCount = 0;
25
+ for (let index = 0; index < buffer.length; index += 1) {
26
+ if (buffer[index] !== 10) continue;
27
+ newlineCount += 1;
28
+ if (newlineCount === remainingNewlines) {
29
+ return { accepted: buffer.subarray(0, index + 1), newlineCount, stopped: true };
30
+ }
31
+ }
32
+ return { accepted: buffer, newlineCount, stopped: false };
33
+ }
34
+
35
+ module.exports = {
36
+ grepContentRecordLimit,
37
+ grepPaginationNotice,
38
+ sliceThroughNewlineLimit,
39
+ };
@@ -0,0 +1,37 @@
1
+ 'use strict';
2
+
3
+ const USER_MESSAGE_MAX_CHARS = 200_000;
4
+ const USER_MESSAGE_PREVIEW_LINES = 5;
5
+ const USER_MESSAGE_PREVIEW_LINE_CHARS = 1_000;
6
+ const USER_MESSAGE_OFFLOAD_MARKER = '[User message offloaded]';
7
+
8
+ function shouldOffloadUserMessage(textChars) {
9
+ return Number.isSafeInteger(textChars) && textChars > USER_MESSAGE_MAX_CHARS;
10
+ }
11
+
12
+ function createUserMessagePreview(text) {
13
+ const lines = String(text).split(/\r?\n/u);
14
+ const render = (line, index) => `${index}: ${line.slice(0, USER_MESSAGE_PREVIEW_LINE_CHARS)}`;
15
+ if (lines.length <= USER_MESSAGE_PREVIEW_LINES * 2) {
16
+ return lines.map((line, index) => render(line, index + 1)).join('\n');
17
+ }
18
+ const head = lines.slice(0, USER_MESSAGE_PREVIEW_LINES)
19
+ .map((line, index) => render(line, index + 1));
20
+ const tailStart = lines.length - USER_MESSAGE_PREVIEW_LINES;
21
+ const tail = lines.slice(tailStart)
22
+ .map((line, index) => render(line, tailStart + index + 1));
23
+ return [
24
+ ...head,
25
+ `... [${lines.length - USER_MESSAGE_PREVIEW_LINES * 2} lines omitted] ...`,
26
+ ...tail,
27
+ ].join('\n');
28
+ }
29
+
30
+ module.exports = {
31
+ USER_MESSAGE_MAX_CHARS,
32
+ USER_MESSAGE_OFFLOAD_MARKER,
33
+ USER_MESSAGE_PREVIEW_LINES,
34
+ USER_MESSAGE_PREVIEW_LINE_CHARS,
35
+ createUserMessagePreview,
36
+ shouldOffloadUserMessage,
37
+ };
package/blun.mjs CHANGED
@@ -78758,6 +78758,7 @@ var init_context$2 = __esmMin((() => {
78758
78758
  this.agent.replayBuilder.removeLastMessages(removedMessages);
78759
78759
  this.agent.microCompaction.reset(this._history.length);
78760
78760
  this.agent.toolResultBatchOffload.reset(this._history);
78761
+ this.agent.userMessageOffload.reset(this._history);
78761
78762
  this.agent.emitStatusUpdated();
78762
78763
  return removedMessages.size;
78763
78764
  }
@@ -78772,6 +78773,7 @@ var init_context$2 = __esmMin((() => {
78772
78773
  this._lastAssistantAt = null;
78773
78774
  this.agent.microCompaction.reset();
78774
78775
  this.agent.toolResultBatchOffload.clear();
78776
+ this.agent.userMessageOffload.clear();
78775
78777
  this.agent.injection.onContextClear();
78776
78778
  this.agent.emitStatusUpdated();
78777
78779
  }
@@ -78811,6 +78813,7 @@ var init_context$2 = __esmMin((() => {
78811
78813
  this.deferredMessages = [];
78812
78814
  this.agent.microCompaction.reset(this._history.length);
78813
78815
  this.agent.toolResultBatchOffload.reset(this._history);
78816
+ this.agent.userMessageOffload.reset(this._history);
78814
78817
  this.agent.emitStatusUpdated();
78815
78818
  if (!this.agent.records.restoring && (stoppedAtBoundary || removedUserCount < count)) throw new BlunError(ErrorCodes.REQUEST_INVALID, formatUndoUnavailableMessage(count, removedUserCount, stoppedAtBoundary), { details: {
78816
78819
  reason: "undo_limit",
@@ -78890,6 +78893,7 @@ var init_context$2 = __esmMin((() => {
78890
78893
  this.tokenCountCoveredMessageCount = this._history.length;
78891
78894
  this.agent.microCompaction.reset();
78892
78895
  this.agent.toolResultBatchOffload.reset(this._history);
78896
+ this.agent.userMessageOffload.reset(this._history);
78893
78897
  this.agent.injection.onContextCompacted();
78894
78898
  this.agent.emitStatusUpdated();
78895
78899
  return result;
@@ -78912,7 +78916,7 @@ var init_context$2 = __esmMin((() => {
78912
78916
  }
78913
78917
  project(messages, options) {
78914
78918
  const anomalies = [];
78915
- const result = project(this.agent.microCompaction.compact(this.agent.toolResultBatchOffload.compact(messages)), {
78919
+ const result = project(this.agent.userMessageOffload.compact(this.agent.microCompaction.compact(this.agent.toolResultBatchOffload.compact(messages))), {
78916
78920
  ...options,
78917
78921
  onAnomaly: (anomaly) => {
78918
78922
  anomalies.push(anomaly);
@@ -234835,6 +234839,9 @@ function restoreAgentRecord(agent, input) {
234835
234839
  case "tool_result_batch_offload.apply":
234836
234840
  agent.toolResultBatchOffload.apply(input.replacements);
234837
234841
  return;
234842
+ case "user_message_offload.apply":
234843
+ agent.userMessageOffload.apply(input.replacement);
234844
+ return;
234838
234845
  case "plan_mode.enter":
234839
234846
  agent.planMode.restoreEnter(input);
234840
234847
  return;
@@ -257665,10 +257672,17 @@ async function runRipgrepOnce(kaos, rgArgs, signal, options = {}) {
257665
257672
  let stderrText = "";
257666
257673
  let bufferTruncated = false;
257667
257674
  let stderrTruncated = false;
257675
+ let earlyStopped = false;
257668
257676
  try {
257669
- const isTerminating = () => timedOut || aborted || killed;
257677
+ const isTerminating = () => timedOut || aborted || killed || earlyStopped;
257670
257678
  const [stdoutResult, stderrResult, code] = await Promise.all([
257671
- readStreamWithCap(proc.stdout, MAX_OUTPUT_BYTES$1, isTerminating),
257679
+ readStreamWithCap(proc.stdout, MAX_OUTPUT_BYTES$1, isTerminating, {
257680
+ stopAfterDelimiterCount: options.stdoutRecordLimit,
257681
+ onStop: () => {
257682
+ earlyStopped = true;
257683
+ void killProc();
257684
+ }
257685
+ }),
257672
257686
  readStreamWithCap(proc.stderr, MAX_OUTPUT_BYTES$1, isTerminating),
257673
257687
  proc.wait()
257674
257688
  ]);
@@ -257704,7 +257718,8 @@ async function runRipgrepOnce(kaos, rgArgs, signal, options = {}) {
257704
257718
  stderrText,
257705
257719
  bufferTruncated,
257706
257720
  stderrTruncated,
257707
- timedOut
257721
+ timedOut,
257722
+ earlyStopped
257708
257723
  };
257709
257724
  }
257710
257725
  function shouldRetryRipgrepEagain(result) {
@@ -257713,23 +257728,37 @@ function shouldRetryRipgrepEagain(result) {
257713
257728
  function isEagainRipgrepError(stderr) {
257714
257729
  return stderr.includes("os error 11") || stderr.includes("Resource temporarily unavailable");
257715
257730
  }
257716
- async function readStreamWithCap(stream, maxBytes, suppressPrematureClose) {
257731
+ async function readStreamWithCap(stream, maxBytes, suppressPrematureClose, options = {}) {
257717
257732
  const chunks = [];
257718
257733
  let total = 0;
257719
257734
  let truncated = false;
257735
+ let delimiterCount = 0;
257720
257736
  try {
257721
257737
  for await (const chunk of stream) {
257722
257738
  const buf = typeof chunk === "string" ? Buffer.from(chunk, "utf8") : chunk;
257723
257739
  if (truncated) continue;
257724
- if (total + buf.length > maxBytes) {
257740
+ let accepted = buf;
257741
+ let stoppedAtRecordLimit = false;
257742
+ const delimiterLimit = options.stopAfterDelimiterCount;
257743
+ if (Number.isSafeInteger(delimiterLimit) && delimiterLimit > 0) {
257744
+ const slice = sliceThroughNewlineLimit(buf, delimiterLimit - delimiterCount);
257745
+ accepted = slice.accepted;
257746
+ delimiterCount += slice.newlineCount;
257747
+ stoppedAtRecordLimit = slice.stopped;
257748
+ }
257749
+ if (total + accepted.length > maxBytes) {
257725
257750
  const remaining = maxBytes - total;
257726
- if (remaining > 0) chunks.push(buf.subarray(0, remaining));
257751
+ if (remaining > 0) chunks.push(accepted.subarray(0, remaining));
257727
257752
  total = maxBytes;
257728
257753
  truncated = true;
257729
257754
  continue;
257730
257755
  }
257731
- chunks.push(buf);
257732
- total += buf.length;
257756
+ chunks.push(accepted);
257757
+ total += accepted.length;
257758
+ if (stoppedAtRecordLimit) {
257759
+ options.onStop?.();
257760
+ break;
257761
+ }
257733
257762
  }
257734
257763
  } catch (error) {
257735
257764
  if (!isPrematureCloseError(error) || suppressPrematureClose?.() !== true) throw error;
@@ -258358,7 +258387,7 @@ function countPayloadFromLegacyLine(line) {
258358
258387
  const idx = line.lastIndexOf(":");
258359
258388
  return idx > 0 ? line.slice(idx + 1) : void 0;
258360
258389
  }
258361
- var GrepInputSchema, RG_MAX_COLUMNS, DEFAULT_HEAD_LIMIT, MTIME_STAT_CONCURRENCY, CONTENT_LINE_RE, GrepTool, GrepAbortedError;
258390
+ var GrepInputSchema, RG_MAX_COLUMNS, DEFAULT_HEAD_LIMIT, MTIME_STAT_CONCURRENCY, CONTENT_LINE_RE, GrepTool, GrepAbortedError, grepContentRecordLimit, grepPaginationNotice, sliceThroughNewlineLimit;
258362
258391
  var init_grep = __esmMin((() => {
258363
258392
  init_dist$6();
258364
258393
  init_zod$1();
@@ -258373,6 +258402,7 @@ var init_grep = __esmMin((() => {
258373
258402
  init_result_builder();
258374
258403
  init_run_rg();
258375
258404
  init_grep$1();
258405
+ ({ grepContentRecordLimit, grepPaginationNotice, sliceThroughNewlineLimit } = createRequire(import.meta.url)("./bin/grep-output-limit-policy.cjs"));
258376
258406
  GrepInputSchema = object({
258377
258407
  pattern: string().describe("Regular expression to search for."),
258378
258408
  path: string().optional().describe("File or directory to search. Accepts an absolute path, or a path relative to the current working directory. Omit to search the current working directory. Use Read instead when you already know a concrete file path and need its contents."),
@@ -258473,15 +258503,21 @@ var init_grep = __esmMin((() => {
258473
258503
  output: rgUnavailableMessage(error)
258474
258504
  };
258475
258505
  }
258476
- let runResult = await runRipgrepOnce(this.kaos, buildRgArgs(rgPath, args, searchPaths), signal, { abortedMessage: "Grep aborted" });
258506
+ let runResult = await runRipgrepOnce(this.kaos, buildRgArgs(rgPath, args, searchPaths), signal, {
258507
+ abortedMessage: "Grep aborted",
258508
+ stdoutRecordLimit: grepContentRecordLimit(args)
258509
+ });
258477
258510
  if (runResult.kind === "tool-error") return runResult.result;
258478
258511
  if (shouldRetryRipgrepEagain(runResult)) {
258479
- runResult = await runRipgrepOnce(this.kaos, buildRgArgs(rgPath, args, searchPaths, true), signal, { abortedMessage: "Grep aborted" });
258512
+ runResult = await runRipgrepOnce(this.kaos, buildRgArgs(rgPath, args, searchPaths, true), signal, {
258513
+ abortedMessage: "Grep aborted",
258514
+ stdoutRecordLimit: grepContentRecordLimit(args)
258515
+ });
258480
258516
  if (runResult.kind === "tool-error") return runResult.result;
258481
258517
  }
258482
- const { exitCode, stderrText, bufferTruncated, stderrTruncated, timedOut } = runResult;
258518
+ const { exitCode, stderrText, bufferTruncated, stderrTruncated, timedOut, earlyStopped } = runResult;
258483
258519
  let { stdoutText } = runResult;
258484
- if (exitCode !== 0 && exitCode !== 1 && !timedOut) return {
258520
+ if (exitCode !== 0 && exitCode !== 1 && !timedOut && !earlyStopped) return {
258485
258521
  isError: true,
258486
258522
  output: formatRipgrepError(exitCode, stderrText, stderrTruncated)
258487
258523
  };
@@ -258522,9 +258558,12 @@ var init_grep = __esmMin((() => {
258522
258558
  }
258523
258559
  if (mode === "count_matches" && orderedLines.length > 0) headerLines.push(formatCountSummary(orderedLines, filteredSensitive.size > 0));
258524
258560
  if (paginationTruncated) {
258525
- const total = afterOffset.length + offset;
258526
- const nextOffset = offset + headLimit;
258527
- const paginationNotice = `Results truncated to ${String(headLimit)} lines (total: ${String(total)}). Use offset=${String(nextOffset)} to see more.`;
258561
+ const paginationNotice = grepPaginationNotice({
258562
+ headLimit,
258563
+ offset,
258564
+ afterOffsetLength: afterOffset.length,
258565
+ earlyStopped
258566
+ });
258528
258567
  if (mode === "count_matches") headerLines.push(paginationNotice);
258529
258568
  else messages.push(paginationNotice);
258530
258569
  }
@@ -260205,6 +260244,121 @@ var ToolResultBatchOffload = class {
260205
260244
  }
260206
260245
  };
260207
260246
  //#endregion
260247
+ //#region ../../packages/agent-core/src/agent/turn/user-message-offload.ts
260248
+ function persistableUserMessageText(message) {
260249
+ if (message?.role !== "user" || message.origin?.kind !== "user") return;
260250
+ const textParts = message.content.filter((part) => part.type === "text");
260251
+ if (textParts.length === 0) return;
260252
+ return textParts.map((part) => part.text).join("\n");
260253
+ }
260254
+ function userMessageOffloadHash(message, text) {
260255
+ return createHash("sha256").update(JSON.stringify({
260256
+ origin: message.origin,
260257
+ text
260258
+ })).digest("hex");
260259
+ }
260260
+ async function saveUserMessage(homedir, text) {
260261
+ try {
260262
+ const dir = join$4(homedir, "conversation-history");
260263
+ await mkdir(dir, {
260264
+ recursive: true,
260265
+ mode: 448
260266
+ });
260267
+ const outputPath = join$4(dir, `user-message-${randomUUID()}.txt`);
260268
+ await writeFile(outputPath, text, {
260269
+ encoding: "utf8",
260270
+ flag: "wx",
260271
+ mode: 384
260272
+ });
260273
+ return outputPath;
260274
+ } catch {
260275
+ return;
260276
+ }
260277
+ }
260278
+ function renderPersistedUserMessage(text, outputPath) {
260279
+ return [
260280
+ USER_MESSAGE_OFFLOAD_MARKER,
260281
+ `Message text exceeded ${String(USER_MESSAGE_MAX_CHARS)} characters; the complete original remains in conversation history.`,
260282
+ `message_size_chars: ${String(text.length)}`,
260283
+ `message_size_bytes: ${String(Buffer.byteLength(text, "utf8"))}`,
260284
+ `output_path: ${outputPath}`,
260285
+ "next_step: Use Read with output_path to inspect the complete message in pages.",
260286
+ "",
260287
+ "[preview: head and tail]",
260288
+ createUserMessagePreview(text)
260289
+ ].join("\n");
260290
+ }
260291
+ var USER_MESSAGE_MAX_CHARS, USER_MESSAGE_OFFLOAD_MARKER, shouldOffloadUserMessage, createUserMessagePreview;
260292
+ var init_user_message_offload = __esmMin((() => {
260293
+ const policy = createRequire(import.meta.url)("./bin/user-message-offload-policy.cjs");
260294
+ USER_MESSAGE_MAX_CHARS = policy.USER_MESSAGE_MAX_CHARS;
260295
+ USER_MESSAGE_OFFLOAD_MARKER = policy.USER_MESSAGE_OFFLOAD_MARKER;
260296
+ shouldOffloadUserMessage = policy.shouldOffloadUserMessage;
260297
+ createUserMessagePreview = policy.createUserMessagePreview;
260298
+ }));
260299
+ var UserMessageOffload = class {
260300
+ agent;
260301
+ replacements = /* @__PURE__ */ new Map();
260302
+ constructor(agent) {
260303
+ this.agent = agent;
260304
+ }
260305
+ async detect() {
260306
+ const message = this.agent.context.history.at(-1);
260307
+ const text = persistableUserMessageText(message);
260308
+ if (text === void 0 || !shouldOffloadUserMessage(text.length)) return 0;
260309
+ const messageHash = userMessageOffloadHash(message, text);
260310
+ if (this.replacements.has(messageHash) || this.agent.homedir === void 0) return 0;
260311
+ const outputPath = await saveUserMessage(this.agent.homedir, text);
260312
+ if (outputPath === void 0) return 0;
260313
+ const replacementText = renderPersistedUserMessage(text, outputPath);
260314
+ const content = [{
260315
+ type: "text",
260316
+ text: replacementText
260317
+ }, ...message.content.filter((part) => part.type !== "text")];
260318
+ this.apply({
260319
+ messageHash,
260320
+ content,
260321
+ outputPath
260322
+ });
260323
+ this.agent.telemetry.track("user_message_offloaded", {
260324
+ chars_before: text.length,
260325
+ chars_after: replacementText.length,
260326
+ chars_saved: text.length - replacementText.length,
260327
+ media_part_count: content.length - 1
260328
+ });
260329
+ return 1;
260330
+ }
260331
+ apply(replacement) {
260332
+ this.replacements.set(replacement.messageHash, replacement);
260333
+ this.agent.records.logRecord({
260334
+ type: "user_message_offload.apply",
260335
+ replacement
260336
+ });
260337
+ }
260338
+ compact(messages) {
260339
+ if (this.replacements.size === 0) return messages;
260340
+ return messages.map((message) => {
260341
+ const text = persistableUserMessageText(message);
260342
+ if (text === void 0) return message;
260343
+ const replacement = this.replacements.get(userMessageOffloadHash(message, text));
260344
+ return replacement === void 0 ? message : {
260345
+ ...message,
260346
+ content: replacement.content
260347
+ };
260348
+ });
260349
+ }
260350
+ reset(history = this.agent.context?.history ?? []) {
260351
+ const activeHashes = new Set(history.map((message) => {
260352
+ const text = persistableUserMessageText(message);
260353
+ return text === void 0 ? void 0 : userMessageOffloadHash(message, text);
260354
+ }).filter((hash) => hash !== void 0));
260355
+ for (const messageHash of this.replacements.keys()) if (!activeHashes.has(messageHash)) this.replacements.delete(messageHash);
260356
+ }
260357
+ clear() {
260358
+ this.replacements.clear();
260359
+ }
260360
+ };
260361
+ //#endregion
260208
260362
  //#region ../../packages/agent-core/src/agent/turn/index.ts
260209
260363
  function blunExtractText(content) {
260210
260364
  if (typeof content === "string") return content;
@@ -260540,6 +260694,7 @@ var init_turn = __esmMin((() => {
260540
260694
  init_canonical_args();
260541
260695
  init_tool_dedup();
260542
260696
  init_tool_result_budget();
260697
+ init_user_message_offload();
260543
260698
  ({ CORE_TOOL_NAMES: BLUN_CORE_TOOL_NAMES, createDeferredToolLoader, toolSchemaBudgetTokens } = createRequire(import.meta.url)("./bin/turn-tool-performance-policy.cjs"));
260544
260699
  BLUN_LEAN_TOOL_NAMES = new Set(BLUN_CORE_TOOL_NAMES);
260545
260700
  BLUN_ATTACHMENT_MARKER_RE = /\b(?:attachment_file_id|telegram-anhang|telegram attachment)\b/i;
@@ -261174,6 +261329,7 @@ var init_turn = __esmMin((() => {
261174
261329
  beforeStep: async ({ signal: stepSignal, stepNumber }) => {
261175
261330
  await this.agent.records.flush();
261176
261331
  this.agent.microCompaction.detect();
261332
+ await this.agent.userMessageOffload.detect();
261177
261333
  await this.agent.injection.inject();
261178
261334
  stepSignal.throwIfAborted();
261179
261335
  deduper.beginStep();
@@ -263982,6 +264138,7 @@ var init_agent = __esmMin((() => {
263982
264138
  this.fullCompaction = new FullCompaction(this, options.compactionStrategy);
263983
264139
  this.microCompaction = new MicroCompaction(this, options.microCompaction);
263984
264140
  this.toolResultBatchOffload = new ToolResultBatchOffload(this);
264141
+ this.userMessageOffload = new UserMessageOffload(this);
263985
264142
  this.context = new ContextMemory(this);
263986
264143
  this.config = new ConfigState(this);
263987
264144
  this.turn = new TurnFlow(this);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blun-king-cli",
3
- "version": "9.1.99",
3
+ "version": "9.1.101",
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": {