blun-king-cli 9.1.518 → 9.1.519

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,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 9.1.519 - 2026-08-31
4
+
5
+ - Persists successful `SessionStart` hook output before startup or reload returns, so AgentSpine readiness reaches the resumed model context automatically instead of waiting for the first user prompt.
6
+ - Keeps a bounded head-and-tail preview in historical offloaded tool-result references, preserving recovery anchors without restoring the full result to every model request.
7
+ - Adds red-to-green runtime regressions for SessionStart context delivery and historical preview retention while preserving the 9.1.518 Todo recovery catalogue.
8
+
3
9
  ## 9.1.518 - 2026-08-31
4
10
 
5
11
  - Keeps `Read`, `Write`, `Edit`, and `Bash` available alongside `TodoList` during proactive task-list maintenance, preserving the normal recovery path for large file changes.
package/LIESMICH.txt CHANGED
@@ -9,11 +9,11 @@ Installation
9
9
  ------------
10
10
  Die geprüfte Version exakt global installieren:
11
11
 
12
- npm install -g blun-king-cli@9.1.518
12
+ npm install -g blun-king-cli@9.1.519
13
13
 
14
14
  AgentSpine 0.10.1
15
15
  -----------------
16
- Version 9.1.518 enthält weiterhin AgentSpine 0.10.1 als inhaltsadressierte Pluginfassung. Der Preflight prüft jede aktive Host-Anweisungsdatei weiterhin race-sicher und bindet SHA-256 sowie Dateiidentität an den Zug, dupliziert den bereits vom Host geladenen Volltext aber nicht im Laufzeitkontext. Die reale Probe mit einer 15.519 Byte großen `CLAUDE.md` blieb dadurch bei 5.667 injizierten Byte. Der Stand enthält außerdem den selbstheilenden Persona- und Beziehungsgraphen, die begrenzte Telegram-Mnemo-Abfrage und eine sichtbare Fünf-Sekunden-Grenze für lokale Beziehungsabfragen.
16
+ Version 9.1.519 enthält weiterhin AgentSpine 0.10.1 als inhaltsadressierte Pluginfassung. Der Preflight prüft jede aktive Host-Anweisungsdatei weiterhin race-sicher und bindet SHA-256 sowie Dateiidentität an den Zug, dupliziert den bereits vom Host geladenen Volltext aber nicht im Laufzeitkontext. Die reale Probe mit einer 15.519 Byte großen `CLAUDE.md` blieb dadurch bei 5.667 injizierten Byte. Der Stand enthält außerdem den selbstheilenden Persona- und Beziehungsgraphen, die begrenzte Telegram-Mnemo-Abfrage und eine sichtbare Fünf-Sekunden-Grenze für lokale Beziehungsabfragen.
17
17
 
18
18
  Start
19
19
  -----
package/README.md CHANGED
@@ -9,12 +9,12 @@ 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.518
12
+ npm install -g blun-king-cli@9.1.519
13
13
  ```
14
14
 
15
15
  ## AgentSpine 0.10.1
16
16
 
17
- Version 9.1.518 enthält weiterhin AgentSpine 0.10.1 als inhaltsadressierte Pluginfassung. Der Preflight prüft jede aktive Host-Anweisungsdatei weiterhin race-sicher und bindet SHA-256 sowie Dateiidentität an den Zug, dupliziert den bereits vom Host geladenen Volltext aber nicht im Laufzeitkontext. Die reale Probe mit einer 15.519 Byte großen `CLAUDE.md` blieb dadurch bei 5.667 injizierten Byte. Der Stand enthält außerdem den selbstheilenden Persona- und Beziehungsgraphen, die begrenzte Telegram-Mnemo-Abfrage und eine sichtbare Fünf-Sekunden-Grenze für lokale Beziehungsabfragen.
17
+ Version 9.1.519 enthält weiterhin AgentSpine 0.10.1 als inhaltsadressierte Pluginfassung. Der Preflight prüft jede aktive Host-Anweisungsdatei weiterhin race-sicher und bindet SHA-256 sowie Dateiidentität an den Zug, dupliziert den bereits vom Host geladenen Volltext aber nicht im Laufzeitkontext. Die reale Probe mit einer 15.519 Byte großen `CLAUDE.md` blieb dadurch bei 5.667 injizierten Byte. Der Stand enthält außerdem den selbstheilenden Persona- und Beziehungsgraphen, die begrenzte Telegram-Mnemo-Abfrage und eine sichtbare Fünf-Sekunden-Grenze für lokale Beziehungsabfragen.
18
18
 
19
19
  ## Reproduzierbares Staging und Packen
20
20
 
@@ -2,6 +2,8 @@
2
2
 
3
3
  const TOOL_RESULT_MAX_CHARS = 12_000;
4
4
  const TOOL_RESULT_PREVIEW_CHARS = 2_000;
5
+ const TOOL_RESULT_HISTORICAL_PREVIEW_CHARS = 400;
6
+ const TOOL_RESULT_COMPACT_PREVIEW_CHARS = 240;
5
7
  const TOOL_RESULT_RECOVERY_PAGE_LINES = 20;
6
8
  const TOOL_RESULT_OFFLOAD_MARKER = '[Tool result offloaded]';
7
9
  const TOOL_RESULT_BATCH_MAX_CHARS = TOOL_RESULT_MAX_CHARS;
@@ -10,7 +12,7 @@ const TOOL_RESULT_BATCH_REPLACEMENT_BUDGET_CHARS = 2_500;
10
12
  const TOOL_RESULT_HISTORICAL_KEEP_RECENT_MESSAGES = 4;
11
13
  const TOOL_RESULT_HISTORICAL_MAX_CHARS = TOOL_RESULT_BATCH_MAX_CHARS;
12
14
  const TOOL_RESULT_HISTORICAL_MIN_ITEM_CHARS = 600;
13
- const TOOL_RESULT_HISTORICAL_REPLACEMENT_BUDGET_CHARS = 600;
15
+ const TOOL_RESULT_HISTORICAL_REPLACEMENT_BUDGET_CHARS = 1_000;
14
16
  const TOOL_RESULT_SUCCESS_KEEP_RECENT_MESSAGES = 12;
15
17
  const TOOL_RESULT_HISTORICAL_SUCCESS_MARKER = '[Old tool result content cleared]';
16
18
  const TOOL_RESULT_REPEAT_MARKER = '[Repeated tool result omitted]';
@@ -24,11 +26,11 @@ function shouldKeepFreshToolResult(toolName) {
24
26
  return toolName === 'Read';
25
27
  }
26
28
 
27
- function createToolResultPreview(text) {
28
- if (text.length <= TOOL_RESULT_PREVIEW_CHARS) return text;
29
+ function createToolResultPreview(text, maxChars = TOOL_RESULT_PREVIEW_CHARS) {
30
+ if (text.length <= maxChars) return text;
29
31
 
30
- const headChars = Math.ceil(TOOL_RESULT_PREVIEW_CHARS / 2);
31
- const tailChars = TOOL_RESULT_PREVIEW_CHARS - headChars;
32
+ const headChars = Math.ceil(maxChars / 2);
33
+ const tailChars = maxChars - headChars;
32
34
  const omittedChars = text.length - headChars - tailChars;
33
35
  return `${text.slice(0, headChars)}\n\n[... ${String(omittedChars)} characters omitted ...]\n\n${text.slice(-tailChars)}`;
34
36
  }
@@ -105,6 +107,14 @@ function compactPersistedToolResultReference(content) {
105
107
  if (nextStep !== undefined) recoveryLines.push(nextStep);
106
108
  }
107
109
  recoveryLines.push(outputPath);
110
+ if (previewIndex >= 0) {
111
+ const preview = part.text.slice(previewIndex + '\n[preview: head and tail]\n'.length);
112
+ recoveryLines.push(
113
+ '',
114
+ '[preview: head and tail]',
115
+ createToolResultPreview(preview, TOOL_RESULT_COMPACT_PREVIEW_CHARS),
116
+ );
117
+ }
108
118
  const text = recoveryLines.join('\n');
109
119
  if (text === part.text) return part;
110
120
  changed = true;
@@ -295,7 +305,9 @@ module.exports = {
295
305
  TOOL_RESULT_HISTORICAL_KEEP_RECENT_MESSAGES,
296
306
  TOOL_RESULT_HISTORICAL_MAX_CHARS,
297
307
  TOOL_RESULT_HISTORICAL_MIN_ITEM_CHARS,
308
+ TOOL_RESULT_HISTORICAL_PREVIEW_CHARS,
298
309
  TOOL_RESULT_HISTORICAL_REPLACEMENT_BUDGET_CHARS,
310
+ TOOL_RESULT_COMPACT_PREVIEW_CHARS,
299
311
  TOOL_RESULT_HISTORICAL_SUCCESS_MARKER,
300
312
  TOOL_RESULT_REPEAT_MARKER,
301
313
  TOOL_RESULT_MAX_CHARS,
@@ -0,0 +1,317 @@
1
+ 'use strict';
2
+
3
+ const TOOL_RESULT_MAX_CHARS = 12_000;
4
+ const TOOL_RESULT_PREVIEW_CHARS = 2_000;
5
+ const TOOL_RESULT_RECOVERY_PAGE_LINES = 20;
6
+ const TOOL_RESULT_OFFLOAD_MARKER = '[Tool result offloaded]';
7
+ const TOOL_RESULT_BATCH_MAX_CHARS = TOOL_RESULT_MAX_CHARS;
8
+ const TOOL_RESULT_BATCH_MIN_ITEM_CHARS = 3_000;
9
+ const TOOL_RESULT_BATCH_REPLACEMENT_BUDGET_CHARS = 2_500;
10
+ const TOOL_RESULT_HISTORICAL_KEEP_RECENT_MESSAGES = 4;
11
+ const TOOL_RESULT_HISTORICAL_MAX_CHARS = TOOL_RESULT_BATCH_MAX_CHARS;
12
+ const TOOL_RESULT_HISTORICAL_MIN_ITEM_CHARS = 600;
13
+ const TOOL_RESULT_HISTORICAL_REPLACEMENT_BUDGET_CHARS = 600;
14
+ const TOOL_RESULT_SUCCESS_KEEP_RECENT_MESSAGES = 12;
15
+ const TOOL_RESULT_HISTORICAL_SUCCESS_MARKER = '[Old tool result content cleared]';
16
+ const TOOL_RESULT_REPEAT_MARKER = '[Repeated tool result omitted]';
17
+ const READ_CONTINUATION_LINE_OFFSET_RE = /^Continue reading with line_offset=(\d+)\. Do not assume you have reached the end of the file\.$/mu;
18
+
19
+ function shouldOffloadToolResult(textLength) {
20
+ return Number.isFinite(textLength) && textLength > TOOL_RESULT_MAX_CHARS;
21
+ }
22
+
23
+ function shouldKeepFreshToolResult(toolName) {
24
+ return toolName === 'Read';
25
+ }
26
+
27
+ function createToolResultPreview(text) {
28
+ if (text.length <= TOOL_RESULT_PREVIEW_CHARS) return text;
29
+
30
+ const headChars = Math.ceil(TOOL_RESULT_PREVIEW_CHARS / 2);
31
+ const tailChars = TOOL_RESULT_PREVIEW_CHARS - headChars;
32
+ const omittedChars = text.length - headChars - tailChars;
33
+ return `${text.slice(0, headChars)}\n\n[... ${String(omittedChars)} characters omitted ...]\n\n${text.slice(-tailChars)}`;
34
+ }
35
+
36
+ function readContinuationLineOffset(text) {
37
+ if (typeof text !== 'string') return undefined;
38
+ const value = Number(text.match(READ_CONTINUATION_LINE_OFFSET_RE)?.[1]);
39
+ return Number.isSafeInteger(value) && value >= 1 ? value : undefined;
40
+ }
41
+
42
+ function isPersistedToolResultReference(content) {
43
+ if (!Array.isArray(content)) return false;
44
+
45
+ return content.some((part) => {
46
+ if (part?.type !== 'text' || typeof part.text !== 'string') return false;
47
+ if (!part.text.includes('\noutput_path: ')) return false;
48
+ return part.text.startsWith(`${TOOL_RESULT_OFFLOAD_MARKER}\n`)
49
+ || /^Tool output exceeded \d+ characters; showing a preview only\.\n/u.test(part.text);
50
+ });
51
+ }
52
+
53
+ function freshToolResultIds(messages) {
54
+ if (!Array.isArray(messages) || messages.length === 0) return new Set();
55
+
56
+ let assistantIndex = -1;
57
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
58
+ if (messages[index]?.role !== 'assistant') continue;
59
+ assistantIndex = index;
60
+ break;
61
+ }
62
+ if (assistantIndex < 0 || !Array.isArray(messages[assistantIndex].toolCalls)) return new Set();
63
+
64
+ const requestedIds = new Set(messages[assistantIndex].toolCalls
65
+ .map((call) => call?.id ?? call?.toolCallId)
66
+ .filter((id) => typeof id === 'string' && id.length > 0));
67
+ if (requestedIds.size === 0) return new Set();
68
+
69
+ const resultIds = new Set();
70
+ for (let index = assistantIndex + 1; index < messages.length; index += 1) {
71
+ const message = messages[index];
72
+ if (message?.role === 'tool' && requestedIds.has(message.toolCallId)) {
73
+ resultIds.add(message.toolCallId);
74
+ }
75
+ }
76
+ return resultIds;
77
+ }
78
+
79
+ function compactPersistedToolResultReference(content) {
80
+ if (!isPersistedToolResultReference(content)) return content;
81
+
82
+ let changed = false;
83
+ const compacted = content.map((part) => {
84
+ if (part?.type !== 'text' || typeof part.text !== 'string') return part;
85
+ const previewIndex = part.text.indexOf('\n[preview: head and tail]\n');
86
+ const referenceText = previewIndex < 0
87
+ ? part.text
88
+ : part.text.slice(0, previewIndex).trimEnd();
89
+ if (!referenceText.startsWith(`${TOOL_RESULT_OFFLOAD_MARKER}\n`)) {
90
+ if (referenceText === part.text) return part;
91
+ changed = true;
92
+ return { ...part, text: referenceText };
93
+ }
94
+
95
+ const lines = referenceText.split(/\r?\n/u);
96
+ const outputPath = lines.find((line) => line.startsWith('output_path: '));
97
+ if (outputPath === undefined) return part;
98
+ const recoveryLines = [TOOL_RESULT_OFFLOAD_MARKER];
99
+ for (const prefix of ['tool_name: ', 'output_size_chars: ', 'next_line_offset: ']) {
100
+ const line = lines.find((candidate) => candidate.startsWith(prefix));
101
+ if (line !== undefined) recoveryLines.push(line);
102
+ }
103
+ if (recoveryLines.some((line) => line.startsWith('next_line_offset: '))) {
104
+ const nextStep = lines.find((line) => line.startsWith('next_step: '));
105
+ if (nextStep !== undefined) recoveryLines.push(nextStep);
106
+ }
107
+ recoveryLines.push(outputPath);
108
+ const text = recoveryLines.join('\n');
109
+ if (text === part.text) return part;
110
+ changed = true;
111
+ return {
112
+ ...part,
113
+ text,
114
+ };
115
+ });
116
+
117
+ return changed ? compacted : content;
118
+ }
119
+
120
+ function compactHistoricalSuccessfulToolResults(messages) {
121
+ if (!Array.isArray(messages) || messages.length === 0) return messages;
122
+
123
+ const recentStart = Math.max(0, messages.length - TOOL_RESULT_SUCCESS_KEEP_RECENT_MESSAGES);
124
+ const freshIds = freshToolResultIds(messages);
125
+ let changed = false;
126
+ const projected = messages.map((message, index) => {
127
+ if (
128
+ index >= recentStart
129
+ || message?.role !== 'tool'
130
+ || freshIds.has(message.toolCallId)
131
+ || message.isError === true
132
+ || !Array.isArray(message.content)
133
+ || isPersistedToolResultReference(message.content)
134
+ || !message.content.every((part) => part?.type === 'text' && typeof part.text === 'string')
135
+ ) return message;
136
+
137
+ const textChars = message.content.reduce((total, part) => total + part.text.length, 0);
138
+ if (textChars <= TOOL_RESULT_HISTORICAL_SUCCESS_MARKER.length) return message;
139
+ changed = true;
140
+ return {
141
+ ...message,
142
+ content: [{ type: 'text', text: TOOL_RESULT_HISTORICAL_SUCCESS_MARKER }],
143
+ };
144
+ });
145
+
146
+ return changed ? projected : messages;
147
+ }
148
+
149
+ function toolCallSignaturesById(messages) {
150
+ const signatures = new Map();
151
+
152
+ for (const message of messages) {
153
+ if (!Array.isArray(message?.toolCalls)) continue;
154
+ for (const call of message.toolCalls) {
155
+ const id = call?.id ?? call?.toolCallId;
156
+ if (typeof id !== 'string' || typeof call?.name !== 'string') continue;
157
+ const args = typeof call.arguments === 'string'
158
+ ? call.arguments
159
+ : JSON.stringify(call.arguments ?? null);
160
+ signatures.set(id, `${call.name}\n${args}`);
161
+ }
162
+ }
163
+
164
+ return signatures;
165
+ }
166
+
167
+ function repeatedToolResultReference(newestToolCallId) {
168
+ return [
169
+ TOOL_RESULT_REPEAT_MARKER,
170
+ `same_as_tool_call_id: ${newestToolCallId}`,
171
+ 'reason: identical successful result for the same tool call arguments',
172
+ ].join('\n');
173
+ }
174
+
175
+ function dedupeRepeatedSuccessfulToolResults(sourceMessages, projectedMessages = sourceMessages) {
176
+ if (
177
+ !Array.isArray(sourceMessages)
178
+ || !Array.isArray(projectedMessages)
179
+ || sourceMessages.length === 0
180
+ || sourceMessages.length !== projectedMessages.length
181
+ ) return projectedMessages;
182
+
183
+ const callSignatures = toolCallSignaturesById(sourceMessages);
184
+ const seenResultsByCall = new Map();
185
+ let changed = false;
186
+ const projected = projectedMessages.slice();
187
+
188
+ for (let index = sourceMessages.length - 1; index >= 0; index -= 1) {
189
+ const message = sourceMessages[index];
190
+ const toolCallId = message?.toolCallId;
191
+ if (
192
+ message?.role !== 'tool'
193
+ || message.isError === true
194
+ || typeof toolCallId !== 'string'
195
+ || !Array.isArray(message.content)
196
+ || message.content.length === 0
197
+ || isPersistedToolResultReference(message.content)
198
+ || !message.content.every((part) => part?.type === 'text' && typeof part.text === 'string')
199
+ ) continue;
200
+
201
+ const callSignature = callSignatures.get(toolCallId);
202
+ if (callSignature === undefined) continue;
203
+ const resultShape = message.content.length;
204
+ const resultSignature = resultShape === 1
205
+ ? message.content[0].text
206
+ : JSON.stringify(message.content.map((part) => part.text));
207
+ let seenResults = seenResultsByCall.get(callSignature);
208
+ if (seenResults === undefined) {
209
+ seenResults = new Map();
210
+ seenResultsByCall.set(callSignature, seenResults);
211
+ }
212
+ let seenResultsForShape = seenResults.get(resultShape);
213
+ if (seenResultsForShape === undefined) {
214
+ seenResultsForShape = new Map();
215
+ seenResults.set(resultShape, seenResultsForShape);
216
+ }
217
+ const newest = seenResultsForShape.get(resultSignature);
218
+ if (newest === undefined) {
219
+ seenResultsForShape.set(resultSignature, { toolCallId, index });
220
+ continue;
221
+ }
222
+
223
+ const reference = repeatedToolResultReference(newest.toolCallId);
224
+ const textChars = message.content.reduce((total, part) => total + part.text.length, 0);
225
+ const projectedMessage = projected[index];
226
+ if (
227
+ reference.length >= textChars
228
+ || projectedMessage?.role !== 'tool'
229
+ || projectedMessage.toolCallId !== toolCallId
230
+ ) continue;
231
+ projected[index] = {
232
+ ...projectedMessage,
233
+ content: [{ type: 'text', text: reference }],
234
+ };
235
+ projected[newest.index] = {
236
+ ...projected[newest.index],
237
+ content: sourceMessages[newest.index].content,
238
+ };
239
+ changed = true;
240
+ }
241
+
242
+ return changed ? projected : projectedMessages;
243
+ }
244
+
245
+ function selectToolResultBatchOffloads(textLengths) {
246
+ if (!Array.isArray(textLengths) || textLengths.length < 2) return [];
247
+
248
+ const normalized = textLengths.map((length) => (
249
+ Number.isFinite(length) && length > 0 ? Math.floor(length) : 0
250
+ ));
251
+ const totalChars = normalized.reduce((total, length) => total + length, 0);
252
+ if (totalChars <= TOOL_RESULT_BATCH_MAX_CHARS) return [];
253
+
254
+ let projectedChars = totalChars;
255
+ const selected = [];
256
+ const candidates = normalized
257
+ .map((length, index) => ({ index, length }))
258
+ .filter(({ length }) => (
259
+ length >= TOOL_RESULT_BATCH_MIN_ITEM_CHARS
260
+ && length <= TOOL_RESULT_MAX_CHARS
261
+ && length > TOOL_RESULT_BATCH_REPLACEMENT_BUDGET_CHARS
262
+ ))
263
+ .sort((left, right) => right.length - left.length || left.index - right.index);
264
+
265
+ for (const candidate of candidates) {
266
+ if (projectedChars <= TOOL_RESULT_BATCH_MAX_CHARS) break;
267
+ projectedChars -= candidate.length - TOOL_RESULT_BATCH_REPLACEMENT_BUDGET_CHARS;
268
+ selected.push(candidate.index);
269
+ }
270
+
271
+ return projectedChars <= TOOL_RESULT_BATCH_MAX_CHARS
272
+ ? selected.sort((left, right) => left - right)
273
+ : [];
274
+ }
275
+
276
+ function selectHistoricalToolResultOffloads(textLengths) {
277
+ if (!Array.isArray(textLengths) || textLengths.length === 0) return [];
278
+
279
+ const normalized = textLengths.map((length) => (
280
+ Number.isFinite(length) && length > 0 ? Math.floor(length) : 0
281
+ ));
282
+ return normalized
283
+ .map((length, index) => ({ index, length }))
284
+ .filter(({ length }) => (
285
+ length >= TOOL_RESULT_HISTORICAL_MIN_ITEM_CHARS
286
+ && length > TOOL_RESULT_HISTORICAL_REPLACEMENT_BUDGET_CHARS
287
+ ))
288
+ .map(({ index }) => index);
289
+ }
290
+
291
+ module.exports = {
292
+ TOOL_RESULT_BATCH_MAX_CHARS,
293
+ TOOL_RESULT_BATCH_MIN_ITEM_CHARS,
294
+ TOOL_RESULT_BATCH_REPLACEMENT_BUDGET_CHARS,
295
+ TOOL_RESULT_HISTORICAL_KEEP_RECENT_MESSAGES,
296
+ TOOL_RESULT_HISTORICAL_MAX_CHARS,
297
+ TOOL_RESULT_HISTORICAL_MIN_ITEM_CHARS,
298
+ TOOL_RESULT_HISTORICAL_REPLACEMENT_BUDGET_CHARS,
299
+ TOOL_RESULT_HISTORICAL_SUCCESS_MARKER,
300
+ TOOL_RESULT_REPEAT_MARKER,
301
+ TOOL_RESULT_MAX_CHARS,
302
+ TOOL_RESULT_PREVIEW_CHARS,
303
+ TOOL_RESULT_RECOVERY_PAGE_LINES,
304
+ TOOL_RESULT_OFFLOAD_MARKER,
305
+ TOOL_RESULT_SUCCESS_KEEP_RECENT_MESSAGES,
306
+ compactHistoricalSuccessfulToolResults,
307
+ compactPersistedToolResultReference,
308
+ createToolResultPreview,
309
+ dedupeRepeatedSuccessfulToolResults,
310
+ freshToolResultIds,
311
+ isPersistedToolResultReference,
312
+ readContinuationLineOffset,
313
+ selectHistoricalToolResultOffloads,
314
+ selectToolResultBatchOffloads,
315
+ shouldKeepFreshToolResult,
316
+ shouldOffloadToolResult,
317
+ };
package/blun.mjs CHANGED
@@ -231005,15 +231005,21 @@ var init_engine = __esmMin((() => {
231005
231005
  function renderHookResult(event, message) {
231006
231006
  return `<hook_result hook_event="${event}">\n${message}\n</hook_result>`;
231007
231007
  }
231008
- function renderUserPromptHookResult(results) {
231008
+ function renderAllowHookResult(event, results) {
231009
231009
  const messages = results?.filter((result) => result.action !== "block")?.map(userPromptHookMessage).filter(isNonEmptyString$1) ?? [];
231010
231010
  if (messages.length === 0) return void 0;
231011
231011
  return {
231012
- event: "UserPromptSubmit",
231012
+ event,
231013
231013
  message: messages.join("\n\n"),
231014
- text: messages.map((message) => renderHookResult("UserPromptSubmit", message)).join("\n")
231014
+ text: messages.map((message) => renderHookResult(event, message)).join("\n")
231015
231015
  };
231016
231016
  }
231017
+ function renderUserPromptHookResult(results) {
231018
+ return renderAllowHookResult("UserPromptSubmit", results);
231019
+ }
231020
+ function renderSessionStartHookResult(results) {
231021
+ return renderAllowHookResult("SessionStart", results);
231022
+ }
231017
231023
  function renderUserPromptHookBlockResult(results) {
231018
231024
  const block = results?.find((result) => result.action === "block");
231019
231025
  if (block === void 0) return void 0;
@@ -261128,7 +261134,7 @@ async function persistHistoricalToolResultForModel(options, knownText) {
261128
261134
  }
261129
261135
  function renderHistoricalToolResultReference(toolName, toolCallId, text, outputPath, storageMode) {
261130
261136
  const nextLineOffset = toolName === "Read" ? readContinuationLineOffset(text) : void 0;
261131
- return [
261137
+ const lines = [
261132
261138
  TOOL_RESULT_OFFLOAD_MARKER,
261133
261139
  "An older tool result was archived to keep the active context fast.",
261134
261140
  `storage_mode: ${storageMode}`,
@@ -261139,18 +261145,21 @@ function renderHistoricalToolResultReference(toolName, toolCallId, text, outputP
261139
261145
  ...(nextLineOffset === void 0 ? [] : [`next_line_offset: ${String(nextLineOffset)}`]),
261140
261146
  `output_path: ${outputPath}`,
261141
261147
  nextLineOffset === void 0 ? `next_step: Use Read with output_path using n_lines <= ${String(TOOL_RESULT_RECOVERY_PAGE_LINES)}; advance line_offset between calls.` : `next_step: Continue the original Read with line_offset=${String(nextLineOffset)}; do not restart at the beginning.`
261142
- ].join("\n");
261148
+ ];
261149
+ lines.push("", "[preview: head and tail]", createToolResultPreview(text, TOOL_RESULT_HISTORICAL_PREVIEW_CHARS));
261150
+ return lines.join("\n");
261143
261151
  }
261144
261152
  function safeToolResultFileStem(toolName, toolCallId) {
261145
261153
  return `${toolName}-${toolCallId}`.replace(/[^a-zA-Z0-9._-]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 80) || "tool-result";
261146
261154
  }
261147
- var TOOL_RESULT_MAX_CHARS, TOOL_RESULT_PREVIEW_CHARS, TOOL_RESULT_RECOVERY_PAGE_LINES, TOOL_RESULT_OFFLOAD_MARKER, TOOL_RESULT_HISTORICAL_KEEP_RECENT_MESSAGES, shouldKeepFreshToolResult, shouldOffloadToolResult, createToolResultPreview, compactPersistedToolResultReference, compactHistoricalSuccessfulToolResults, dedupeRepeatedSuccessfulToolResults, readContinuationLineOffset, selectToolResultBatchOffloads, selectHistoricalToolResultOffloads, buildToolResultOffloadTelemetry;
261155
+ var TOOL_RESULT_MAX_CHARS, TOOL_RESULT_PREVIEW_CHARS, TOOL_RESULT_HISTORICAL_PREVIEW_CHARS, TOOL_RESULT_RECOVERY_PAGE_LINES, TOOL_RESULT_OFFLOAD_MARKER, TOOL_RESULT_HISTORICAL_KEEP_RECENT_MESSAGES, shouldKeepFreshToolResult, shouldOffloadToolResult, createToolResultPreview, compactPersistedToolResultReference, compactHistoricalSuccessfulToolResults, dedupeRepeatedSuccessfulToolResults, readContinuationLineOffset, selectToolResultBatchOffloads, selectHistoricalToolResultOffloads, buildToolResultOffloadTelemetry;
261148
261156
  var init_tool_result_budget = __esmMin((() => {
261149
261157
  init_dist$6();
261150
261158
  const toolResultOffloadPolicy = createRequire(import.meta.url)("./bin/tool-result-offload-policy.cjs");
261151
261159
  ({ buildToolResultOffloadTelemetry } = createRequire(import.meta.url)("./bin/tool-result-offload-telemetry.cjs"));
261152
261160
  TOOL_RESULT_MAX_CHARS = toolResultOffloadPolicy.TOOL_RESULT_MAX_CHARS;
261153
261161
  TOOL_RESULT_PREVIEW_CHARS = toolResultOffloadPolicy.TOOL_RESULT_PREVIEW_CHARS;
261162
+ TOOL_RESULT_HISTORICAL_PREVIEW_CHARS = toolResultOffloadPolicy.TOOL_RESULT_HISTORICAL_PREVIEW_CHARS;
261154
261163
  TOOL_RESULT_RECOVERY_PAGE_LINES = toolResultOffloadPolicy.TOOL_RESULT_RECOVERY_PAGE_LINES;
261155
261164
  TOOL_RESULT_OFFLOAD_MARKER = toolResultOffloadPolicy.TOOL_RESULT_OFFLOAD_MARKER;
261156
261165
  TOOL_RESULT_HISTORICAL_KEEP_RECENT_MESSAGES = toolResultOffloadPolicy.TOOL_RESULT_HISTORICAL_KEEP_RECENT_MESSAGES;
@@ -298492,10 +298501,18 @@ var init_session$1 = __esmMin((() => {
298492
298501
  return agent;
298493
298502
  }
298494
298503
  async triggerSessionStart(source) {
298495
- await this.hookEngine.trigger("SessionStart", {
298504
+ const results = await this.hookEngine.trigger("SessionStart", {
298496
298505
  matcherValue: source,
298497
298506
  inputData: { source }
298498
298507
  });
298508
+ const hookResult = renderSessionStartHookResult(results);
298509
+ if (hookResult === void 0) return;
298510
+ const mainAgent = this.requireMainAgent();
298511
+ mainAgent.context.appendSystemReminder(`${hookResult.text}\n\nThis supersedes any earlier SessionStart hook result in this session.`, {
298512
+ kind: "injection",
298513
+ variant: "session_start_hook"
298514
+ });
298515
+ await mainAgent.records.flush();
298499
298516
  }
298500
298517
  async triggerSessionEnd(reason) {
298501
298518
  await this.hookEngine.trigger("SessionEnd", {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blun-king-cli",
3
- "version": "9.1.518",
3
+ "version": "9.1.519",
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": {
@@ -9,7 +9,7 @@
9
9
  },
10
10
  "scripts": {
11
11
  "test": "node --test test/*.test.js",
12
- "prepack": "node scripts/check-release-metadata.js && node scripts/check-todo-loop-regression.js && node scripts/check-todo-recovery-catalog-regression.js && node scripts/check-queue-controls-regression.js && node scripts/check-approval-queue-shortcuts-regression.js && node scripts/check-telegram-bridge-watchdog.js && node scripts/check-resume-replay-regression.js && node scripts/check-session-cancel-regression.js && node scripts/check-plugin-startup-regression.js && node scripts/check-active-profile-plugin-startup.js && node scripts/check-mcp-startup-wait-budget.js",
12
+ "prepack": "node scripts/check-release-metadata.js && node scripts/check-todo-loop-regression.js && node scripts/check-todo-recovery-catalog-regression.js && node scripts/check-historical-tool-result-preview-regression.js && node scripts/check-session-start-hook-context-regression.js && node scripts/check-queue-controls-regression.js && node scripts/check-approval-queue-shortcuts-regression.js && node scripts/check-telegram-bridge-watchdog.js && node scripts/check-resume-replay-regression.js && node scripts/check-session-cancel-regression.js && node scripts/check-plugin-startup-regression.js && node scripts/check-active-profile-plugin-startup.js && node scripts/check-mcp-startup-wait-budget.js",
13
13
  "release:verify": "node scripts/check-release-metadata.js --external",
14
14
  "postinstall": "node scripts/fix-node-pty-perms.js"
15
15
  },
@@ -45,6 +45,8 @@
45
45
  "scripts/check-resume-replay-regression.js",
46
46
  "scripts/check-session-cancel-regression.js",
47
47
  "scripts/check-release-metadata.js",
48
+ "scripts/check-historical-tool-result-preview-regression.js",
49
+ "scripts/check-session-start-hook-context-regression.js",
48
50
  "scripts/check-todo-recovery-catalog-regression.js",
49
51
  "scripts/check-todo-loop-regression.js",
50
52
  "scripts/fix-node-pty-perms.js",
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('node:fs');
5
+ const path = require('node:path');
6
+
7
+ const bundlePath = process.env.BLUN_BUNDLE_UNDER_TEST
8
+ ? path.resolve(process.env.BLUN_BUNDLE_UNDER_TEST)
9
+ : path.resolve(__dirname, '..', 'blun.mjs');
10
+ const bundle = fs.readFileSync(bundlePath, 'utf8');
11
+
12
+ function assert(condition, message) {
13
+ if (!condition) throw new Error(message);
14
+ }
15
+
16
+ const rendererMatch = bundle.match(
17
+ /function renderHistoricalToolResultReference\(toolName, toolCallId, text, outputPath, storageMode\) \{[\s\S]*?\n\}/,
18
+ );
19
+ assert(rendererMatch, 'HISTORICAL_TOOL_RESULT_PREVIEW_REGRESSION: renderer is missing');
20
+ const policy = require('../bin/tool-result-offload-policy.cjs');
21
+
22
+ const renderHistoricalToolResultReference = Function(
23
+ 'Buffer',
24
+ 'TOOL_RESULT_OFFLOAD_MARKER',
25
+ 'TOOL_RESULT_RECOVERY_PAGE_LINES',
26
+ 'TOOL_RESULT_HISTORICAL_PREVIEW_CHARS',
27
+ 'readContinuationLineOffset',
28
+ 'createToolResultPreview',
29
+ `return (${rendererMatch[0]});`,
30
+ )(
31
+ Buffer,
32
+ '[Tool result offloaded]',
33
+ 20,
34
+ policy.TOOL_RESULT_HISTORICAL_PREVIEW_CHARS,
35
+ () => undefined,
36
+ (text, maxChars) => `${text.slice(0, maxChars / 2)}\n[preview omitted]\n${text.slice(-maxChars / 2)}`,
37
+ );
38
+
39
+ const text = `HEAD-${'a'.repeat(900)}-TAIL`;
40
+ const rendered = renderHistoricalToolResultReference(
41
+ 'Bash',
42
+ 'call-preview',
43
+ text,
44
+ 'C:/archive/call-preview.txt',
45
+ 'private_archive',
46
+ );
47
+
48
+ assert(
49
+ rendered.includes('[preview: head and tail]'),
50
+ 'HISTORICAL_TOOL_RESULT_PREVIEW_REGRESSION: historical offload has no preview marker',
51
+ );
52
+ assert(
53
+ rendered.includes('HEAD-') && rendered.includes('-TAIL'),
54
+ 'HISTORICAL_TOOL_RESULT_PREVIEW_REGRESSION: historical offload does not preserve both ends',
55
+ );
56
+ assert(
57
+ rendered.includes('output_path: C:/archive/call-preview.txt'),
58
+ 'HISTORICAL_TOOL_RESULT_PREVIEW_REGRESSION: recovery path was lost',
59
+ );
60
+ const compacted = policy.compactPersistedToolResultReference([
61
+ { type: 'text', text: rendered },
62
+ ]);
63
+ const compactedText = compacted[0].text;
64
+ assert(
65
+ compactedText.includes('[preview: head and tail]'),
66
+ 'HISTORICAL_TOOL_RESULT_PREVIEW_REGRESSION: context compaction removed the preview',
67
+ );
68
+ assert(
69
+ compactedText.includes('HEAD-') && compactedText.includes('-TAIL'),
70
+ 'HISTORICAL_TOOL_RESULT_PREVIEW_REGRESSION: compact preview lost an endpoint',
71
+ );
72
+ assert(
73
+ compactedText.length < rendered.length,
74
+ 'HISTORICAL_TOOL_RESULT_PREVIEW_REGRESSION: compact preview is not bounded',
75
+ );
76
+
77
+ process.stdout.write('historical-tool-result-preview-regression PASS\n');
@@ -0,0 +1,115 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('node:fs');
5
+ const path = require('node:path');
6
+
7
+ const bundlePath = process.env.BLUN_BUNDLE_UNDER_TEST
8
+ ? path.resolve(process.env.BLUN_BUNDLE_UNDER_TEST)
9
+ : path.resolve(__dirname, '..', 'blun.mjs');
10
+ const bundle = fs.readFileSync(bundlePath, 'utf8');
11
+
12
+ function assert(condition, message) {
13
+ if (!condition) throw new Error(message);
14
+ }
15
+
16
+ const renderer = bundle.match(
17
+ /function renderSessionStartHookResult\(results\) \{[\s\S]*?\n\}/,
18
+ );
19
+ const allowRenderer = bundle.match(
20
+ /function renderAllowHookResult\(event, results\) \{[\s\S]*?\n\}/,
21
+ );
22
+ const wrapperRenderer = bundle.match(
23
+ /function renderHookResult\(event, message\) \{[\s\S]*?\n\}/,
24
+ );
25
+ const messageSelector = bundle.match(
26
+ /function userPromptHookMessage\(result\) \{[\s\S]*?\n\}/,
27
+ );
28
+ const stringPredicate = bundle.match(
29
+ /function isNonEmptyString\$1\(value\) \{[\s\S]*?\n\}/,
30
+ );
31
+ const trigger = bundle.match(
32
+ /async triggerSessionStart\(source\) \{[\s\S]*?\n\t\t\}/,
33
+ );
34
+
35
+ assert(renderer, 'SESSION_START_HOOK_CONTEXT_REGRESSION: SessionStart renderer is missing');
36
+ assert(allowRenderer, 'SESSION_START_HOOK_CONTEXT_REGRESSION: shared allow renderer is missing');
37
+ assert(wrapperRenderer, 'SESSION_START_HOOK_CONTEXT_REGRESSION: hook wrapper renderer is missing');
38
+ assert(messageSelector, 'SESSION_START_HOOK_CONTEXT_REGRESSION: hook message selector is missing');
39
+ assert(stringPredicate, 'SESSION_START_HOOK_CONTEXT_REGRESSION: hook string predicate is missing');
40
+ assert(trigger, 'SESSION_START_HOOK_CONTEXT_REGRESSION: SessionStart trigger is missing');
41
+ assert(
42
+ /renderAllowHookResult\("SessionStart", results\)/.test(renderer[0]),
43
+ 'SESSION_START_HOOK_CONTEXT_REGRESSION: SessionStart output is not rendered',
44
+ );
45
+ assert(
46
+ /const results = await this\.hookEngine\.trigger\("SessionStart"/.test(trigger[0]),
47
+ 'SESSION_START_HOOK_CONTEXT_REGRESSION: SessionStart results are still discarded',
48
+ );
49
+ assert(
50
+ /renderSessionStartHookResult\(results\)/.test(trigger[0]),
51
+ 'SESSION_START_HOOK_CONTEXT_REGRESSION: SessionStart result does not reach the context path',
52
+ );
53
+ assert(
54
+ /appendSystemReminder\([\s\S]*variant: "session_start_hook"/.test(trigger[0]),
55
+ 'SESSION_START_HOOK_CONTEXT_REGRESSION: SessionStart result is not persisted as a scoped reminder',
56
+ );
57
+ assert(
58
+ /await mainAgent\.records\.flush\(\)/.test(trigger[0]),
59
+ 'SESSION_START_HOOK_CONTEXT_REGRESSION: SessionStart reminder is not flushed before reload returns',
60
+ );
61
+
62
+ const triggerFunction = trigger[0].replace(
63
+ /^async triggerSessionStart/,
64
+ 'async function triggerSessionStart',
65
+ );
66
+ const runTrigger = Function(
67
+ `${wrapperRenderer[0]}\n${messageSelector[0]}\n${stringPredicate[0]}\n${allowRenderer[0]}\n${renderer[0]}\n${triggerFunction}\nreturn triggerSessionStart;`,
68
+ )();
69
+
70
+ (async () => {
71
+ const reminders = [];
72
+ let flushes = 0;
73
+ const fakeSession = {
74
+ hookEngine: {
75
+ async trigger(event, args) {
76
+ assert(event === 'SessionStart', 'runtime probe called the wrong hook event');
77
+ assert(args.matcherValue === 'resume', 'runtime probe lost the reload matcher');
78
+ return [{ action: 'allow', message: 'AgentSpine ready: 152 sources indexed.' }];
79
+ },
80
+ },
81
+ requireMainAgent() {
82
+ return {
83
+ context: {
84
+ appendSystemReminder(text, origin) {
85
+ reminders.push({ text, origin });
86
+ },
87
+ },
88
+ records: {
89
+ async flush() {
90
+ flushes += 1;
91
+ },
92
+ },
93
+ };
94
+ },
95
+ };
96
+
97
+ await runTrigger.call(fakeSession, 'resume');
98
+ assert(reminders.length === 1, 'runtime probe did not append exactly one reminder');
99
+ assert(flushes === 1, 'runtime probe did not flush exactly once');
100
+ assert(
101
+ reminders[0].text.includes('<hook_result hook_event="SessionStart">')
102
+ && reminders[0].text.includes('AgentSpine ready: 152 sources indexed.'),
103
+ 'runtime probe lost the AgentSpine SessionStart message',
104
+ );
105
+ assert(
106
+ reminders[0].origin?.kind === 'injection'
107
+ && reminders[0].origin?.variant === 'session_start_hook',
108
+ 'runtime probe used the wrong reminder origin',
109
+ );
110
+
111
+ process.stdout.write('session-start-hook-context-regression PASS\n');
112
+ })().catch((error) => {
113
+ process.stderr.write(`${error.stack || error.message}\n`);
114
+ process.exitCode = 1;
115
+ });