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 +6 -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 +24 -7
- package/package.json +4 -2
- package/scripts/check-historical-tool-result-preview-regression.js +77 -0
- package/scripts/check-session-start-hook-context-regression.js +115 -0
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.
|
|
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;
|
|
@@ -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.
|
|
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
|
+
});
|