blun-king-cli 9.1.518 → 9.1.520
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/agent-spine-plugin/blun.plugin.json +1 -1
- package/agent-spine-plugin/hooks/codex.json +1 -1
- package/bin/tool-result-offload-policy.cjs +17 -5
- 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 +178 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 9.1.520 - 2026-08-31
|
|
4
|
+
|
|
5
|
+
- Gives AgentSpine's `SessionStart` hook a 60-second cold-project budget, so startup and `/reload` can persist the automatic briefing even when the measured source scan exceeds the former 15-second limit.
|
|
6
|
+
- Keeps prompt, tool, compact, stop, and subagent hook budgets unchanged; only the full SessionStart briefing receives the larger allowance.
|
|
7
|
+
- Extends the SessionStart regression with the packaged AgentSpine manifests and a real hook-process output probe instead of relying only on a simulated result.
|
|
8
|
+
|
|
9
|
+
## 9.1.519 - 2026-08-31
|
|
10
|
+
|
|
11
|
+
- 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.
|
|
12
|
+
- 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.
|
|
13
|
+
- Adds red-to-green runtime regressions for SessionStart context delivery and historical preview retention while preserving the 9.1.518 Todo recovery catalogue.
|
|
14
|
+
|
|
3
15
|
## 9.1.518 - 2026-08-31
|
|
4
16
|
|
|
5
17
|
- 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.520
|
|
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.520
|
|
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
|
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
}
|
|
16
16
|
},
|
|
17
17
|
"hooks": [
|
|
18
|
-
{ "event": "SessionStart", "command": "node \"./src/hook.js\"", "timeout":
|
|
18
|
+
{ "event": "SessionStart", "command": "node \"./src/hook.js\"", "timeout": 60 },
|
|
19
19
|
{ "event": "UserPromptSubmit", "command": "node \"./src/hook.js\"", "timeout": 15 },
|
|
20
20
|
{ "event": "PreToolUse", "matcher": "Edit|Write|apply_patch|Bash|exec_command", "command": "node \"./src/hook.js\"", "timeout": 15 },
|
|
21
21
|
{ "event": "PostToolUse", "command": "node \"./src/hook.js\"", "timeout": 15 },
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"SessionStart": [
|
|
5
5
|
{
|
|
6
6
|
"matcher": "startup|resume|clear|compact",
|
|
7
|
-
"hooks": [{ "type": "command", "command": "node \"${PLUGIN_ROOT}/src/hook.js\"", "timeout":
|
|
7
|
+
"hooks": [{ "type": "command", "command": "node \"${PLUGIN_ROOT}/src/hook.js\"", "timeout": 60 }]
|
|
8
8
|
}
|
|
9
9
|
],
|
|
10
10
|
"UserPromptSubmit": [
|
|
@@ -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,
|
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.520",
|
|
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,178 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const os = require('node:os');
|
|
6
|
+
const path = require('node:path');
|
|
7
|
+
const { spawnSync } = require('node:child_process');
|
|
8
|
+
|
|
9
|
+
const bundlePath = process.env.BLUN_BUNDLE_UNDER_TEST
|
|
10
|
+
? path.resolve(process.env.BLUN_BUNDLE_UNDER_TEST)
|
|
11
|
+
: path.resolve(__dirname, '..', 'blun.mjs');
|
|
12
|
+
const bundle = fs.readFileSync(bundlePath, 'utf8');
|
|
13
|
+
|
|
14
|
+
function assert(condition, message) {
|
|
15
|
+
if (!condition) throw new Error(message);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const renderer = bundle.match(
|
|
19
|
+
/function renderSessionStartHookResult\(results\) \{[\s\S]*?\n\}/,
|
|
20
|
+
);
|
|
21
|
+
const allowRenderer = bundle.match(
|
|
22
|
+
/function renderAllowHookResult\(event, results\) \{[\s\S]*?\n\}/,
|
|
23
|
+
);
|
|
24
|
+
const wrapperRenderer = bundle.match(
|
|
25
|
+
/function renderHookResult\(event, message\) \{[\s\S]*?\n\}/,
|
|
26
|
+
);
|
|
27
|
+
const messageSelector = bundle.match(
|
|
28
|
+
/function userPromptHookMessage\(result\) \{[\s\S]*?\n\}/,
|
|
29
|
+
);
|
|
30
|
+
const stringPredicate = bundle.match(
|
|
31
|
+
/function isNonEmptyString\$1\(value\) \{[\s\S]*?\n\}/,
|
|
32
|
+
);
|
|
33
|
+
const trigger = bundle.match(
|
|
34
|
+
/async triggerSessionStart\(source\) \{[\s\S]*?\n\t\t\}/,
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
assert(renderer, 'SESSION_START_HOOK_CONTEXT_REGRESSION: SessionStart renderer is missing');
|
|
38
|
+
assert(allowRenderer, 'SESSION_START_HOOK_CONTEXT_REGRESSION: shared allow renderer is missing');
|
|
39
|
+
assert(wrapperRenderer, 'SESSION_START_HOOK_CONTEXT_REGRESSION: hook wrapper renderer is missing');
|
|
40
|
+
assert(messageSelector, 'SESSION_START_HOOK_CONTEXT_REGRESSION: hook message selector is missing');
|
|
41
|
+
assert(stringPredicate, 'SESSION_START_HOOK_CONTEXT_REGRESSION: hook string predicate is missing');
|
|
42
|
+
assert(trigger, 'SESSION_START_HOOK_CONTEXT_REGRESSION: SessionStart trigger is missing');
|
|
43
|
+
assert(
|
|
44
|
+
/renderAllowHookResult\("SessionStart", results\)/.test(renderer[0]),
|
|
45
|
+
'SESSION_START_HOOK_CONTEXT_REGRESSION: SessionStart output is not rendered',
|
|
46
|
+
);
|
|
47
|
+
assert(
|
|
48
|
+
/const results = await this\.hookEngine\.trigger\("SessionStart"/.test(trigger[0]),
|
|
49
|
+
'SESSION_START_HOOK_CONTEXT_REGRESSION: SessionStart results are still discarded',
|
|
50
|
+
);
|
|
51
|
+
assert(
|
|
52
|
+
/renderSessionStartHookResult\(results\)/.test(trigger[0]),
|
|
53
|
+
'SESSION_START_HOOK_CONTEXT_REGRESSION: SessionStart result does not reach the context path',
|
|
54
|
+
);
|
|
55
|
+
assert(
|
|
56
|
+
/appendSystemReminder\([\s\S]*variant: "session_start_hook"/.test(trigger[0]),
|
|
57
|
+
'SESSION_START_HOOK_CONTEXT_REGRESSION: SessionStart result is not persisted as a scoped reminder',
|
|
58
|
+
);
|
|
59
|
+
assert(
|
|
60
|
+
/await mainAgent\.records\.flush\(\)/.test(trigger[0]),
|
|
61
|
+
'SESSION_START_HOOK_CONTEXT_REGRESSION: SessionStart reminder is not flushed before reload returns',
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const packageRoot = path.resolve(__dirname, '..');
|
|
65
|
+
const agentSpineRoot = path.join(packageRoot, 'agent-spine-plugin');
|
|
66
|
+
const agentSpineManifest = JSON.parse(
|
|
67
|
+
fs.readFileSync(path.join(agentSpineRoot, 'blun.plugin.json'), 'utf8'),
|
|
68
|
+
);
|
|
69
|
+
const agentSpineCodexHooks = JSON.parse(
|
|
70
|
+
fs.readFileSync(path.join(agentSpineRoot, 'hooks', 'codex.json'), 'utf8'),
|
|
71
|
+
);
|
|
72
|
+
const blunSessionStart = agentSpineManifest.hooks.find(
|
|
73
|
+
(hook) => hook.event === 'SessionStart',
|
|
74
|
+
);
|
|
75
|
+
const codexSessionStart = agentSpineCodexHooks.hooks.SessionStart?.[0]?.hooks?.[0];
|
|
76
|
+
|
|
77
|
+
assert(
|
|
78
|
+
blunSessionStart?.timeout >= 60,
|
|
79
|
+
'SESSION_START_HOOK_CONTEXT_REGRESSION: BLUN AgentSpine SessionStart timeout must cover a cold project scan',
|
|
80
|
+
);
|
|
81
|
+
assert(
|
|
82
|
+
codexSessionStart?.timeout >= 60,
|
|
83
|
+
'SESSION_START_HOOK_CONTEXT_REGRESSION: Codex AgentSpine SessionStart timeout must match the BLUN runtime budget',
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
const probeRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'blun-session-start-hook-'));
|
|
87
|
+
try {
|
|
88
|
+
fs.mkdirSync(path.join(probeRoot, '.git'));
|
|
89
|
+
fs.writeFileSync(
|
|
90
|
+
path.join(probeRoot, 'AGENTS.md'),
|
|
91
|
+
'# SessionStart probe\n\nLoad this project instruction automatically.\n',
|
|
92
|
+
);
|
|
93
|
+
const stateRoot = path.join(probeRoot, '.agentspine-state');
|
|
94
|
+
const hookRun = spawnSync(process.execPath, [path.join(agentSpineRoot, 'src', 'hook.js')], {
|
|
95
|
+
cwd: agentSpineRoot,
|
|
96
|
+
input: JSON.stringify({
|
|
97
|
+
hook_event_name: 'SessionStart',
|
|
98
|
+
session_id: 'session-start-regression',
|
|
99
|
+
cwd: probeRoot,
|
|
100
|
+
source: 'resume',
|
|
101
|
+
}),
|
|
102
|
+
encoding: 'utf8',
|
|
103
|
+
timeout: 30_000,
|
|
104
|
+
env: {
|
|
105
|
+
...process.env,
|
|
106
|
+
BLUN_HOME: probeRoot,
|
|
107
|
+
BLUN_PLUGIN_ROOT: agentSpineRoot,
|
|
108
|
+
AGENTSPINE_STATE_DIR: stateRoot,
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
assert(
|
|
112
|
+
hookRun.error === undefined && hookRun.status === 0,
|
|
113
|
+
`SESSION_START_HOOK_CONTEXT_REGRESSION: real AgentSpine hook failed: ${hookRun.error?.message || hookRun.stderr}`,
|
|
114
|
+
);
|
|
115
|
+
const hookOutput = JSON.parse(hookRun.stdout.trim());
|
|
116
|
+
assert(
|
|
117
|
+
hookOutput.hookSpecificOutput?.hookEventName === 'SessionStart'
|
|
118
|
+
&& hookOutput.hookSpecificOutput?.message?.includes('AgentSpine'),
|
|
119
|
+
`SESSION_START_HOOK_CONTEXT_REGRESSION: real AgentSpine process output is not injectable: ${hookRun.stdout.trim()}`,
|
|
120
|
+
);
|
|
121
|
+
} finally {
|
|
122
|
+
fs.rmSync(probeRoot, { recursive: true, force: true });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const triggerFunction = trigger[0].replace(
|
|
126
|
+
/^async triggerSessionStart/,
|
|
127
|
+
'async function triggerSessionStart',
|
|
128
|
+
);
|
|
129
|
+
const runTrigger = Function(
|
|
130
|
+
`${wrapperRenderer[0]}\n${messageSelector[0]}\n${stringPredicate[0]}\n${allowRenderer[0]}\n${renderer[0]}\n${triggerFunction}\nreturn triggerSessionStart;`,
|
|
131
|
+
)();
|
|
132
|
+
|
|
133
|
+
(async () => {
|
|
134
|
+
const reminders = [];
|
|
135
|
+
let flushes = 0;
|
|
136
|
+
const fakeSession = {
|
|
137
|
+
hookEngine: {
|
|
138
|
+
async trigger(event, args) {
|
|
139
|
+
assert(event === 'SessionStart', 'runtime probe called the wrong hook event');
|
|
140
|
+
assert(args.matcherValue === 'resume', 'runtime probe lost the reload matcher');
|
|
141
|
+
return [{ action: 'allow', message: 'AgentSpine ready: 152 sources indexed.' }];
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
requireMainAgent() {
|
|
145
|
+
return {
|
|
146
|
+
context: {
|
|
147
|
+
appendSystemReminder(text, origin) {
|
|
148
|
+
reminders.push({ text, origin });
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
records: {
|
|
152
|
+
async flush() {
|
|
153
|
+
flushes += 1;
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
await runTrigger.call(fakeSession, 'resume');
|
|
161
|
+
assert(reminders.length === 1, 'runtime probe did not append exactly one reminder');
|
|
162
|
+
assert(flushes === 1, 'runtime probe did not flush exactly once');
|
|
163
|
+
assert(
|
|
164
|
+
reminders[0].text.includes('<hook_result hook_event="SessionStart">')
|
|
165
|
+
&& reminders[0].text.includes('AgentSpine ready: 152 sources indexed.'),
|
|
166
|
+
'runtime probe lost the AgentSpine SessionStart message',
|
|
167
|
+
);
|
|
168
|
+
assert(
|
|
169
|
+
reminders[0].origin?.kind === 'injection'
|
|
170
|
+
&& reminders[0].origin?.variant === 'session_start_hook',
|
|
171
|
+
'runtime probe used the wrong reminder origin',
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
process.stdout.write('session-start-hook-context-regression PASS\n');
|
|
175
|
+
})().catch((error) => {
|
|
176
|
+
process.stderr.write(`${error.stack || error.message}\n`);
|
|
177
|
+
process.exitCode = 1;
|
|
178
|
+
});
|