blun-king-cli 9.1.523 → 9.1.525
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 +13 -0
- package/LIESMICH.txt +4 -2
- package/README.md +4 -2
- package/agent-spine-plugin/.claude-plugin/marketplace.json +1 -1
- package/agent-spine-plugin/.claude-plugin/plugin.json +1 -1
- package/agent-spine-plugin/.codex-plugin/plugin.json +1 -2
- package/agent-spine-plugin/CHANGELOG.md +81 -14
- package/agent-spine-plugin/CONTRIBUTING.md +52 -0
- package/agent-spine-plugin/README.md +6 -4
- package/agent-spine-plugin/SECURITY.md +47 -0
- package/agent-spine-plugin/blun.plugin.json +1 -1
- package/agent-spine-plugin/docs/acceptance.md +2 -2
- package/agent-spine-plugin/docs/architecture.md +1 -1
- package/agent-spine-plugin/docs/gateway-runtime.md +2 -2
- package/agent-spine-plugin/docs/host-integration.md +2 -2
- package/agent-spine-plugin/docs/learning.md +37 -2
- package/agent-spine-plugin/docs/preflight-recall.md +1 -1
- package/agent-spine-plugin/docs/quality-gates.md +1 -1
- package/agent-spine-plugin/docs/relationships.md +1 -1
- package/agent-spine-plugin/docs/session-briefing.md +1 -1
- package/agent-spine-plugin/docs/source-roots.md +1 -1
- package/agent-spine-plugin/hooks/version.json +1 -1
- package/agent-spine-plugin/package.json +1 -1
- package/agent-spine-plugin/scripts/check-hosts.js +2 -2
- package/agent-spine-plugin/skills/agent-spine/SKILL.md +3 -3
- package/agent-spine-plugin/src/cli.js +69 -5
- package/agent-spine-plugin/src/hook.js +41 -23
- package/agent-spine-plugin/src/index.js +2 -1
- package/agent-spine-plugin/src/lib/acceptance.js +2 -3
- package/agent-spine-plugin/src/lib/attention.js +4 -4
- package/agent-spine-plugin/src/lib/audit.js +12 -3
- package/agent-spine-plugin/src/lib/authentication.js +4 -2
- package/agent-spine-plugin/src/lib/briefing.js +20 -8
- package/agent-spine-plugin/src/lib/catalog.js +28 -3
- package/agent-spine-plugin/src/lib/channel-runtime.js +2 -2
- package/agent-spine-plugin/src/lib/continuity.js +18 -13
- package/agent-spine-plugin/src/lib/documents.js +23 -5
- package/agent-spine-plugin/src/lib/feed-transport.js +4 -2
- package/agent-spine-plugin/src/lib/graph.js +47 -12
- package/agent-spine-plugin/src/lib/learning.js +422 -48
- package/agent-spine-plugin/src/lib/paths.js +50 -1
- package/agent-spine-plugin/src/lib/persona-runtime.js +20 -14
- package/agent-spine-plugin/src/lib/preflight.js +50 -26
- package/agent-spine-plugin/src/lib/source-roots.js +38 -7
- package/agent-spine-plugin/src/mcp.js +50 -4
- package/agent-spine-plugin/src/version.js +1 -1
- package/blun.mjs +91 -4
- package/package.json +6 -2
- package/scripts/check-bundled-agent-spine-regression.js +48 -0
- package/scripts/check-session-picker-resume-metrics-regression.js +97 -0
- package/scripts/check-shell-terminal-isolation-regression.js +81 -0
- package/scripts/check-telegram-loop-exactly-once-regression.js +71 -0
package/blun.mjs
CHANGED
|
@@ -316411,7 +316411,7 @@ function buildLocalSpawnOptions(isWindows, cwd, env) {
|
|
|
316411
316411
|
"pipe",
|
|
316412
316412
|
"pipe"
|
|
316413
316413
|
],
|
|
316414
|
-
detached:
|
|
316414
|
+
detached: true,
|
|
316415
316415
|
windowsHide: true
|
|
316416
316416
|
};
|
|
316417
316417
|
}
|
|
@@ -424165,7 +424165,8 @@ async function handleLoopCommand(host, args) {
|
|
|
424165
424165
|
host.setAppState?.({ loop });
|
|
424166
424166
|
host.track("loop_start", { interval: loop.interval });
|
|
424167
424167
|
host.showStatus(`/loop ${loop.status}`);
|
|
424168
|
-
if (
|
|
424168
|
+
if (host.telegramRemoteCommandContext !== void 0) host.sendNormalUserInput(loop.prompt);
|
|
424169
|
+
else if (isStreaming(host) || host.state.appState.isCompacting) host.enqueueNormalUserInput(loop.prompt);
|
|
424169
424170
|
else host.sendNormalUserInput(loop.prompt);
|
|
424170
424171
|
return;
|
|
424171
424172
|
}
|
|
@@ -500660,6 +500661,10 @@ registerUiCatalogFragment({
|
|
|
500660
500661
|
"sessionPicker.time.daysAgo": "{count}d ago",
|
|
500661
500662
|
"sessionPicker.current": "current",
|
|
500662
500663
|
"sessionPicker.imported": "imported",
|
|
500664
|
+
"sessionPicker.resume.metrics": "Resume {resumeSize} · {messages} messages · {tokens} tokens · Wire {wireSize}",
|
|
500665
|
+
"sessionPicker.resume.missing": "checkpoint missing",
|
|
500666
|
+
"sessionPicker.resume.stale": "checkpoint stale",
|
|
500667
|
+
"sessionPicker.resume.invalid": "checkpoint invalid",
|
|
500663
500668
|
"sessionPicker.footer.matches": "Showing {from}-{to} of {loaded} loaded / {matches} matches",
|
|
500664
500669
|
"sessionPicker.footer.sessions": "Showing {from}-{to} of {count} sessions",
|
|
500665
500670
|
"sessionPicker.footer.loadedSessions": "Showing {from}-{to} of {loaded} loaded / {total} sessions"
|
|
@@ -500684,6 +500689,10 @@ registerUiCatalogFragment({
|
|
|
500684
500689
|
"sessionPicker.time.daysAgo": "vor {count} T.",
|
|
500685
500690
|
"sessionPicker.current": "aktuell",
|
|
500686
500691
|
"sessionPicker.imported": "importiert",
|
|
500692
|
+
"sessionPicker.resume.metrics": "Resume {resumeSize} · {messages} Nachrichten · {tokens} Tokens · Wire {wireSize}",
|
|
500693
|
+
"sessionPicker.resume.missing": "Checkpoint fehlt",
|
|
500694
|
+
"sessionPicker.resume.stale": "Checkpoint veraltet",
|
|
500695
|
+
"sessionPicker.resume.invalid": "Checkpoint ungültig",
|
|
500687
500696
|
"sessionPicker.footer.matches": "Anzeige {from}-{to} · {loaded} geladen · {matches} Treffer",
|
|
500688
500697
|
"sessionPicker.footer.sessions": "Anzeige {from}-{to} von {count} Sitzungen",
|
|
500689
500698
|
"sessionPicker.footer.loadedSessions": "Anzeige {from}-{to} · {loaded} von {total} Sitzungen geladen"
|
|
@@ -500708,6 +500717,10 @@ registerUiCatalogFragment({
|
|
|
500708
500717
|
"sessionPicker.time.daysAgo": "hace {count} d",
|
|
500709
500718
|
"sessionPicker.current": "actual",
|
|
500710
500719
|
"sessionPicker.imported": "importada",
|
|
500720
|
+
"sessionPicker.resume.metrics": "Reanudación {resumeSize} · {messages} mensajes · {tokens} tokens · Wire {wireSize}",
|
|
500721
|
+
"sessionPicker.resume.missing": "Falta el punto de reanudación",
|
|
500722
|
+
"sessionPicker.resume.stale": "Punto de reanudación desactualizado",
|
|
500723
|
+
"sessionPicker.resume.invalid": "Punto de reanudación no válido",
|
|
500711
500724
|
"sessionPicker.footer.matches": "Mostrando {from}-{to} · {loaded} cargadas · {matches} coincidencias",
|
|
500712
500725
|
"sessionPicker.footer.sessions": "Mostrando {from}-{to} de {count} sesiones",
|
|
500713
500726
|
"sessionPicker.footer.loadedSessions": "Mostrando {from}-{to} · {loaded} de {total} sesiones cargadas"
|
|
@@ -500732,6 +500745,10 @@ registerUiCatalogFragment({
|
|
|
500732
500745
|
"sessionPicker.time.daysAgo": "il y a {count} j",
|
|
500733
500746
|
"sessionPicker.current": "actuelle",
|
|
500734
500747
|
"sessionPicker.imported": "importée",
|
|
500748
|
+
"sessionPicker.resume.metrics": "Reprise {resumeSize} · {messages} messages · {tokens} tokens · Wire {wireSize}",
|
|
500749
|
+
"sessionPicker.resume.missing": "Point de reprise manquant",
|
|
500750
|
+
"sessionPicker.resume.stale": "Point de reprise obsolète",
|
|
500751
|
+
"sessionPicker.resume.invalid": "Point de reprise non valide",
|
|
500735
500752
|
"sessionPicker.footer.matches": "Affichage de {from}-{to} · {loaded} chargées · {matches} résultats",
|
|
500736
500753
|
"sessionPicker.footer.sessions": "Affichage de {from}-{to} sur {count} sessions",
|
|
500737
500754
|
"sessionPicker.footer.loadedSessions": "Affichage de {from}-{to} · {loaded} sessions chargées sur {total}"
|
|
@@ -500756,6 +500773,10 @@ registerUiCatalogFragment({
|
|
|
500756
500773
|
"sessionPicker.time.daysAgo": "för {count} d sedan",
|
|
500757
500774
|
"sessionPicker.current": "aktuell",
|
|
500758
500775
|
"sessionPicker.imported": "importerad",
|
|
500776
|
+
"sessionPicker.resume.metrics": "Återupptagning {resumeSize} · {messages} meddelanden · {tokens} token · Wire {wireSize}",
|
|
500777
|
+
"sessionPicker.resume.missing": "Återupptagningspunkt saknas",
|
|
500778
|
+
"sessionPicker.resume.stale": "Återupptagningspunkt är inaktuell",
|
|
500779
|
+
"sessionPicker.resume.invalid": "Ogiltig återupptagningspunkt",
|
|
500759
500780
|
"sessionPicker.footer.matches": "Visar {from}-{to} · {loaded} inlästa · {matches} träffar",
|
|
500760
500781
|
"sessionPicker.footer.sessions": "Visar {from}-{to} av {count} sessioner",
|
|
500761
500782
|
"sessionPicker.footer.loadedSessions": "Visar {from}-{to} · {loaded} av {total} sessioner inlästa"
|
|
@@ -500780,6 +500801,10 @@ registerUiCatalogFragment({
|
|
|
500780
500801
|
"sessionPicker.time.daysAgo": "před {count} d",
|
|
500781
500802
|
"sessionPicker.current": "aktuální",
|
|
500782
500803
|
"sessionPicker.imported": "importováno",
|
|
500804
|
+
"sessionPicker.resume.metrics": "Obnovení {resumeSize} · {messages} zpráv · {tokens} tokenů · Wire {wireSize}",
|
|
500805
|
+
"sessionPicker.resume.missing": "Bod obnovení chybí",
|
|
500806
|
+
"sessionPicker.resume.stale": "Bod obnovení je zastaralý",
|
|
500807
|
+
"sessionPicker.resume.invalid": "Neplatný bod obnovení",
|
|
500783
500808
|
"sessionPicker.footer.matches": "Zobrazeno {from}–{to}; načteno {loaded}, shod {matches}",
|
|
500784
500809
|
"sessionPicker.footer.sessions": "Zobrazeno {from}–{to} z {count} relací",
|
|
500785
500810
|
"sessionPicker.footer.loadedSessions": "Zobrazeno {from}–{to}; načteno {loaded} z {total} relací"
|
|
@@ -501034,6 +501059,21 @@ var SessionPickerComponent = class extends Container {
|
|
|
501034
501059
|
const promptLine = indent + currentTheme.fg("textDim", promptMarker + promptText);
|
|
501035
501060
|
card.push(promptLine);
|
|
501036
501061
|
}
|
|
501062
|
+
const resume = session.resume;
|
|
501063
|
+
if (resume !== void 0) {
|
|
501064
|
+
if (resume.status === "missing") card.push(indent + currentTheme.fg("warning", uiText("sessionPicker.resume.missing")));
|
|
501065
|
+
else if (resume.status === "invalid") card.push(indent + currentTheme.fg("error", uiText("sessionPicker.resume.invalid")));
|
|
501066
|
+
else {
|
|
501067
|
+
const metrics = uiText("sessionPicker.resume.metrics", {
|
|
501068
|
+
resumeSize: formatByteSize(resume.resumeBytes),
|
|
501069
|
+
messages: resume.messages.toLocaleString(getCurrentUiLocale()),
|
|
501070
|
+
tokens: formatLiveTokenCount(resume.tokens),
|
|
501071
|
+
wireSize: formatByteSize(resume.wireBytes)
|
|
501072
|
+
});
|
|
501073
|
+
const suffix = resume.status === "stale" ? ` · ${uiText("sessionPicker.resume.stale")}` : "";
|
|
501074
|
+
card.push(indent + currentTheme.fg(resume.status === "stale" ? "warning" : "textMuted", truncateToWidth(metrics + suffix, Math.max(8, width - indentWidth), ELLIPSIS$4)));
|
|
501075
|
+
}
|
|
501076
|
+
}
|
|
501037
501077
|
return card;
|
|
501038
501078
|
}
|
|
501039
501079
|
};
|
|
@@ -516682,9 +516722,56 @@ function sessionRowsForPicker(sessions, currentSessionId, currentSessionHasConte
|
|
|
516682
516722
|
last_prompt: session.lastPrompt ?? null,
|
|
516683
516723
|
work_dir: session.workDir,
|
|
516684
516724
|
updated_at: session.updatedAt ?? session.createdAt ?? 0,
|
|
516685
|
-
metadata: session.metadata
|
|
516725
|
+
metadata: session.metadata,
|
|
516726
|
+
session_dir: session.sessionDir
|
|
516686
516727
|
}));
|
|
516687
516728
|
}
|
|
516729
|
+
const SESSION_RESUME_METRICS_CONCURRENCY = 8;
|
|
516730
|
+
const SESSION_RESUME_STALE_AFTER_MS = 6e4;
|
|
516731
|
+
async function statSessionFile(filePath) {
|
|
516732
|
+
try {
|
|
516733
|
+
return await stat(filePath);
|
|
516734
|
+
} catch {
|
|
516735
|
+
return null;
|
|
516736
|
+
}
|
|
516737
|
+
}
|
|
516738
|
+
async function readSessionResumeMetrics(session) {
|
|
516739
|
+
if (typeof session.session_dir !== "string" || session.session_dir.length === 0) return { status: "missing" };
|
|
516740
|
+
const agentDir = join(session.session_dir, "agents", "main");
|
|
516741
|
+
const wirePath = join(agentDir, "wire.jsonl");
|
|
516742
|
+
const checkpointPath = join(agentDir, "resume-checkpoint.json");
|
|
516743
|
+
const [resumeStat, wireStat] = await Promise.all([
|
|
516744
|
+
statSessionFile(checkpointPath),
|
|
516745
|
+
statSessionFile(wirePath)
|
|
516746
|
+
]);
|
|
516747
|
+
if (resumeStat === null) return { status: "missing" };
|
|
516748
|
+
if (wireStat === null) return { status: "invalid" };
|
|
516749
|
+
const checkpoint = await loadResumeCheckpoint({ wirePath }).catch(() => ({ ok: false }));
|
|
516750
|
+
if (!checkpoint.ok) return { status: "invalid" };
|
|
516751
|
+
const context = checkpoint.snapshot?.context;
|
|
516752
|
+
const messages = Array.isArray(context?.history) ? context.history.length : Number.isSafeInteger(context?.tokenCountCoveredMessageCount) ? context.tokenCountCoveredMessageCount : 0;
|
|
516753
|
+
const tokens = Number.isSafeInteger(context?.tokenCount) ? context.tokenCount : 0;
|
|
516754
|
+
const stale = wireStat.size > checkpoint.offset && wireStat.mtimeMs - resumeStat.mtimeMs > SESSION_RESUME_STALE_AFTER_MS;
|
|
516755
|
+
return {
|
|
516756
|
+
status: stale ? "stale" : "ok",
|
|
516757
|
+
resumeBytes: resumeStat.size,
|
|
516758
|
+
wireBytes: wireStat.size,
|
|
516759
|
+
messages,
|
|
516760
|
+
tokens
|
|
516761
|
+
};
|
|
516762
|
+
}
|
|
516763
|
+
async function enrichSessionRowsForPicker(rows) {
|
|
516764
|
+
let nextIndex = 0;
|
|
516765
|
+
const workerCount = Math.min(SESSION_RESUME_METRICS_CONCURRENCY, rows.length);
|
|
516766
|
+
await Promise.all(Array.from({ length: workerCount }, async () => {
|
|
516767
|
+
while (nextIndex < rows.length) {
|
|
516768
|
+
const index = nextIndex++;
|
|
516769
|
+
const row = rows[index];
|
|
516770
|
+
if (row !== void 0) row.resume = await readSessionResumeMetrics(row);
|
|
516771
|
+
}
|
|
516772
|
+
}));
|
|
516773
|
+
return rows;
|
|
516774
|
+
}
|
|
516688
516775
|
//#endregion
|
|
516689
516776
|
//#region src/tui/utils/startup.ts
|
|
516690
516777
|
function combineStartupNotice(existing, next) {
|
|
@@ -519889,7 +519976,7 @@ var BlunTUI = class {
|
|
|
519889
519976
|
this.state.sessionsScope = scope;
|
|
519890
519977
|
try {
|
|
519891
519978
|
const sessions = scope === "all" ? await this.harness.listSessions({}) : await this.harness.listSessions({ workDir: this.state.appState.workDir });
|
|
519892
|
-
this.state.sessions = sessionRowsForPicker(sessions, this.state.appState.sessionId, this.hasSessionContent());
|
|
519979
|
+
this.state.sessions = await enrichSessionRowsForPicker(sessionRowsForPicker(sessions, this.state.appState.sessionId, this.hasSessionContent()));
|
|
519893
519980
|
} catch {} finally {
|
|
519894
519981
|
this.state.loadingSessions = false;
|
|
519895
519982
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "blun-king-cli",
|
|
3
|
-
"version": "9.1.
|
|
3
|
+
"version": "9.1.525",
|
|
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-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-approval-observability-regression.js && node scripts/check-slash-escape-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-shell-terminal-isolation-regression.js && node scripts/check-todo-loop-regression.js && node scripts/check-telegram-loop-exactly-once-regression.js && node scripts/check-session-picker-resume-metrics-regression.js && node scripts/check-bundled-agent-spine-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-approval-observability-regression.js && node scripts/check-slash-escape-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
|
},
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
"scripts/check-package-regression.js",
|
|
39
39
|
"scripts/check-active-profile-plugin-startup.js",
|
|
40
40
|
"scripts/check-approval-queue-shortcuts-regression.js",
|
|
41
|
+
"scripts/check-bundled-agent-spine-regression.js",
|
|
41
42
|
"scripts/check-approval-observability-regression.js",
|
|
42
43
|
"scripts/check-slash-escape-regression.js",
|
|
43
44
|
"scripts/check-mcp-startup-wait-budget.js",
|
|
@@ -46,9 +47,12 @@
|
|
|
46
47
|
"scripts/check-telegram-bridge-watchdog.js",
|
|
47
48
|
"scripts/check-resume-replay-regression.js",
|
|
48
49
|
"scripts/check-session-cancel-regression.js",
|
|
50
|
+
"scripts/check-shell-terminal-isolation-regression.js",
|
|
51
|
+
"scripts/check-session-picker-resume-metrics-regression.js",
|
|
49
52
|
"scripts/check-release-metadata.js",
|
|
50
53
|
"scripts/check-historical-tool-result-preview-regression.js",
|
|
51
54
|
"scripts/check-session-start-hook-context-regression.js",
|
|
55
|
+
"scripts/check-telegram-loop-exactly-once-regression.js",
|
|
52
56
|
"scripts/check-todo-recovery-catalog-regression.js",
|
|
53
57
|
"scripts/check-todo-loop-regression.js",
|
|
54
58
|
"scripts/fix-node-pty-perms.js",
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
const assert = require('node:assert/strict');
|
|
2
|
+
const { execFileSync } = require('node:child_process');
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
|
|
6
|
+
const expectedVersion = '0.11.4';
|
|
7
|
+
const pluginRoot = process.env.BLUN_AGENT_SPINE_ROOT_UNDER_TEST
|
|
8
|
+
? path.resolve(process.env.BLUN_AGENT_SPINE_ROOT_UNDER_TEST)
|
|
9
|
+
: path.resolve(__dirname, '..', 'agent-spine-plugin');
|
|
10
|
+
|
|
11
|
+
function readJson(relativePath) {
|
|
12
|
+
return JSON.parse(fs.readFileSync(path.join(pluginRoot, relativePath), 'utf8'));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
for (const relativePath of [
|
|
16
|
+
'package.json',
|
|
17
|
+
'hooks/version.json',
|
|
18
|
+
'.claude-plugin/plugin.json',
|
|
19
|
+
'.codex-plugin/plugin.json',
|
|
20
|
+
'blun.plugin.json',
|
|
21
|
+
]) {
|
|
22
|
+
assert.equal(
|
|
23
|
+
readJson(relativePath).version,
|
|
24
|
+
expectedVersion,
|
|
25
|
+
`BUNDLED_AGENT_SPINE_REGRESSION: ${relativePath} must declare ${expectedVersion}`,
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const runtimeVersion = execFileSync(
|
|
30
|
+
process.execPath,
|
|
31
|
+
[path.join(pluginRoot, 'bin', 'agentspine.js'), '--version'],
|
|
32
|
+
{ encoding: 'utf8' },
|
|
33
|
+
).trim();
|
|
34
|
+
assert.equal(
|
|
35
|
+
runtimeVersion,
|
|
36
|
+
expectedVersion,
|
|
37
|
+
'BUNDLED_AGENT_SPINE_REGRESSION: packaged runtime version is stale',
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
for (const relativePath of ['CONTRIBUTING.md', 'SECURITY.md']) {
|
|
41
|
+
assert.equal(
|
|
42
|
+
fs.existsSync(path.join(pluginRoot, relativePath)),
|
|
43
|
+
true,
|
|
44
|
+
`BUNDLED_AGENT_SPINE_REGRESSION: README link target ${relativePath} is missing`,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
console.log(`bundled-agent-spine-regression PASS (${expectedVersion})`);
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
const assert = require('node:assert/strict');
|
|
2
|
+
const fs = require('node:fs');
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
|
|
5
|
+
const bundlePath = process.env.BLUN_BUNDLE_UNDER_TEST
|
|
6
|
+
? path.resolve(process.env.BLUN_BUNDLE_UNDER_TEST)
|
|
7
|
+
: path.join(__dirname, '..', 'blun.mjs');
|
|
8
|
+
const bundle = fs.readFileSync(bundlePath, 'utf8');
|
|
9
|
+
|
|
10
|
+
const rowsMatch = /function sessionRowsForPicker\(sessions, currentSessionId, currentSessionHasContent\) \{([\s\S]*?)\n\}/.exec(bundle);
|
|
11
|
+
assert(rowsMatch, 'SESSION_PICKER_RESUME_METRICS: sessionRowsForPicker is missing');
|
|
12
|
+
const sessionRowsForPicker = Function(
|
|
13
|
+
`return function sessionRowsForPicker(sessions, currentSessionId, currentSessionHasContent) {${rowsMatch[1]}\n};`,
|
|
14
|
+
)();
|
|
15
|
+
|
|
16
|
+
const rows = sessionRowsForPicker([{
|
|
17
|
+
id: 'session-a',
|
|
18
|
+
title: 'A',
|
|
19
|
+
lastPrompt: 'work',
|
|
20
|
+
workDir: 'C:/work',
|
|
21
|
+
updatedAt: 1,
|
|
22
|
+
metadata: {},
|
|
23
|
+
sessionDir: 'C:/state/session-a',
|
|
24
|
+
}], 'other', true);
|
|
25
|
+
assert.equal(rows[0].session_dir, 'C:/state/session-a');
|
|
26
|
+
|
|
27
|
+
const metricsMatch = /async function readSessionResumeMetrics\(session\) \{([\s\S]*?)\n\}\nasync function enrichSessionRowsForPicker/.exec(bundle);
|
|
28
|
+
assert(metricsMatch, 'SESSION_PICKER_RESUME_METRICS: readSessionResumeMetrics is missing');
|
|
29
|
+
|
|
30
|
+
function buildReader({ resumeStat, wireStat, checkpoint }) {
|
|
31
|
+
return Function(
|
|
32
|
+
'join',
|
|
33
|
+
'statSessionFile',
|
|
34
|
+
'loadResumeCheckpoint',
|
|
35
|
+
'SESSION_RESUME_STALE_AFTER_MS',
|
|
36
|
+
`return async function readSessionResumeMetrics(session) {${metricsMatch[1]}\n};`,
|
|
37
|
+
)(
|
|
38
|
+
path.join,
|
|
39
|
+
async (filePath) => filePath.endsWith('resume-checkpoint.json') ? resumeStat : wireStat,
|
|
40
|
+
async () => checkpoint,
|
|
41
|
+
60_000,
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
(async () => {
|
|
46
|
+
const missing = await buildReader({ resumeStat: null, wireStat: null, checkpoint: null })({
|
|
47
|
+
session_dir: 'C:/state/session-a',
|
|
48
|
+
});
|
|
49
|
+
assert.deepEqual(missing, { status: 'missing' });
|
|
50
|
+
|
|
51
|
+
const invalid = await buildReader({
|
|
52
|
+
resumeStat: { size: 1_024, mtimeMs: 1_000 },
|
|
53
|
+
wireStat: { size: 2_048, mtimeMs: 1_000 },
|
|
54
|
+
checkpoint: { ok: false },
|
|
55
|
+
})({ session_dir: 'C:/state/session-a' });
|
|
56
|
+
assert.deepEqual(invalid, { status: 'invalid' });
|
|
57
|
+
|
|
58
|
+
const ok = await buildReader({
|
|
59
|
+
resumeStat: { size: 1_024, mtimeMs: 1_000 },
|
|
60
|
+
wireStat: { size: 2_048, mtimeMs: 1_500 },
|
|
61
|
+
checkpoint: {
|
|
62
|
+
ok: true,
|
|
63
|
+
offset: 2_048,
|
|
64
|
+
snapshot: { context: { history: [{}, {}, {}], tokenCount: 1_200 } },
|
|
65
|
+
},
|
|
66
|
+
})({ session_dir: 'C:/state/session-a' });
|
|
67
|
+
assert.deepEqual(ok, {
|
|
68
|
+
status: 'ok',
|
|
69
|
+
resumeBytes: 1_024,
|
|
70
|
+
wireBytes: 2_048,
|
|
71
|
+
messages: 3,
|
|
72
|
+
tokens: 1_200,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const stale = await buildReader({
|
|
76
|
+
resumeStat: { size: 1_024, mtimeMs: 1_000 },
|
|
77
|
+
wireStat: { size: 4_096, mtimeMs: 62_000 },
|
|
78
|
+
checkpoint: {
|
|
79
|
+
ok: true,
|
|
80
|
+
offset: 2_048,
|
|
81
|
+
snapshot: { context: { history: [{}], tokenCount: 500 } },
|
|
82
|
+
},
|
|
83
|
+
})({ session_dir: 'C:/state/session-a' });
|
|
84
|
+
assert.equal(stale.status, 'stale');
|
|
85
|
+
assert.equal(stale.wireBytes, 4_096);
|
|
86
|
+
|
|
87
|
+
const renderBlock = /const resume = session\.resume;([\s\S]*?)\n\t\treturn card;/.exec(bundle);
|
|
88
|
+
assert(renderBlock, 'SESSION_PICKER_RESUME_METRICS: resume metrics are not rendered');
|
|
89
|
+
assert.match(renderBlock[1], /formatByteSize\(resume\.resumeBytes\)/);
|
|
90
|
+
assert.match(renderBlock[1], /formatByteSize\(resume\.wireBytes\)/);
|
|
91
|
+
assert.match(renderBlock[1], /sessionPicker\.resume\.stale/);
|
|
92
|
+
|
|
93
|
+
console.log('session-picker-resume-metrics-regression PASS');
|
|
94
|
+
})().catch((error) => {
|
|
95
|
+
console.error(error);
|
|
96
|
+
process.exitCode = 1;
|
|
97
|
+
});
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const assert = require('node:assert/strict');
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
const vm = require('node:vm');
|
|
7
|
+
const { spawn } = require('node:child_process');
|
|
8
|
+
|
|
9
|
+
const packageRoot = path.resolve(__dirname, '..');
|
|
10
|
+
const bundlePath = process.argv[2] === undefined
|
|
11
|
+
? path.join(packageRoot, 'blun.mjs')
|
|
12
|
+
: path.resolve(process.argv[2]);
|
|
13
|
+
const bundle = fs.readFileSync(bundlePath, 'utf8');
|
|
14
|
+
const source = /function buildLocalSpawnOptions\(isWindows, cwd, env\) \{[\s\S]*?\n\}/.exec(bundle)?.[0];
|
|
15
|
+
|
|
16
|
+
assert(source, 'SHELL_TERMINAL_ISOLATION: buildLocalSpawnOptions is missing');
|
|
17
|
+
|
|
18
|
+
const context = {};
|
|
19
|
+
vm.runInNewContext(`${source}; globalThis.buildLocalSpawnOptions = buildLocalSpawnOptions;`, context);
|
|
20
|
+
const buildLocalSpawnOptions = context.buildLocalSpawnOptions;
|
|
21
|
+
const cwd = packageRoot;
|
|
22
|
+
const env = { ...process.env, BLUN_SHELL_ISOLATION_PROBE: '1' };
|
|
23
|
+
|
|
24
|
+
for (const isWindows of [false, true]) {
|
|
25
|
+
const options = buildLocalSpawnOptions(isWindows, cwd, env);
|
|
26
|
+
assert.deepEqual(Array.from(options.stdio), ['pipe', 'pipe', 'pipe']);
|
|
27
|
+
assert.equal(options.detached, true,
|
|
28
|
+
`local shell must start in a separate process group when isWindows=${String(isWindows)}`);
|
|
29
|
+
assert.equal(options.windowsHide, true);
|
|
30
|
+
assert.equal(options.cwd, cwd);
|
|
31
|
+
assert.equal(options.env, env);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function runCapturedProcessProbe() {
|
|
35
|
+
const options = buildLocalSpawnOptions(process.platform === 'win32', cwd, env);
|
|
36
|
+
const child = spawn(process.execPath, [
|
|
37
|
+
'-e',
|
|
38
|
+
[
|
|
39
|
+
"process.stdin.setEncoding('utf8')",
|
|
40
|
+
"process.stdin.on('data', () => { process.exitCode = 90 })",
|
|
41
|
+
"process.stdin.on('end', () => {",
|
|
42
|
+
" process.stdout.write('stdout-ok')",
|
|
43
|
+
" process.stderr.write('stderr-ok')",
|
|
44
|
+
'})',
|
|
45
|
+
].join(';'),
|
|
46
|
+
], options);
|
|
47
|
+
|
|
48
|
+
let stdout = '';
|
|
49
|
+
let stderr = '';
|
|
50
|
+
child.stdout.setEncoding('utf8');
|
|
51
|
+
child.stderr.setEncoding('utf8');
|
|
52
|
+
child.stdout.on('data', (chunk) => { stdout += chunk; });
|
|
53
|
+
child.stderr.on('data', (chunk) => { stderr += chunk; });
|
|
54
|
+
child.stdin.end();
|
|
55
|
+
|
|
56
|
+
const exit = await new Promise((resolve, reject) => {
|
|
57
|
+
const timer = setTimeout(() => {
|
|
58
|
+
child.kill();
|
|
59
|
+
reject(new Error('captured child did not observe stdin EOF'));
|
|
60
|
+
}, 10_000);
|
|
61
|
+
child.once('error', (error) => {
|
|
62
|
+
clearTimeout(timer);
|
|
63
|
+
reject(error);
|
|
64
|
+
});
|
|
65
|
+
child.once('close', (code, signal) => {
|
|
66
|
+
clearTimeout(timer);
|
|
67
|
+
resolve({ code, signal });
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
assert.deepEqual(exit, { code: 0, signal: null });
|
|
72
|
+
assert.equal(stdout, 'stdout-ok');
|
|
73
|
+
assert.equal(stderr, 'stderr-ok');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
runCapturedProcessProbe().then(() => {
|
|
77
|
+
process.stdout.write('shell-terminal-isolation-regression PASS\n');
|
|
78
|
+
}).catch((error) => {
|
|
79
|
+
console.error(error);
|
|
80
|
+
process.exitCode = 1;
|
|
81
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
const assert = require('node:assert/strict');
|
|
2
|
+
const fs = require('node:fs');
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
|
|
5
|
+
const bundlePath = process.env.BLUN_BUNDLE_UNDER_TEST
|
|
6
|
+
? path.resolve(process.env.BLUN_BUNDLE_UNDER_TEST)
|
|
7
|
+
: path.join(__dirname, '..', 'blun.mjs');
|
|
8
|
+
const bundle = fs.readFileSync(bundlePath, 'utf8');
|
|
9
|
+
const match = /async function handleLoopCommand\(host, args\) \{([\s\S]*?)\n\}\nfunction hasRunnableSession/.exec(bundle);
|
|
10
|
+
assert(match, 'TELEGRAM_LOOP_EXACTLY_ONCE: handleLoopCommand is missing');
|
|
11
|
+
|
|
12
|
+
const handleLoopCommand = Function(
|
|
13
|
+
'parseLoopCommand',
|
|
14
|
+
'hasRunnableSession',
|
|
15
|
+
'llmNotSetMessage',
|
|
16
|
+
'isStreaming',
|
|
17
|
+
`return async function handleLoopCommand(host, args) {${match[1]}\n};`,
|
|
18
|
+
)(
|
|
19
|
+
() => ({ kind: 'create', interval: '*/5 * * * *', prompt: 'work once' }),
|
|
20
|
+
() => true,
|
|
21
|
+
() => 'missing model',
|
|
22
|
+
(host) => host.streaming,
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
function createHost({ telegram = false, streaming = true } = {}) {
|
|
26
|
+
const calls = { send: [], enqueue: [] };
|
|
27
|
+
const session = {
|
|
28
|
+
async createLoop({ interval, prompt }) {
|
|
29
|
+
return { interval, prompt, status: 'active' };
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
const host = {
|
|
33
|
+
streaming,
|
|
34
|
+
session,
|
|
35
|
+
telegramRemoteCommandContext: telegram ? { item: { telegramCommandName: 'loop' } } : undefined,
|
|
36
|
+
state: { appState: { isCompacting: false, model: 'blun/king' } },
|
|
37
|
+
requireSession() { return session; },
|
|
38
|
+
setAppState() {},
|
|
39
|
+
track() {},
|
|
40
|
+
showStatus() {},
|
|
41
|
+
showError(message) { throw new Error(message); },
|
|
42
|
+
sendNormalUserInput(text) { calls.send.push(text); },
|
|
43
|
+
enqueueNormalUserInput(text) { calls.enqueue.push(text); },
|
|
44
|
+
};
|
|
45
|
+
return { host, calls };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
(async () => {
|
|
49
|
+
const telegram = createHost({ telegram: true, streaming: true });
|
|
50
|
+
await handleLoopCommand(telegram.host, '5 min work once');
|
|
51
|
+
assert.deepEqual(telegram.calls.send, ['work once']);
|
|
52
|
+
assert.deepEqual(
|
|
53
|
+
telegram.calls.enqueue,
|
|
54
|
+
[],
|
|
55
|
+
'TELEGRAM_LOOP_EXACTLY_ONCE: Telegram loop payload must not survive in the normal queue',
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
const local = createHost({ telegram: false, streaming: true });
|
|
59
|
+
await handleLoopCommand(local.host, '5 min work once');
|
|
60
|
+
assert.deepEqual(local.calls.send, []);
|
|
61
|
+
assert.deepEqual(
|
|
62
|
+
local.calls.enqueue,
|
|
63
|
+
['work once'],
|
|
64
|
+
'TELEGRAM_LOOP_EXACTLY_ONCE: local busy-loop behavior must stay queued',
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
console.log('telegram-loop-exactly-once-regression PASS');
|
|
68
|
+
})().catch((error) => {
|
|
69
|
+
console.error(error);
|
|
70
|
+
process.exitCode = 1;
|
|
71
|
+
});
|