blun-king-cli 9.1.517 → 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 +12 -0
- package/LIESMICH.txt +2 -2
- package/README.md +2 -2
- package/bin/tool-result-offload-policy.cjs +17 -5
- package/bin/tool-result-offload-policy.cjs.vor-historical-preview-20260831-210300 +317 -0
- package/blun.mjs +41 -8
- package/package.json +5 -2
- package/scripts/check-historical-tool-result-preview-regression.js +77 -0
- package/scripts/check-session-start-hook-context-regression.js +115 -0
- package/scripts/check-todo-recovery-catalog-regression.js +50 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
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
|
+
|
|
9
|
+
## 9.1.518 - 2026-08-31
|
|
10
|
+
|
|
11
|
+
- Keeps `Read`, `Write`, `Edit`, and `Bash` available alongside `TodoList` during proactive task-list maintenance, preserving the normal recovery path for large file changes.
|
|
12
|
+
- Retains automatic Todo maintenance and its bounded evidence prompt without switching the active model step to a TodoList-only tool catalogue.
|
|
13
|
+
- Adds a red-to-green regression gate against the 9.1.499 catalogue change and verifies recovery-tool selection without duplicate schemas.
|
|
14
|
+
|
|
3
15
|
## 9.1.517 - 2026-08-31
|
|
4
16
|
|
|
5
17
|
- Keeps `Ctrl+S` and `Escape` queue controls available while an approval panel is open: `Ctrl+S` flushes all waiting messages, while `Escape` releases one waiting message before rejecting the approval when the queue is empty.
|
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.
|
|
12
|
+
npm install -g blun-king-cli@9.1.519
|
|
13
13
|
|
|
14
14
|
AgentSpine 0.10.1
|
|
15
15
|
-----------------
|
|
16
|
-
Version 9.1.
|
|
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.
|
|
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.
|
|
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 =
|
|
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 <=
|
|
29
|
+
function createToolResultPreview(text, maxChars = TOOL_RESULT_PREVIEW_CHARS) {
|
|
30
|
+
if (text.length <= maxChars) return text;
|
|
29
31
|
|
|
30
|
-
const headChars = Math.ceil(
|
|
31
|
-
const tailChars =
|
|
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
|
|
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
|
|
231012
|
+
event,
|
|
231013
231013
|
message: messages.join("\n\n"),
|
|
231014
|
-
text: messages.map((message) => renderHookResult(
|
|
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
|
-
|
|
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
|
-
]
|
|
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;
|
|
@@ -262877,11 +262886,12 @@ var init_turn = __esmMin((() => {
|
|
|
262877
262886
|
mode: todoMaintenanceMode,
|
|
262878
262887
|
workCallsSinceRefresh: this.agent.goalTodoPolicyState?.workCallsSinceRefresh ?? 0
|
|
262879
262888
|
});
|
|
262889
|
+
const todoMaintenanceTools = goalTodoMaintenanceTools(eligibleTools, selectedTools);
|
|
262880
262890
|
const todoSystemPrompt = buildGoalTodoMaintenanceSystemPrompt(this.agent, todoMaintenanceMode);
|
|
262881
262891
|
const todoMessages = buildGoalTodoMaintenanceMessages(this.agent, todoMaintenanceMode);
|
|
262882
262892
|
return {
|
|
262883
262893
|
llm: this.agent.llmForTurn("low", todoSystemPrompt),
|
|
262884
|
-
tools:
|
|
262894
|
+
tools: todoMaintenanceTools,
|
|
262885
262895
|
buildMessages: () => todoMessages,
|
|
262886
262896
|
buildMessagesStrict: () => todoMessages
|
|
262887
262897
|
};
|
|
@@ -298491,10 +298501,18 @@ var init_session$1 = __esmMin((() => {
|
|
|
298491
298501
|
return agent;
|
|
298492
298502
|
}
|
|
298493
298503
|
async triggerSessionStart(source) {
|
|
298494
|
-
await this.hookEngine.trigger("SessionStart", {
|
|
298504
|
+
const results = await this.hookEngine.trigger("SessionStart", {
|
|
298495
298505
|
matcherValue: source,
|
|
298496
298506
|
inputData: { source }
|
|
298497
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();
|
|
298498
298516
|
}
|
|
298499
298517
|
async triggerSessionEnd(reason) {
|
|
298500
298518
|
await this.hookEngine.trigger("SessionEnd", {
|
|
@@ -423347,6 +423365,13 @@ const IDEA_CONTRACT_MARKER = "Work as a self-directing employee:";
|
|
|
423347
423365
|
const IDEA_ALLOWED_CHANNELS = Object.freeze([]);
|
|
423348
423366
|
const GOAL_TODO_REFRESH_WORK_CALL_LIMIT = 8;
|
|
423349
423367
|
const GOAL_TODO_EVIDENCE_LIMIT = 8;
|
|
423368
|
+
const TODO_MAINTENANCE_RECOVERY_TOOL_NAMES = new Set([
|
|
423369
|
+
"TodoList",
|
|
423370
|
+
"Read",
|
|
423371
|
+
"Write",
|
|
423372
|
+
"Edit",
|
|
423373
|
+
"Bash"
|
|
423374
|
+
]);
|
|
423350
423375
|
const IDEA_TERMINAL_TODO_STATUSES = Object.freeze([
|
|
423351
423376
|
"done",
|
|
423352
423377
|
"blocked",
|
|
@@ -423425,6 +423450,14 @@ function goalTodoMaintenanceMode(agent) {
|
|
|
423425
423450
|
progress.refreshRequired = true;
|
|
423426
423451
|
return "refresh";
|
|
423427
423452
|
}
|
|
423453
|
+
function goalTodoMaintenanceTools(eligibleTools, selectedTools) {
|
|
423454
|
+
const seen = new Set();
|
|
423455
|
+
return [...selectedTools, ...eligibleTools.filter((tool) => TODO_MAINTENANCE_RECOVERY_TOOL_NAMES.has(tool.name))].filter((tool) => {
|
|
423456
|
+
if (seen.has(tool.name)) return false;
|
|
423457
|
+
seen.add(tool.name);
|
|
423458
|
+
return true;
|
|
423459
|
+
});
|
|
423460
|
+
}
|
|
423428
423461
|
function buildGoalTodoMaintenanceSystemPrompt(agent, mode) {
|
|
423429
423462
|
const todos = ideaTodos(agent);
|
|
423430
423463
|
const visibleTodoList = todos.length === 0 ? "The visible TodoList is empty." : ["Current visible TodoList:", ...todos.map((todo) => `- [${String(todo?.status ?? "pending")}] ${String(todo?.title ?? "").trim()}`)].join("\n");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "blun-king-cli",
|
|
3
|
-
"version": "9.1.
|
|
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-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,9 @@
|
|
|
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",
|
|
50
|
+
"scripts/check-todo-recovery-catalog-regression.js",
|
|
48
51
|
"scripts/check-todo-loop-regression.js",
|
|
49
52
|
"scripts/fix-node-pty-perms.js",
|
|
50
53
|
"standard-skills/",
|
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
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 namesMatch = bundle.match(
|
|
17
|
+
/const TODO_MAINTENANCE_RECOVERY_TOOL_NAMES = new Set\(\[([\s\S]*?)\]\);/,
|
|
18
|
+
);
|
|
19
|
+
const functionMatch = bundle.match(
|
|
20
|
+
/function goalTodoMaintenanceTools\(eligibleTools, selectedTools\) \{[\s\S]*?\n\}/,
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
assert(
|
|
24
|
+
!/proactive TodoList maintenance step[\s\S]{0,900}tools:\s*\[todoTool\]/.test(bundle),
|
|
25
|
+
'TODO_RECOVERY_CATALOG_REGRESSION: proactive maintenance still switches to TodoList-only',
|
|
26
|
+
);
|
|
27
|
+
assert(namesMatch, 'TODO_RECOVERY_CATALOG_REGRESSION: recovery tool names are missing');
|
|
28
|
+
assert(functionMatch, 'TODO_RECOVERY_CATALOG_REGRESSION: recovery catalogue selector is missing');
|
|
29
|
+
assert(
|
|
30
|
+
/proactive TodoList maintenance step[\s\S]{0,1100}tools:\s*todoMaintenanceTools/.test(bundle),
|
|
31
|
+
'TODO_RECOVERY_CATALOG_REGRESSION: proactive maintenance is not wired to the recovery catalogue',
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
const selectMaintenanceTools = Function(
|
|
35
|
+
`const TODO_MAINTENANCE_RECOVERY_TOOL_NAMES = new Set([${namesMatch[1]}]); return (${functionMatch[0]});`,
|
|
36
|
+
)();
|
|
37
|
+
const tools = ['TodoList', 'Read', 'Write', 'Edit', 'Bash', 'WebSearch'].map((name) => ({ name }));
|
|
38
|
+
const selected = [tools[5], tools[1]];
|
|
39
|
+
const actual = selectMaintenanceTools(tools, selected).map((tool) => tool.name);
|
|
40
|
+
|
|
41
|
+
assert(
|
|
42
|
+
actual.join(',') === 'WebSearch,Read,TodoList,Write,Edit,Bash',
|
|
43
|
+
`TODO_RECOVERY_CATALOG_REGRESSION: unexpected catalogue ${actual.join(',')}`,
|
|
44
|
+
);
|
|
45
|
+
assert(
|
|
46
|
+
new Set(actual).size === actual.length,
|
|
47
|
+
'TODO_RECOVERY_CATALOG_REGRESSION: recovery catalogue contains duplicates',
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
process.stdout.write('todo-recovery-catalog-regression PASS\n');
|