blun-king-cli 9.1.515 → 9.1.517
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 +17 -0
- package/LIESMICH.txt +2 -2
- package/README.md +2 -2
- package/bin/launcher-runtime.js +7 -30
- package/bin/plugin-bootstrap.js +13 -2
- package/bin/todo-list-turn-policy.cjs +21 -0
- package/blun.mjs +99 -35
- package/package.json +10 -2
- package/scripts/check-active-profile-plugin-startup.js +36 -0
- package/scripts/check-approval-queue-shortcuts-regression.js +65 -0
- package/scripts/check-mcp-startup-wait-budget.js +48 -0
- package/scripts/check-plugin-startup-regression.js +53 -0
- package/scripts/check-queue-controls-regression.js +189 -0
- package/scripts/check-resume-replay-regression.js +100 -0
- package/scripts/check-session-cancel-regression.js +43 -0
- package/scripts/check-telegram-bridge-watchdog.js +60 -0
- package/scripts/check-todo-loop-regression.js +52 -1
- package/README.md.vor-91515-20260831-064010 +0 -821
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 9.1.517 - 2026-08-31
|
|
4
|
+
|
|
5
|
+
- Keeps `Ctrl+S` and `Escape` queue controls available while an approval panel is open: `Ctrl+S` flushes all waiting messages, while `Escape` releases one waiting message before rejecting the approval when the queue is empty.
|
|
6
|
+
- Observes asynchronous session cancellation in the keyboard, Telegram `/reload`, and slash-command paths, treating `session.not_found` as an already-stopped session instead of crashing the core with an unhandled rejection.
|
|
7
|
+
- Adds red-to-green package gates for the raw Windows control bytes and the measured stale-session cancellation failure.
|
|
8
|
+
|
|
9
|
+
## 9.1.516 - 2026-08-31
|
|
10
|
+
|
|
11
|
+
- Caps the first tool-capable turn's wait for the initial MCP catalogue at 2.5 seconds, so a cold optional desktop server can finish in the background instead of blocking Read, Bash, Grep, TodoList, Telegram, and AgentSpine.
|
|
12
|
+
- Verifies and installs AgentSpine only for the active profile at startup; dormant profiles are verified when they are launched, preventing startup time from growing with abandoned proof profiles.
|
|
13
|
+
- Reuses the immutable packaged plugin source snapshot across managed-plugin checks while still reading and byte-verifying every active installed plugin tree, and logs each MCP server's startup status for reproducible timing evidence.
|
|
14
|
+
- Retires an active-steer queue entry exactly once as soon as the core has durably accepted its `turn.steer`, instead of restoring an already delivered message after a 15-second UI confirmation timeout; genuine rejections remain queued.
|
|
15
|
+
- Restarts an owned Telegram bridge after an unexpected exit or launch error with one deduplicated timer, while ownership loss and normal shutdown still prevent respawn.
|
|
16
|
+
- Reconstructs a missing assistant step when a persisted `content.part` or tool call follows full compaction without its `step.begin`, preserving the continuation instead of making `king -c` fail during replay.
|
|
17
|
+
- Allows only the first TodoList write in a model step, preventing parallel list replacements from racing while leaving read-only list queries untouched.
|
|
18
|
+
- Extends the package gates for Ctrl+S, Escape, durable queue acknowledgement, rejection replay, stable urgent/direct/addressed/bot/normal priority, Todo lifecycle, visible pending markers, valid initial Todo state, and single-bridge recovery.
|
|
19
|
+
|
|
3
20
|
## 9.1.515 - 2026-08-31
|
|
4
21
|
|
|
5
22
|
- Accepts a truthful unchanged TodoList during mandatory maintenance instead of terminating the turn after five identical retries.
|
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.517
|
|
13
13
|
|
|
14
14
|
AgentSpine 0.10.1
|
|
15
15
|
-----------------
|
|
16
|
-
Version 9.1.
|
|
16
|
+
Version 9.1.517 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.517
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
## AgentSpine 0.10.1
|
|
16
16
|
|
|
17
|
-
Version 9.1.
|
|
17
|
+
Version 9.1.517 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
|
|
package/bin/launcher-runtime.js
CHANGED
|
@@ -282,36 +282,13 @@ function installAgentSpineForProfile(packageRoot, blunDir) {
|
|
|
282
282
|
installManagedAgentSpinePlugin({ packageRoot, blunDir });
|
|
283
283
|
}
|
|
284
284
|
|
|
285
|
-
function installAgentSpineForExistingProfiles(packageRoot,
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
const
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
};
|
|
293
|
-
|
|
294
|
-
addProfileHome(currentProfileHome);
|
|
295
|
-
if (fs.existsSync(profileRoot)) {
|
|
296
|
-
for (const entry of fs.readdirSync(profileRoot, { withFileTypes: true })) {
|
|
297
|
-
if (!entry.isDirectory() || entry.isSymbolicLink()) continue;
|
|
298
|
-
let profileHome;
|
|
299
|
-
try {
|
|
300
|
-
profileHome = resolveProfilePaths(sharedHome, entry.name).home;
|
|
301
|
-
} catch {
|
|
302
|
-
continue;
|
|
303
|
-
}
|
|
304
|
-
const stat = fs.lstatSync(profileHome);
|
|
305
|
-
if (!stat.isDirectory() || stat.isSymbolicLink()) continue;
|
|
306
|
-
addProfileHome(profileHome);
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
for (const profileHome of profileHomes.values()) {
|
|
311
|
-
ensurePrivateDirectory(profileHome);
|
|
312
|
-
installAgentSpineForProfile(packageRoot, profileHome);
|
|
313
|
-
}
|
|
314
|
-
return [...profileHomes.values()];
|
|
285
|
+
function installAgentSpineForExistingProfiles(packageRoot, _sharedHome, currentProfileHome) {
|
|
286
|
+
// A dormant profile is verified when that profile is launched. Rescanning all
|
|
287
|
+
// profiles here makes every startup slower as old profiles accumulate.
|
|
288
|
+
const profileHome = path.resolve(currentProfileHome);
|
|
289
|
+
ensurePrivateDirectory(profileHome);
|
|
290
|
+
installAgentSpineForProfile(packageRoot, profileHome);
|
|
291
|
+
return [profileHome];
|
|
315
292
|
}
|
|
316
293
|
|
|
317
294
|
function createManagedNodeEnvironment(binary, env = process.env, platform = process.platform) {
|
package/bin/plugin-bootstrap.js
CHANGED
|
@@ -11,6 +11,8 @@ const {
|
|
|
11
11
|
writePrivateFile,
|
|
12
12
|
} = require('./private-paths');
|
|
13
13
|
|
|
14
|
+
const managedPluginSourceSnapshots = new Map();
|
|
15
|
+
|
|
14
16
|
function isObject(value) {
|
|
15
17
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
16
18
|
}
|
|
@@ -42,9 +44,18 @@ function snapshotPluginTree(root) {
|
|
|
42
44
|
return visit(root) ? entries : null;
|
|
43
45
|
}
|
|
44
46
|
|
|
47
|
+
function snapshotManagedPluginSource(root) {
|
|
48
|
+
const resolvedRoot = path.resolve(root);
|
|
49
|
+
const cacheKey = process.platform === 'win32' ? resolvedRoot.toLowerCase() : resolvedRoot;
|
|
50
|
+
if (!managedPluginSourceSnapshots.has(cacheKey)) {
|
|
51
|
+
managedPluginSourceSnapshots.set(cacheKey, snapshotPluginTree(resolvedRoot));
|
|
52
|
+
}
|
|
53
|
+
return managedPluginSourceSnapshots.get(cacheKey);
|
|
54
|
+
}
|
|
55
|
+
|
|
45
56
|
function pluginTreesAreIdentical(sourceRoot, pluginRoot) {
|
|
46
57
|
try {
|
|
47
|
-
const source =
|
|
58
|
+
const source = snapshotManagedPluginSource(sourceRoot);
|
|
48
59
|
const installed = snapshotPluginTree(pluginRoot);
|
|
49
60
|
if (source === null || installed === null || source.length !== installed.length) return false;
|
|
50
61
|
return source.every((entry, index) => {
|
|
@@ -59,7 +70,7 @@ function pluginTreesAreIdentical(sourceRoot, pluginRoot) {
|
|
|
59
70
|
}
|
|
60
71
|
|
|
61
72
|
function pluginTreeDigest(root) {
|
|
62
|
-
const entries =
|
|
73
|
+
const entries = snapshotManagedPluginSource(root);
|
|
63
74
|
if (entries === null) throw new Error(`Invalid managed plugin tree: ${root}`);
|
|
64
75
|
const hash = createHash('sha256');
|
|
65
76
|
for (const entry of entries) {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const TODO_LIST_TOOL_NAME = 'TodoList';
|
|
4
|
+
|
|
5
|
+
function enforceSingleTodoWritePerStep(context) {
|
|
6
|
+
if (context?.toolCall?.name !== TODO_LIST_TOOL_NAME) return undefined;
|
|
7
|
+
if (!Array.isArray(context.args?.todos)) return undefined;
|
|
8
|
+
|
|
9
|
+
const todoWrites = Array.isArray(context.toolCalls)
|
|
10
|
+
? context.toolCalls.filter((call) => call?.name === TODO_LIST_TOOL_NAME)
|
|
11
|
+
: [];
|
|
12
|
+
if (todoWrites.length <= 1) return undefined;
|
|
13
|
+
if (context.toolCall.id === todoWrites[0]?.id) return undefined;
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
block: true,
|
|
17
|
+
reason: 'Only one TodoList write is allowed per model step. Use the first update, then continue from its stored result in the next step.',
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
module.exports = { enforceSingleTodoWritePerStep };
|
package/blun.mjs
CHANGED
|
@@ -79167,6 +79167,25 @@ var init_context$2 = __esmMin((() => {
|
|
|
79167
79167
|
closeAbandonedToolExchange(output) {
|
|
79168
79168
|
return this.closePendingToolResults(output).length;
|
|
79169
79169
|
}
|
|
79170
|
+
recoverMissingOpenStep(event) {
|
|
79171
|
+
const resolvedStepUuid = resolveStepEventUuid(this.openSteps, event);
|
|
79172
|
+
const existingStep = this.openSteps.get(resolvedStepUuid);
|
|
79173
|
+
if (existingStep !== void 0) return existingStep;
|
|
79174
|
+
const recoveredStepUuid = event.stepUuid ?? `recovered-${this._history.length}-${event.type}`;
|
|
79175
|
+
const message = {
|
|
79176
|
+
role: "assistant",
|
|
79177
|
+
content: [],
|
|
79178
|
+
toolCalls: []
|
|
79179
|
+
};
|
|
79180
|
+
this.pushHistory(message);
|
|
79181
|
+
this.openSteps.set(recoveredStepUuid, message);
|
|
79182
|
+
this.agent.log.warn("recovered loop event without step.begin", {
|
|
79183
|
+
eventType: event.type,
|
|
79184
|
+
stepUuid: event.stepUuid,
|
|
79185
|
+
restoring: this.agent.records.restoring !== null
|
|
79186
|
+
});
|
|
79187
|
+
return message;
|
|
79188
|
+
}
|
|
79170
79189
|
appendLoopEvent(event) {
|
|
79171
79190
|
this.agent.records.logRecord({
|
|
79172
79191
|
type: "context.append_loop_event",
|
|
@@ -79208,17 +79227,13 @@ var init_context$2 = __esmMin((() => {
|
|
|
79208
79227
|
return;
|
|
79209
79228
|
}
|
|
79210
79229
|
case "content.part": {
|
|
79211
|
-
const
|
|
79212
|
-
const openStep = this.openSteps.get(stepEventUuid);
|
|
79213
|
-
if (openStep === void 0) throw new Error(`Received content_part for unknown step_uuid '${stepEventUuid}' (no open step_begin)`);
|
|
79230
|
+
const openStep = this.recoverMissingOpenStep(event);
|
|
79214
79231
|
openStep.content.push(event.part);
|
|
79215
79232
|
this.markPendingTokenEstimateDirty();
|
|
79216
79233
|
return;
|
|
79217
79234
|
}
|
|
79218
79235
|
case "tool.call": {
|
|
79219
|
-
const
|
|
79220
|
-
const openStep = this.openSteps.get(stepEventUuid);
|
|
79221
|
-
if (openStep === void 0) throw new Error(`Received tool_call for unknown step_uuid '${stepEventUuid}' (no open step_begin)`);
|
|
79236
|
+
const openStep = this.recoverMissingOpenStep(event);
|
|
79222
79237
|
openStep.toolCalls.push({
|
|
79223
79238
|
type: "function",
|
|
79224
79239
|
id: event.toolCallId,
|
|
@@ -261610,6 +261625,13 @@ function blunTurnNeedsInitialMcp(input, origin) {
|
|
|
261610
261625
|
if (blunTurnNeedsTools(input, origin)) return true;
|
|
261611
261626
|
return BLUN_TELEGRAM_CHANNEL_RE.test(blunExtractText(input)) || blunTurnHasAttachment(input);
|
|
261612
261627
|
}
|
|
261628
|
+
async function blunWaitForInitialMcpBudget(mcp, signal) {
|
|
261629
|
+
if (mcp === void 0) return;
|
|
261630
|
+
await Promise.race([
|
|
261631
|
+
mcp.waitForInitialLoad(signal),
|
|
261632
|
+
new Promise((resolve) => setTimeout(resolve, 2500))
|
|
261633
|
+
]);
|
|
261634
|
+
}
|
|
261613
261635
|
function blunMessageTextChars(message) {
|
|
261614
261636
|
return message.content.reduce((total, part) => total + (part.type === "text" ? part.text.length : 0), 0);
|
|
261615
261637
|
}
|
|
@@ -261919,7 +261941,7 @@ function durablePromptOrigin(origin) {
|
|
|
261919
261941
|
void externalReportSources;
|
|
261920
261942
|
return durableOrigin;
|
|
261921
261943
|
}
|
|
261922
|
-
var BLUN_CORE_TOOL_NAMES, BLUN_LEAN_TOOL_NAMES, BLUN_ATTACHMENT_MARKER_RE, BLUN_TELEGRAM_OUTBOUND_TOOL_RE, BLUN_TELEGRAM_CHANNEL_RE, BLUN_TOOL_BUDGET_RATIO, createDeferredToolLoader, mediaToolNamesForTurnText, rankedSupportToolNamesForGoal, rankedToolNamesForTurnText, rememberDeferredToolAfterNotFound, toolSchemaBudgetTokens, projectRecurringCronHistory, TelegramDeliveryLedger, createRuntimeCognitiveTurnLifecycle, cognitiveFocusScopesForTurn, buildCognitiveWorkFocus, buildAttentionQueueItem, LLM_NOT_SET_MESSAGE, GOAL_CONTINUATION_ORIGIN, GOAL_COMPLETION_REMINDER_NAME, GOAL_BLOCKED_REMINDER_NAME, GOAL_RATE_LIMIT_PAUSE_REASON, GOAL_PROVIDER_CONNECTION_PAUSE_PREFIX, GOAL_PROVIDER_AUTH_PAUSE_PREFIX, GOAL_PROVIDER_API_PAUSE_PREFIX, GOAL_MODEL_CONFIG_PAUSE_PREFIX, GOAL_RUNTIME_PAUSE_PREFIX, GOAL_PROVIDER_FILTERED_PAUSE_REASON, GOAL_CONTINUATION_PROMPT, TurnFlow;
|
|
261944
|
+
var BLUN_CORE_TOOL_NAMES, BLUN_LEAN_TOOL_NAMES, BLUN_ATTACHMENT_MARKER_RE, BLUN_TELEGRAM_OUTBOUND_TOOL_RE, BLUN_TELEGRAM_CHANNEL_RE, BLUN_TOOL_BUDGET_RATIO, createDeferredToolLoader, mediaToolNamesForTurnText, rankedSupportToolNamesForGoal, rankedToolNamesForTurnText, rememberDeferredToolAfterNotFound, toolSchemaBudgetTokens, enforceSingleTodoWritePerStep, projectRecurringCronHistory, TelegramDeliveryLedger, createRuntimeCognitiveTurnLifecycle, cognitiveFocusScopesForTurn, buildCognitiveWorkFocus, buildAttentionQueueItem, LLM_NOT_SET_MESSAGE, GOAL_CONTINUATION_ORIGIN, GOAL_COMPLETION_REMINDER_NAME, GOAL_BLOCKED_REMINDER_NAME, GOAL_RATE_LIMIT_PAUSE_REASON, GOAL_PROVIDER_CONNECTION_PAUSE_PREFIX, GOAL_PROVIDER_AUTH_PAUSE_PREFIX, GOAL_PROVIDER_API_PAUSE_PREFIX, GOAL_MODEL_CONFIG_PAUSE_PREFIX, GOAL_RUNTIME_PAUSE_PREFIX, GOAL_PROVIDER_FILTERED_PAUSE_REASON, GOAL_CONTINUATION_PROMPT, TurnFlow;
|
|
261923
261945
|
var init_turn = __esmMin((() => {
|
|
261924
261946
|
init_dist$4();
|
|
261925
261947
|
init_src$4();
|
|
@@ -261937,6 +261959,7 @@ var init_turn = __esmMin((() => {
|
|
|
261937
261959
|
init_user_message_offload();
|
|
261938
261960
|
init_assistant_message_offload();
|
|
261939
261961
|
({ CORE_TOOL_NAMES: BLUN_CORE_TOOL_NAMES, createDeferredToolLoader, mediaToolNamesForTurnText, rankedSupportToolNamesForGoal, rankedToolNamesForTurnText, rememberDeferredToolAfterNotFound, toolSchemaBudgetTokens } = createRequire(import.meta.url)("./bin/turn-tool-performance-policy.cjs"));
|
|
261962
|
+
({ enforceSingleTodoWritePerStep } = createRequire(import.meta.url)("./bin/todo-list-turn-policy.cjs"));
|
|
261940
261963
|
({ projectRecurringCronHistory } = createRequire(import.meta.url)("./bin/recurring-cron-history-policy.cjs"));
|
|
261941
261964
|
({ TelegramDeliveryLedger } = createRequire(import.meta.url)("./bin/repeated-user-message-projection.cjs"));
|
|
261942
261965
|
({ createRuntimeCognitiveTurnLifecycle } = createRequire(import.meta.url)("./bin/cognitive-turn-lifecycle.cjs"));
|
|
@@ -262750,7 +262773,7 @@ var init_turn = __esmMin((() => {
|
|
|
262750
262773
|
let goalOutcomeMessageContinuationUsed = false;
|
|
262751
262774
|
const directReplyTurnStop = createDirectReplyTurnStop(blunExtractText(input));
|
|
262752
262775
|
const deduper = new ToolCallDeduplicator({ telemetry: this.agent.telemetry });
|
|
262753
|
-
if (blunTurnNeedsInitialMcp(input, origin)) await this.agent.mcp
|
|
262776
|
+
if (blunTurnNeedsInitialMcp(input, origin)) await blunWaitForInitialMcpBudget(this.agent.mcp, signal);
|
|
262754
262777
|
const personalMemoryRecall = await this.agent.injection.injectPersonalMemoryForTurn(turnId, input, origin, signal);
|
|
262755
262778
|
await this.agent.injection.injectGoal();
|
|
262756
262779
|
const cognitiveProjection = this.projectCognitiveState(turnId, input);
|
|
@@ -262953,6 +262976,8 @@ var init_turn = __esmMin((() => {
|
|
|
262953
262976
|
return { continue: false };
|
|
262954
262977
|
},
|
|
262955
262978
|
prepareToolExecution: async (ctx) => {
|
|
262979
|
+
const todoTurnPolicy = enforceSingleTodoWritePerStep(ctx);
|
|
262980
|
+
if (todoTurnPolicy !== void 0) return todoTurnPolicy;
|
|
262956
262981
|
const goalTodoPolicy = enforceGoalTodoPolicy(this.agent, ctx);
|
|
262957
262982
|
if (goalTodoPolicy !== void 0) return goalTodoPolicy;
|
|
262958
262983
|
const ideaPolicy = enforceIdeaToolPolicy(this.agent, ctx);
|
|
@@ -298308,6 +298333,11 @@ var init_session$1 = __esmMin((() => {
|
|
|
298308
298333
|
});
|
|
298309
298334
|
}
|
|
298310
298335
|
onMcpServerStatusChange(entry) {
|
|
298336
|
+
this.log.info("mcp server startup status", {
|
|
298337
|
+
server: entry.name,
|
|
298338
|
+
status: entry.status,
|
|
298339
|
+
toolCount: entry.toolCount
|
|
298340
|
+
});
|
|
298311
298341
|
this.rpc.emitEvent({
|
|
298312
298342
|
type: "mcp.server.status",
|
|
298313
298343
|
agentId: "main",
|
|
@@ -419872,7 +419902,7 @@ const OWNERSHIP_SETTLE_MS = 50;
|
|
|
419872
419902
|
const OWNERSHIP_HANDOFF_WARNING_MS = 5e3;
|
|
419873
419903
|
const BRIDGE_STOP_TIMEOUT_MS = 2500;
|
|
419874
419904
|
const BRIDGE_FORCE_STOP_TIMEOUT_MS = 500;
|
|
419875
|
-
const
|
|
419905
|
+
const BRIDGE_RESTART_DELAY_MS = 1e3;
|
|
419876
419906
|
const CHANNEL_DELIVERY_MAX_FAILURES = 3;
|
|
419877
419907
|
const BLUN_NODE_FALLBACK_SUBCOMMAND = "__plugin_run_node";
|
|
419878
419908
|
function isBlunNativeBinary(execPath) {
|
|
@@ -419990,6 +420020,7 @@ var TelegramChannelController = class {
|
|
|
419990
420020
|
ownershipTimer;
|
|
419991
420021
|
activationTimer;
|
|
419992
420022
|
handoffTimer;
|
|
420023
|
+
bridgeRestartTimer;
|
|
419993
420024
|
offset = 0;
|
|
419994
420025
|
checkpointOffset = 0;
|
|
419995
420026
|
queueFileId = "";
|
|
@@ -420006,6 +420037,7 @@ var TelegramChannelController = class {
|
|
|
420006
420037
|
spawnBridge;
|
|
420007
420038
|
bridgeStopTimeoutMs;
|
|
420008
420039
|
bridgeForceStopTimeoutMs;
|
|
420040
|
+
bridgeRestartDelayMs;
|
|
420009
420041
|
ownerId;
|
|
420010
420042
|
ownershipPollMs;
|
|
420011
420043
|
ownershipSettleMs;
|
|
@@ -420017,6 +420049,7 @@ var TelegramChannelController = class {
|
|
|
420017
420049
|
this.spawnBridge = options.spawnBridge ?? spawn;
|
|
420018
420050
|
this.bridgeStopTimeoutMs = options.bridgeStopTimeoutMs ?? BRIDGE_STOP_TIMEOUT_MS;
|
|
420019
420051
|
this.bridgeForceStopTimeoutMs = options.bridgeForceStopTimeoutMs ?? BRIDGE_FORCE_STOP_TIMEOUT_MS;
|
|
420052
|
+
this.bridgeRestartDelayMs = options.bridgeRestartDelayMs ?? BRIDGE_RESTART_DELAY_MS;
|
|
420020
420053
|
this.ownerId = options.ownerId ?? randomUUID();
|
|
420021
420054
|
this.ownershipPollMs = options.ownershipPollMs ?? OWNERSHIP_POLL_MS;
|
|
420022
420055
|
this.ownershipSettleMs = options.ownershipSettleMs ?? OWNERSHIP_SETTLE_MS;
|
|
@@ -420192,6 +420225,7 @@ var TelegramChannelController = class {
|
|
|
420192
420225
|
if (this.ownershipTimer !== void 0) clearInterval(this.ownershipTimer);
|
|
420193
420226
|
if (this.activationTimer !== void 0) clearTimeout(this.activationTimer);
|
|
420194
420227
|
if (this.handoffTimer !== void 0) clearTimeout(this.handoffTimer);
|
|
420228
|
+
if (this.bridgeRestartTimer !== void 0) clearTimeout(this.bridgeRestartTimer);
|
|
420195
420229
|
try {
|
|
420196
420230
|
if (parseInt(readFileSync(this.leaseFile, "utf8"), 10) === process.pid) rmSync(this.leaseFile);
|
|
420197
420231
|
} catch {}
|
|
@@ -420362,6 +420396,15 @@ var TelegramChannelController = class {
|
|
|
420362
420396
|
* (it will see our fresh lease and route to us). Otherwise start one bridge
|
|
420363
420397
|
* owned by this controller; shutdown never signals a pre-existing process.
|
|
420364
420398
|
*/
|
|
420399
|
+
scheduleBridgeRestart() {
|
|
420400
|
+
if (this.stopped || !this.activeOwner || !this.ownsChannel() || this.bridgeRestartTimer !== void 0) return;
|
|
420401
|
+
this.bridgeRestartTimer = setTimeout(() => {
|
|
420402
|
+
this.bridgeRestartTimer = void 0;
|
|
420403
|
+
if (this.stopped || !this.activeOwner || !this.ownsChannel()) return;
|
|
420404
|
+
this.ensureBridgeRunning();
|
|
420405
|
+
}, this.bridgeRestartDelayMs);
|
|
420406
|
+
this.bridgeRestartTimer.unref?.();
|
|
420407
|
+
}
|
|
420365
420408
|
ensureBridgeRunning() {
|
|
420366
420409
|
if (this.liveBridgePid() !== void 0) return;
|
|
420367
420410
|
const bridge = this.resolveBridgeEntry();
|
|
@@ -420384,14 +420427,17 @@ var TelegramChannelController = class {
|
|
|
420384
420427
|
child.once("exit", () => {
|
|
420385
420428
|
this.removeOwnedBridgePid(child.pid);
|
|
420386
420429
|
if (this.ownedBridge === child) this.ownedBridge = void 0;
|
|
420430
|
+
this.scheduleBridgeRestart();
|
|
420387
420431
|
});
|
|
420388
420432
|
child.once("error", (error) => {
|
|
420389
420433
|
this.removeOwnedBridgePid(child.pid);
|
|
420390
420434
|
if (this.ownedBridge === child) this.ownedBridge = void 0;
|
|
420391
420435
|
if (!this.stopped) this.host.warn(uiText("telegramChannel.bridgeStartFailed", { error: String(error) }));
|
|
420436
|
+
this.scheduleBridgeRestart();
|
|
420392
420437
|
});
|
|
420393
420438
|
} catch (error) {
|
|
420394
420439
|
this.host.warn(uiText("telegramChannel.bridgeStartFailed", { error: String(error) }));
|
|
420440
|
+
this.scheduleBridgeRestart();
|
|
420395
420441
|
}
|
|
420396
420442
|
}
|
|
420397
420443
|
resolveBridgeEntry() {
|
|
@@ -499153,12 +499199,16 @@ var ApprovalPanelComponent = class extends Container {
|
|
|
499153
499199
|
request;
|
|
499154
499200
|
onToggleToolOutput;
|
|
499155
499201
|
onOpenPreview;
|
|
499156
|
-
|
|
499202
|
+
onCtrlS;
|
|
499203
|
+
onEscape;
|
|
499204
|
+
constructor(request, onResponse, onToggleToolOutput, onOpenPreview, onCtrlS, onEscape) {
|
|
499157
499205
|
super();
|
|
499158
499206
|
this.request = request;
|
|
499159
499207
|
this.onResponse = onResponse;
|
|
499160
499208
|
this.onToggleToolOutput = onToggleToolOutput;
|
|
499161
499209
|
this.onOpenPreview = onOpenPreview;
|
|
499210
|
+
this.onCtrlS = onCtrlS;
|
|
499211
|
+
this.onEscape = onEscape;
|
|
499162
499212
|
this.feedbackInput.onSubmit = (value) => {
|
|
499163
499213
|
this.submit(this.selectedIndex, value);
|
|
499164
499214
|
};
|
|
@@ -499185,7 +499235,16 @@ var ApprovalPanelComponent = class extends Container {
|
|
|
499185
499235
|
} else this.submit(index);
|
|
499186
499236
|
}
|
|
499187
499237
|
handleInput(data) {
|
|
499188
|
-
if (matchesKey(data, Key.escape)
|
|
499238
|
+
if (matchesKey(data, Key.escape)) {
|
|
499239
|
+
if (this.onEscape?.() === true) return;
|
|
499240
|
+
this.onResponse({ response: "rejected" });
|
|
499241
|
+
return;
|
|
499242
|
+
}
|
|
499243
|
+
if (matchesKey(data, Key.ctrl("s"))) {
|
|
499244
|
+
this.onCtrlS?.();
|
|
499245
|
+
return;
|
|
499246
|
+
}
|
|
499247
|
+
if (matchesKey(data, Key.ctrl("c")) || matchesKey(data, Key.ctrl("d"))) {
|
|
499189
499248
|
this.onResponse({ response: "rejected" });
|
|
499190
499249
|
return;
|
|
499191
499250
|
}
|
|
@@ -505941,7 +506000,10 @@ var EditorKeyboardController = class {
|
|
|
505941
506000
|
}
|
|
505942
506001
|
cancelCurrentStream() {
|
|
505943
506002
|
this.host.cancelRunningShellCommand();
|
|
505944
|
-
this.host.session?.cancel()
|
|
506003
|
+
this.host.session?.cancel().catch((error) => {
|
|
506004
|
+
if (error instanceof BlunError && error.code === ErrorCodes.SESSION_NOT_FOUND) return;
|
|
506005
|
+
this.host.showError(formatErrorMessage$2(error));
|
|
506006
|
+
});
|
|
505945
506007
|
}
|
|
505946
506008
|
cancelCurrentCompaction() {
|
|
505947
506009
|
const session = this.host.session;
|
|
@@ -518332,21 +518394,7 @@ var BlunTUI = class {
|
|
|
518332
518394
|
try {
|
|
518333
518395
|
const result = await this.harness.withInteractiveAgent(item.agentId ?? "main", () => session.steerActive(input, expectedTurnId === void 0 ? {} : { expectedTurnId }));
|
|
518334
518396
|
if (!result.accepted) return this.recoverRejectedActiveSteer(inFlight);
|
|
518335
|
-
inFlight
|
|
518336
|
-
inFlight.accepted = true;
|
|
518337
|
-
for (const queued of items) this.traceTelegramDelivery?.({ stage: "accepted", ...queued.channelDeliveryTrace, route: "active_steer", turnId: inFlight.turnId });
|
|
518338
|
-
if (result.duplicate === true) {
|
|
518339
|
-
inFlight.duplicate = true;
|
|
518340
|
-
return this.commitQueuedSteer(inFlight);
|
|
518341
|
-
}
|
|
518342
|
-
if (inFlight.turnEnded) return restoreHead();
|
|
518343
|
-
if (!inFlight.stepStarted) {
|
|
518344
|
-
inFlight.confirmationTimer = setTimeout(() => {
|
|
518345
|
-
if (this.queueSteerInFlight === inFlight && inFlight.accepted && !inFlight.stepStarted) restoreHead(new Error("channel steer confirmation timed out"));
|
|
518346
|
-
}, CHANNEL_STEER_CONFIRM_TIMEOUT_MS);
|
|
518347
|
-
inFlight.confirmationTimer.unref?.();
|
|
518348
|
-
}
|
|
518349
|
-
this.commitQueuedSteerIfReady(inFlight);
|
|
518397
|
+
return this.acceptQueuedSteer(inFlight, result, true);
|
|
518350
518398
|
} catch (error) {
|
|
518351
518399
|
return restoreHead(error);
|
|
518352
518400
|
}
|
|
@@ -518462,6 +518510,15 @@ var BlunTUI = class {
|
|
|
518462
518510
|
this.updateQueueDisplay();
|
|
518463
518511
|
this.state.ui.requestRender();
|
|
518464
518512
|
}
|
|
518513
|
+
acceptQueuedSteer(inFlight, result, traceDelivery = false) {
|
|
518514
|
+
if (this.queueSteerInFlight !== inFlight || !result.accepted) return false;
|
|
518515
|
+
inFlight.turnId = String(result.turnId);
|
|
518516
|
+
inFlight.accepted = true;
|
|
518517
|
+
inFlight.duplicate = result.duplicate === true;
|
|
518518
|
+
if (traceDelivery) for (const queued of inFlight.items) this.traceTelegramDelivery?.({ stage: "accepted", ...queued.channelDeliveryTrace, route: "active_steer", turnId: inFlight.turnId });
|
|
518519
|
+
this.commitQueuedSteer(inFlight);
|
|
518520
|
+
return true;
|
|
518521
|
+
}
|
|
518465
518522
|
commitQueuedSteerIfReady(inFlight) {
|
|
518466
518523
|
if (this.queueSteerInFlight !== inFlight || !inFlight.accepted || !inFlight.stepStarted || inFlight.turnEnded) return;
|
|
518467
518524
|
this.commitQueuedSteer(inFlight);
|
|
@@ -518647,12 +518704,7 @@ var BlunTUI = class {
|
|
|
518647
518704
|
session.steerActive(items.map((item) => item.text.trim()).join("\n\n"), expectedTurnId === void 0 ? {} : { expectedTurnId }).then((result) => {
|
|
518648
518705
|
if (this.queueSteerInFlight !== inFlight) return;
|
|
518649
518706
|
if (!result.accepted) this.recoverRejectedActiveSteer(inFlight);
|
|
518650
|
-
else
|
|
518651
|
-
inFlight.turnId = String(result.turnId);
|
|
518652
|
-
inFlight.accepted = true;
|
|
518653
|
-
if (inFlight.turnEnded) this.restoreQueuedSteer(inFlight);
|
|
518654
|
-
else this.commitQueuedSteerIfReady(inFlight);
|
|
518655
|
-
}
|
|
518707
|
+
else this.acceptQueuedSteer(inFlight, result);
|
|
518656
518708
|
}).catch((error) => {
|
|
518657
518709
|
this.restoreQueuedSteer(inFlight, error);
|
|
518658
518710
|
});
|
|
@@ -518847,7 +518899,10 @@ var BlunTUI = class {
|
|
|
518847
518899
|
const activeTurn = this.streamingUI?.hasActiveTurn?.() === true || typeof phase === "string" && phase !== "idle" || this.state.appState.isCompacting === true;
|
|
518848
518900
|
if (command.name === "reload" && activeTurn && !this.queueCommandRunning) {
|
|
518849
518901
|
this.state.queuedMessages.unshift(item);
|
|
518850
|
-
this.session?.cancel()
|
|
518902
|
+
this.session?.cancel().catch((error) => {
|
|
518903
|
+
if (error instanceof BlunError && error.code === ErrorCodes.SESSION_NOT_FOUND) return;
|
|
518904
|
+
this.showError(formatErrorMessage$2(error));
|
|
518905
|
+
});
|
|
518851
518906
|
this.traceTelegramDelivery?.({ stage: "queued", ...deliveryTrace, route: "channel_command", priority: "preemptive", queueDepth: this.state.queuedMessages.length });
|
|
518852
518907
|
this.track("input_queue", { kind: "channel-command-preemptive" });
|
|
518853
518908
|
this.updateQueueDisplay();
|
|
@@ -519360,7 +519415,10 @@ var BlunTUI = class {
|
|
|
519360
519415
|
if (activeTurn) {
|
|
519361
519416
|
this.state.queuedMessages.unshift(item);
|
|
519362
519417
|
this.preserveQueueAcrossSessionReset = true;
|
|
519363
|
-
this.session?.cancel()
|
|
519418
|
+
this.session?.cancel().catch((error) => {
|
|
519419
|
+
if (error instanceof BlunError && error.code === ErrorCodes.SESSION_NOT_FOUND) return;
|
|
519420
|
+
this.showError(formatErrorMessage$2(error));
|
|
519421
|
+
});
|
|
519364
519422
|
this.track("input_queue", { kind: "command-preemptive" });
|
|
519365
519423
|
this.updateQueueDisplay();
|
|
519366
519424
|
this.state.ui.requestRender();
|
|
@@ -520860,6 +520918,12 @@ var BlunTUI = class {
|
|
|
520860
520918
|
this.toggleToolOutputExpansion();
|
|
520861
520919
|
}, (block) => {
|
|
520862
520920
|
this.openApprovalPreview(panel, block);
|
|
520921
|
+
}, () => {
|
|
520922
|
+
this.flushQueuedMessages(this.state.editor.getText().trim(), this.state.editor.inputMode);
|
|
520923
|
+
this.updateQueueDisplay();
|
|
520924
|
+
this.state.ui.requestRender();
|
|
520925
|
+
}, () => {
|
|
520926
|
+
return this.releaseNextQueuedMessage();
|
|
520863
520927
|
});
|
|
520864
520928
|
this.activeApprovalPanel = panel;
|
|
520865
520929
|
this.mountEditorReplacement(panel);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "blun-king-cli",
|
|
3
|
-
"version": "9.1.
|
|
3
|
+
"version": "9.1.517",
|
|
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",
|
|
12
|
+
"prepack": "node scripts/check-release-metadata.js && node scripts/check-todo-loop-regression.js && node scripts/check-queue-controls-regression.js && node scripts/check-approval-queue-shortcuts-regression.js && node scripts/check-telegram-bridge-watchdog.js && node scripts/check-resume-replay-regression.js && node scripts/check-session-cancel-regression.js && node scripts/check-plugin-startup-regression.js && node scripts/check-active-profile-plugin-startup.js && node scripts/check-mcp-startup-wait-budget.js",
|
|
13
13
|
"release:verify": "node scripts/check-release-metadata.js --external",
|
|
14
14
|
"postinstall": "node scripts/fix-node-pty-perms.js"
|
|
15
15
|
},
|
|
@@ -36,6 +36,14 @@
|
|
|
36
36
|
"native/",
|
|
37
37
|
"release-planned-removals.json",
|
|
38
38
|
"scripts/check-package-regression.js",
|
|
39
|
+
"scripts/check-active-profile-plugin-startup.js",
|
|
40
|
+
"scripts/check-approval-queue-shortcuts-regression.js",
|
|
41
|
+
"scripts/check-mcp-startup-wait-budget.js",
|
|
42
|
+
"scripts/check-plugin-startup-regression.js",
|
|
43
|
+
"scripts/check-queue-controls-regression.js",
|
|
44
|
+
"scripts/check-telegram-bridge-watchdog.js",
|
|
45
|
+
"scripts/check-resume-replay-regression.js",
|
|
46
|
+
"scripts/check-session-cancel-regression.js",
|
|
39
47
|
"scripts/check-release-metadata.js",
|
|
40
48
|
"scripts/check-todo-loop-regression.js",
|
|
41
49
|
"scripts/fix-node-pty-perms.js",
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const assert = require('node:assert/strict');
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const os = require('node:os');
|
|
6
|
+
const path = require('node:path');
|
|
7
|
+
|
|
8
|
+
const { installAgentSpineForExistingProfiles } = require('../bin/launcher-runtime');
|
|
9
|
+
|
|
10
|
+
const packageRoot = path.resolve(__dirname, '..');
|
|
11
|
+
const temporaryRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'blun-active-profile-plugin-'));
|
|
12
|
+
const sharedHome = path.join(temporaryRoot, '.blun');
|
|
13
|
+
const currentProfile = path.join(sharedHome, 'profile', 'current');
|
|
14
|
+
const dormantProfile = path.join(sharedHome, 'profile', 'dormant');
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
fs.mkdirSync(currentProfile, { recursive: true });
|
|
18
|
+
fs.mkdirSync(dormantProfile, { recursive: true });
|
|
19
|
+
|
|
20
|
+
installAgentSpineForExistingProfiles(packageRoot, sharedHome, currentProfile);
|
|
21
|
+
|
|
22
|
+
assert.equal(
|
|
23
|
+
fs.existsSync(path.join(currentProfile, 'plugins', 'installed.json')),
|
|
24
|
+
true,
|
|
25
|
+
'the active profile must receive the managed AgentSpine plugin',
|
|
26
|
+
);
|
|
27
|
+
assert.equal(
|
|
28
|
+
fs.existsSync(path.join(dormantProfile, 'plugins', 'installed.json')),
|
|
29
|
+
false,
|
|
30
|
+
'startup must not rescan or mutate dormant profiles',
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
process.stdout.write('active profile plugin startup regression: PASS\n');
|
|
34
|
+
} finally {
|
|
35
|
+
fs.rmSync(temporaryRoot, { recursive: true, force: true });
|
|
36
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
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.join(__dirname, '..', 'blun.mjs');
|
|
7
|
+
const bundle = fs.readFileSync(bundlePath, 'utf8');
|
|
8
|
+
|
|
9
|
+
function between(start, end, from = 0) {
|
|
10
|
+
const startIndex = bundle.indexOf(start, from);
|
|
11
|
+
assert.notEqual(startIndex, -1, `missing bundle marker: ${start}`);
|
|
12
|
+
const endIndex = bundle.indexOf(end, startIndex + start.length);
|
|
13
|
+
assert.notEqual(endIndex, -1, `missing bundle marker: ${end}`);
|
|
14
|
+
return bundle.slice(startIndex, endIndex);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const panelStart = bundle.indexOf('var ApprovalPanelComponent = class extends Container {');
|
|
18
|
+
assert.notEqual(panelStart, -1, 'ApprovalPanelComponent must exist');
|
|
19
|
+
const handleSource = between('\n\thandleInput(data) {', '\n\trender(width) {', panelStart).trim();
|
|
20
|
+
|
|
21
|
+
const Key = {
|
|
22
|
+
escape: 'escape',
|
|
23
|
+
ctrl(key) { return `ctrl+${key}`; },
|
|
24
|
+
};
|
|
25
|
+
const matchesKey = (data, expected) => {
|
|
26
|
+
if (expected === 'escape') return data === '\x1b';
|
|
27
|
+
if (expected === 'ctrl+s') return data === '\x13';
|
|
28
|
+
return data === expected;
|
|
29
|
+
};
|
|
30
|
+
const handleInput = Function(
|
|
31
|
+
'matchesKey',
|
|
32
|
+
'Key',
|
|
33
|
+
`"use strict"; return ({${handleSource}}).handleInput;`,
|
|
34
|
+
)(matchesKey, Key);
|
|
35
|
+
|
|
36
|
+
let responses = 0;
|
|
37
|
+
let flushed = 0;
|
|
38
|
+
let released = 0;
|
|
39
|
+
const panel = {
|
|
40
|
+
choiceCount() { return 0; },
|
|
41
|
+
onResponse(response) {
|
|
42
|
+
responses += 1;
|
|
43
|
+
assert.equal(response.response, 'rejected');
|
|
44
|
+
},
|
|
45
|
+
onCtrlS() { flushed += 1; },
|
|
46
|
+
onEscape() { released += 1; return true; },
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
handleInput.call(panel, '\x13');
|
|
50
|
+
assert.equal(flushed, 1, 'Ctrl+S must reach the global queue flush while approval is open');
|
|
51
|
+
assert.equal(responses, 0);
|
|
52
|
+
|
|
53
|
+
handleInput.call(panel, '\x1b');
|
|
54
|
+
assert.equal(released, 1, 'Escape must first release one queued message while approval is open');
|
|
55
|
+
assert.equal(responses, 0, 'a released queued message must not reject the approval');
|
|
56
|
+
|
|
57
|
+
panel.onEscape = () => false;
|
|
58
|
+
handleInput.call(panel, '\x1b');
|
|
59
|
+
assert.equal(responses, 1, 'Escape must reject the approval when no queued message waits');
|
|
60
|
+
|
|
61
|
+
const showApprovalPanel = between('\n\tshowApprovalPanel(payload) {', '\n\thideApprovalPanel() {');
|
|
62
|
+
assert.match(showApprovalPanel, /this\.flushQueuedMessages\(this\.state\.editor\.getText\(\)\.trim\(\), this\.state\.editor\.inputMode\)/);
|
|
63
|
+
assert.match(showApprovalPanel, /return this\.releaseNextQueuedMessage\(\)/);
|
|
64
|
+
|
|
65
|
+
console.log('approval queue shortcuts regression: PASS');
|
|
@@ -0,0 +1,48 @@
|
|
|
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
|
+
|
|
8
|
+
const bundlePath = path.resolve(__dirname, '..', 'blun.mjs');
|
|
9
|
+
const source = fs.readFileSync(bundlePath, 'utf8');
|
|
10
|
+
const match = source.match(
|
|
11
|
+
/async function blunWaitForInitialMcpBudget\(mcp, signal\) \{[\s\S]*?\n\}/u,
|
|
12
|
+
);
|
|
13
|
+
assert.ok(match, 'the bundle must contain the bounded initial MCP wait helper');
|
|
14
|
+
assert.match(
|
|
15
|
+
source,
|
|
16
|
+
/await blunWaitForInitialMcpBudget\(this\.agent\.mcp, signal\)/u,
|
|
17
|
+
'tool-capable turns must use the bounded initial MCP wait helper',
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
const context = vm.createContext({ setTimeout });
|
|
21
|
+
vm.runInContext(`${match[0]}; globalThis.waitForInitialMcp = blunWaitForInitialMcpBudget;`, context);
|
|
22
|
+
|
|
23
|
+
let initialLoadFinished = false;
|
|
24
|
+
const mcp = {
|
|
25
|
+
waitForInitialLoad() {
|
|
26
|
+
return new Promise((resolve) => {
|
|
27
|
+
const timer = setTimeout(() => {
|
|
28
|
+
initialLoadFinished = true;
|
|
29
|
+
resolve();
|
|
30
|
+
}, 20_000);
|
|
31
|
+
timer.unref();
|
|
32
|
+
});
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
(async () => {
|
|
37
|
+
const startedAt = performance.now();
|
|
38
|
+
await context.waitForInitialMcp(mcp, undefined);
|
|
39
|
+
const elapsedMs = performance.now() - startedAt;
|
|
40
|
+
|
|
41
|
+
assert.ok(elapsedMs >= 2_300, `wait budget returned too early: ${elapsedMs} ms`);
|
|
42
|
+
assert.ok(elapsedMs < 3_200, `slow optional MCP blocked the turn: ${elapsedMs} ms`);
|
|
43
|
+
assert.equal(initialLoadFinished, false, 'the slow MCP must continue loading in the background');
|
|
44
|
+
process.stdout.write(`MCP startup wait regression: PASS (${Math.round(elapsedMs)} ms)\n`);
|
|
45
|
+
})().catch((error) => {
|
|
46
|
+
process.stderr.write(`${error.stack || error}\n`);
|
|
47
|
+
process.exitCode = 1;
|
|
48
|
+
});
|