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.
@@ -282,36 +282,13 @@ function installAgentSpineForProfile(packageRoot, blunDir) {
282
282
  installManagedAgentSpinePlugin({ packageRoot, blunDir });
283
283
  }
284
284
 
285
- function installAgentSpineForExistingProfiles(packageRoot, sharedHome, currentProfileHome) {
286
- const profileRoot = path.join(sharedHome, 'profile');
287
- const profileHomes = new Map();
288
- const addProfileHome = (profileHome) => {
289
- const resolved = path.resolve(profileHome);
290
- const key = process.platform === 'win32' ? resolved.toLowerCase() : resolved;
291
- profileHomes.set(key, resolved);
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) {
@@ -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 = snapshotPluginTree(sourceRoot);
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 = snapshotPluginTree(root);
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: (task) => !agent.turn.hasActiveTurn && (task?.owner !== "session-loop" || !hasUnfinishedTodoWork(agent)),
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 stepEventUuid = resolveStepEventUuid(this.openSteps, event);
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 stepEventUuid = resolveStepEventUuid(this.openSteps, event);
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?.waitForInitialLoad(signal);
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 CHANNEL_STEER_CONFIRM_TIMEOUT_MS = 15e3;
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.turnId = String(result.turnId);
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.514",
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
+ }