blun-king-cli 9.1.514 → 9.1.516
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/README.md.vor-91515-20260831-064010 +821 -0
- 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 +67 -38
- package/package.json +9 -2
- package/scripts/check-active-profile-plugin-startup.js +36 -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-telegram-bridge-watchdog.js +60 -0
- package/scripts/check-todo-loop-regression.js +78 -0
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
|
@@ -77249,7 +77249,7 @@ var init_manager$3 = __esmMin((() => {
|
|
|
77249
77249
|
this.scheduler = createCronScheduler({
|
|
77250
77250
|
clocks: this.clocks,
|
|
77251
77251
|
source: () => this.store.list(),
|
|
77252
|
-
isIdle: (
|
|
77252
|
+
isIdle: () => !agent.turn.hasActiveTurn,
|
|
77253
77253
|
isKilled: () => process.env["BLUN_DISABLE_CRON"] === "1",
|
|
77254
77254
|
onFire: (task, ctx) => {
|
|
77255
77255
|
this.handleFire(task, ctx);
|
|
@@ -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() {
|
|
@@ -423460,13 +423506,6 @@ function enforceGoalTodoPolicy(agent, context) {
|
|
|
423460
423506
|
};
|
|
423461
423507
|
}
|
|
423462
423508
|
progress.allVisibleWorkCompleted = nextTodos.length > 0 && nextTodos.every((todo) => todo?.status === "done");
|
|
423463
|
-
if (progress.refreshRequired) {
|
|
423464
|
-
const normalized = (value) => value.map((todo) => ({ title: String(todo?.title ?? "").trim(), status: todo?.status }));
|
|
423465
|
-
if (JSON.stringify(normalized(nextTodos)) === JSON.stringify(normalized(todos))) return {
|
|
423466
|
-
block: true,
|
|
423467
|
-
reason: "TodoList is unchanged after extended work. Mark evidenced progress, advance the active item, or refine its title to the concrete verified substep before continuing."
|
|
423468
|
-
};
|
|
423469
|
-
}
|
|
423470
423509
|
progress.workCallsSinceRefresh = 0;
|
|
423471
423510
|
progress.refreshRequired = false;
|
|
423472
423511
|
return;
|
|
@@ -518339,21 +518378,7 @@ var BlunTUI = class {
|
|
|
518339
518378
|
try {
|
|
518340
518379
|
const result = await this.harness.withInteractiveAgent(item.agentId ?? "main", () => session.steerActive(input, expectedTurnId === void 0 ? {} : { expectedTurnId }));
|
|
518341
518380
|
if (!result.accepted) return this.recoverRejectedActiveSteer(inFlight);
|
|
518342
|
-
inFlight
|
|
518343
|
-
inFlight.accepted = true;
|
|
518344
|
-
for (const queued of items) this.traceTelegramDelivery?.({ stage: "accepted", ...queued.channelDeliveryTrace, route: "active_steer", turnId: inFlight.turnId });
|
|
518345
|
-
if (result.duplicate === true) {
|
|
518346
|
-
inFlight.duplicate = true;
|
|
518347
|
-
return this.commitQueuedSteer(inFlight);
|
|
518348
|
-
}
|
|
518349
|
-
if (inFlight.turnEnded) return restoreHead();
|
|
518350
|
-
if (!inFlight.stepStarted) {
|
|
518351
|
-
inFlight.confirmationTimer = setTimeout(() => {
|
|
518352
|
-
if (this.queueSteerInFlight === inFlight && inFlight.accepted && !inFlight.stepStarted) restoreHead(new Error("channel steer confirmation timed out"));
|
|
518353
|
-
}, CHANNEL_STEER_CONFIRM_TIMEOUT_MS);
|
|
518354
|
-
inFlight.confirmationTimer.unref?.();
|
|
518355
|
-
}
|
|
518356
|
-
this.commitQueuedSteerIfReady(inFlight);
|
|
518381
|
+
return this.acceptQueuedSteer(inFlight, result, true);
|
|
518357
518382
|
} catch (error) {
|
|
518358
518383
|
return restoreHead(error);
|
|
518359
518384
|
}
|
|
@@ -518469,6 +518494,15 @@ var BlunTUI = class {
|
|
|
518469
518494
|
this.updateQueueDisplay();
|
|
518470
518495
|
this.state.ui.requestRender();
|
|
518471
518496
|
}
|
|
518497
|
+
acceptQueuedSteer(inFlight, result, traceDelivery = false) {
|
|
518498
|
+
if (this.queueSteerInFlight !== inFlight || !result.accepted) return false;
|
|
518499
|
+
inFlight.turnId = String(result.turnId);
|
|
518500
|
+
inFlight.accepted = true;
|
|
518501
|
+
inFlight.duplicate = result.duplicate === true;
|
|
518502
|
+
if (traceDelivery) for (const queued of inFlight.items) this.traceTelegramDelivery?.({ stage: "accepted", ...queued.channelDeliveryTrace, route: "active_steer", turnId: inFlight.turnId });
|
|
518503
|
+
this.commitQueuedSteer(inFlight);
|
|
518504
|
+
return true;
|
|
518505
|
+
}
|
|
518472
518506
|
commitQueuedSteerIfReady(inFlight) {
|
|
518473
518507
|
if (this.queueSteerInFlight !== inFlight || !inFlight.accepted || !inFlight.stepStarted || inFlight.turnEnded) return;
|
|
518474
518508
|
this.commitQueuedSteer(inFlight);
|
|
@@ -518654,12 +518688,7 @@ var BlunTUI = class {
|
|
|
518654
518688
|
session.steerActive(items.map((item) => item.text.trim()).join("\n\n"), expectedTurnId === void 0 ? {} : { expectedTurnId }).then((result) => {
|
|
518655
518689
|
if (this.queueSteerInFlight !== inFlight) return;
|
|
518656
518690
|
if (!result.accepted) this.recoverRejectedActiveSteer(inFlight);
|
|
518657
|
-
else
|
|
518658
|
-
inFlight.turnId = String(result.turnId);
|
|
518659
|
-
inFlight.accepted = true;
|
|
518660
|
-
if (inFlight.turnEnded) this.restoreQueuedSteer(inFlight);
|
|
518661
|
-
else this.commitQueuedSteerIfReady(inFlight);
|
|
518662
|
-
}
|
|
518691
|
+
else this.acceptQueuedSteer(inFlight, result);
|
|
518663
518692
|
}).catch((error) => {
|
|
518664
518693
|
this.restoreQueuedSteer(inFlight, error);
|
|
518665
518694
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "blun-king-cli",
|
|
3
|
-
"version": "9.1.
|
|
3
|
+
"version": "9.1.516",
|
|
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",
|
|
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-telegram-bridge-watchdog.js && node scripts/check-resume-replay-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,7 +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-mcp-startup-wait-budget.js",
|
|
41
|
+
"scripts/check-plugin-startup-regression.js",
|
|
42
|
+
"scripts/check-queue-controls-regression.js",
|
|
43
|
+
"scripts/check-telegram-bridge-watchdog.js",
|
|
44
|
+
"scripts/check-resume-replay-regression.js",
|
|
39
45
|
"scripts/check-release-metadata.js",
|
|
46
|
+
"scripts/check-todo-loop-regression.js",
|
|
40
47
|
"scripts/fix-node-pty-perms.js",
|
|
41
48
|
"standard-skills/",
|
|
42
49
|
"standard-tools/",
|
|
@@ -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,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
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
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 { installManagedAgentSpinePlugin } = require('../bin/plugin-bootstrap');
|
|
9
|
+
|
|
10
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'blun-plugin-startup-'));
|
|
11
|
+
const packageRoot = path.join(root, 'package');
|
|
12
|
+
const sourceRoot = path.join(packageRoot, 'agent-spine-plugin');
|
|
13
|
+
const profileA = path.join(root, 'profiles', 'a');
|
|
14
|
+
const profileB = path.join(root, 'profiles', 'b');
|
|
15
|
+
|
|
16
|
+
fs.mkdirSync(path.join(sourceRoot, 'src'), { recursive: true });
|
|
17
|
+
fs.writeFileSync(path.join(sourceRoot, 'package.json'), '{"name":"agent-spine"}\n');
|
|
18
|
+
fs.writeFileSync(path.join(sourceRoot, 'src', 'mcp.js'), 'export const ready = true;\n');
|
|
19
|
+
|
|
20
|
+
const originalReadFileSync = fs.readFileSync;
|
|
21
|
+
let sourceReads = 0;
|
|
22
|
+
fs.readFileSync = function countedReadFileSync(filePath, ...args) {
|
|
23
|
+
const absolutePath = path.resolve(String(filePath));
|
|
24
|
+
if (absolutePath === sourceRoot || absolutePath.startsWith(`${sourceRoot}${path.sep}`)) {
|
|
25
|
+
sourceReads += 1;
|
|
26
|
+
}
|
|
27
|
+
return originalReadFileSync.call(this, filePath, ...args);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
const first = installManagedAgentSpinePlugin({ packageRoot, blunDir: profileA });
|
|
32
|
+
installManagedAgentSpinePlugin({ packageRoot, blunDir: profileA });
|
|
33
|
+
installManagedAgentSpinePlugin({ packageRoot, blunDir: profileB });
|
|
34
|
+
installManagedAgentSpinePlugin({ packageRoot, blunDir: profileB });
|
|
35
|
+
|
|
36
|
+
assert.equal(
|
|
37
|
+
sourceReads,
|
|
38
|
+
2,
|
|
39
|
+
'one launcher process must read each managed source file only once',
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
fs.writeFileSync(path.join(first.pluginRoot, 'src', 'mcp.js'), 'tampered\n');
|
|
43
|
+
assert.throws(
|
|
44
|
+
() => installManagedAgentSpinePlugin({ packageRoot, blunDir: profileA }),
|
|
45
|
+
/digest mismatch/,
|
|
46
|
+
'source caching must not weaken installed-tree verification',
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
process.stdout.write('Plugin startup regression gate: PASS\n');
|
|
50
|
+
} finally {
|
|
51
|
+
fs.readFileSync = originalReadFileSync;
|
|
52
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
53
|
+
}
|