@link-assistant/hive-mind 2.1.6 → 2.1.7

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 CHANGED
@@ -1,5 +1,12 @@
1
1
  # @link-assistant/hive-mind
2
2
 
3
+ ## 2.1.7
4
+
5
+ ### Patch Changes
6
+
7
+ - 0ce779b: Enforce the solve queue minimum start interval for immediate Telegram `/solve`
8
+ launches so direct starts consume the same global pacing slot as queued starts.
9
+
3
10
  ## 2.1.6
4
11
 
5
12
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "2.1.6",
3
+ "version": "2.1.7",
4
4
  "description": "AI-powered issue solver and hive mind for collaborative problem solving",
5
5
  "main": "src/hive.mjs",
6
6
  "type": "module",
@@ -0,0 +1,41 @@
1
+ const reservationLocks = new WeakMap();
2
+
3
+ function getReservationLock(queue) {
4
+ return reservationLocks.get(queue) || Promise.resolve();
5
+ }
6
+
7
+ /**
8
+ * Atomically check and reserve the global startup slot for direct execution.
9
+ *
10
+ * Queue consumer starts are naturally serialized. Telegram direct starts need
11
+ * the same serialization because overlapping handlers can both pass resource
12
+ * checks before the first detached process is visible to process scanning.
13
+ *
14
+ * @param {Object} queue - SolveQueue-like object.
15
+ * @param {Object} options - Same options as queue.canStartCommand().
16
+ * @returns {Promise<Object>} canStartCommand() result plus reservation fields.
17
+ */
18
+ export async function reserveStartSlotForQueue(queue, options = {}) {
19
+ const previousReservation = getReservationLock(queue);
20
+ let releaseReservation;
21
+ reservationLocks.set(
22
+ queue,
23
+ new Promise(resolve => {
24
+ releaseReservation = resolve;
25
+ })
26
+ );
27
+
28
+ await previousReservation.catch(() => {});
29
+
30
+ try {
31
+ const check = await queue.canStartCommand(options);
32
+ if (!check.canStart) {
33
+ return { ...check, startReserved: false };
34
+ }
35
+
36
+ const reservedStartTime = queue.recordStart(options.tool || 'claude');
37
+ return { ...check, startReserved: true, reservedStartTime };
38
+ } finally {
39
+ releaseReservation();
40
+ }
41
+ }
@@ -916,8 +916,9 @@ async function handleSolveCommand(ctx) {
916
916
  await safeReply(ctx, t('telegram.url_session_running', { url: escapeMarkdown(normalizedUrl), session: activeSession.sessionName }, { locale: solveLocale }), { reply_to_message_id: ctx.message.message_id });
917
917
  return;
918
918
  }
919
- const check = await solveQueue.canStartCommand({ tool: solveTool, locale: solveLocale }); // Skip Claude limits for agent (#1159)
920
919
  const queueStats = solveQueue.getStats();
920
+ const hasPendingQueueItems = queueStats.queued > 0;
921
+ const check = hasPendingQueueItems ? await solveQueue.canStartCommand({ tool: solveTool, locale: solveLocale }) : await solveQueue.reserveStartSlot({ tool: solveTool, locale: solveLocale }); // Skip Claude limits for agent (#1159)
921
922
  // Handle rejection: threshold strategy is 'reject' — fail immediately (issue #1267)
922
923
  if (check.rejected) {
923
924
  await safeReply(ctx, t('telegram.solve_rejected', { infoBlock, reason: escapeMarkdown(check.rejectReason || 'Unknown') }, { locale: solveLocale }), { reply_to_message_id: ctx.message.message_id });
@@ -935,18 +936,18 @@ async function handleSolveCommand(ctx) {
935
936
  let solveLimitsAtStart = null;
936
937
  if (solveShowLimits) ({ infoBlock, limitsAtStart: solveLimitsAtStart } = await captureStartSnapshotAndAppend({ infoBlock, tool: solveTool, verbose: VERBOSE, limitsLib, commandLabel: '/solve', locale: solveLocale }));
937
938
 
938
- if (check.canStart && toolQueuedCount === 0) {
939
+ if (check.canStart && check.startReserved) {
939
940
  const startingMessage = await safeReply(ctx, formatStartingWorkSessionMessage({ infoBlock, locale: solveLocale }), { reply_to_message_id: ctx.message.message_id });
940
941
  await executeAndUpdateMessage(ctx, startingMessage, 'solve', argsWithLocale, infoBlock, effectiveSolveIsolation, solveTool, solveUrlContext, { showLimits: solveShowLimits, limitsAtStart: solveLimitsAtStart, locale: solveLocale });
941
942
  } else {
942
- const queueItem = solveQueue.enqueue({ url: normalizedUrl, args: argsWithLocale, ctx, requester, infoBlock, tool: solveTool, perCommandIsolation: effectiveSolveIsolation, urlContext: solveUrlContext, showLimits: solveShowLimits, limitsAtStart: solveLimitsAtStart, locale: solveLocale });
943
- const queueMessage = buildSolveQueuedMessage({ locale: solveLocale, tool: solveTool, position: toolQueuedCount + 1, infoBlock, reason: check.reason ? escapeMarkdown(check.reason) : '' }); // tool-specific position (#1551)
944
- const queuedMessage = await safeReply(ctx, queueMessage, { reply_to_message_id: ctx.message.message_id });
945
- queueItem.messageInfo = { chatId: queuedMessage.chat.id, messageId: queuedMessage.message_id };
946
943
  if (!solveQueue.executeCallback) {
947
944
  const _t = (s, i) => trackSession(s, i, VERBOSE);
948
945
  solveQueue.executeCallback = createIsolationAwareQueueCallback(ISOLATION_BACKEND, isolationRunner, _t, createQueueExecuteCallback(executeStartScreen, _t), VERBOSE);
949
946
  }
947
+ const queueItem = solveQueue.enqueue({ url: normalizedUrl, args: argsWithLocale, ctx, requester, infoBlock, tool: solveTool, perCommandIsolation: effectiveSolveIsolation, urlContext: solveUrlContext, showLimits: solveShowLimits, limitsAtStart: solveLimitsAtStart, locale: solveLocale });
948
+ const queueMessage = buildSolveQueuedMessage({ locale: solveLocale, tool: solveTool, position: toolQueuedCount + 1, infoBlock, reason: check.reason ? escapeMarkdown(check.reason) : '' }); // tool-specific position (#1551)
949
+ const queuedMessage = await safeReply(ctx, queueMessage, { reply_to_message_id: ctx.message.message_id });
950
+ queueItem.messageInfo = { chatId: queuedMessage.chat.id, messageId: queuedMessage.message_id };
950
951
  }
951
952
  }
952
953
 
@@ -20,6 +20,7 @@ export { formatDuration, getRunningAgentProcesses, getRunningClaudeProcesses, ge
20
20
  import { collectExecutingItems, formatDuration, formatQueueToolSection, formatWaitingReason, getRunningAgentProcesses, getRunningClaudeProcesses, getRunningCodexProcesses, getRunningGeminiProcesses, getRunningProcesses, getRunningQwenProcesses, getRunningSessionItems, groupQueueItemsByTool } from './telegram-solve-queue.helpers.lib.mjs';
21
21
  export { QUEUE_CONFIG, THRESHOLD_STRATEGIES } from './queue-config.lib.mjs';
22
22
  import { QUEUE_CONFIG } from './queue-config.lib.mjs';
23
+ import { reserveStartSlotForQueue } from './queue-start-reservation.lib.mjs';
23
24
  import { formatExecutingWorkSessionMessage, formatStartingWorkSessionMessage } from './work-session-formatting.lib.mjs';
24
25
  import { t } from './i18n.lib.mjs';
25
26
  import { lt } from './limits-i18n.lib.mjs';
@@ -373,6 +374,16 @@ export class SolveQueue {
373
374
  return count;
374
375
  }
375
376
 
377
+ recordStart(tool = 'claude', startTime = Date.now()) {
378
+ this.lastStartTimeByTool[tool] = startTime;
379
+ this.lastStartTime = startTime;
380
+ return startTime;
381
+ }
382
+
383
+ reserveStartSlot(options = {}) {
384
+ return reserveStartSlotForQueue(this, options);
385
+ }
386
+
376
387
  /**
377
388
  * Find the next startable item across all tool queues.
378
389
  * With separate queues, each tool is checked independently so tool-specific
@@ -1074,7 +1085,7 @@ export class SolveQueue {
1074
1085
  * - Each tool queue is checked independently
1075
1086
  * - Claude limits only affect Claude queue
1076
1087
  * - Agent queue can proceed even when Claude is blocked (and vice versa)
1077
- * - Multiple items can start in the same cycle (one per tool)
1088
+ * - The oldest startable item starts each cycle to preserve global pacing
1078
1089
  *
1079
1090
  * @see https://github.com/link-assistant/hive-mind/issues/1159
1080
1091
  */
@@ -1088,9 +1099,6 @@ export class SolveQueue {
1088
1099
  continue;
1089
1100
  }
1090
1101
 
1091
- // Find startable items from each tool queue
1092
- // Each tool is checked independently so they don't block each other
1093
- // See: https://github.com/link-assistant/hive-mind/issues/1159
1094
1102
  const startableItems = await this.findStartableItems();
1095
1103
 
1096
1104
  if (startableItems.length === 0) {
@@ -1101,8 +1109,6 @@ export class SolveQueue {
1101
1109
  continue;
1102
1110
  }
1103
1111
 
1104
- // Start items from each tool that can proceed
1105
- // This allows parallel starts from different tool queues
1106
1112
  for (const startable of startableItems) {
1107
1113
  const { tool } = startable;
1108
1114
  const toolQueue = this.getToolQueue(tool);
@@ -1115,12 +1121,9 @@ export class SolveQueue {
1115
1121
  item.setStarting();
1116
1122
  this.processing.set(item.id, item);
1117
1123
 
1118
- // Update tool-specific last start time
1119
- this.lastStartTimeByTool[tool] = Date.now();
1120
- this.lastStartTime = Date.now(); // Legacy compatibility
1124
+ this.recordStart(tool);
1121
1125
  this.stats.totalStarted++;
1122
1126
 
1123
- // Update message to show Starting status
1124
1127
  await this.updateItemMessage(item, formatStartingWorkSessionMessage({ infoBlock: item.infoBlock, locale: item.locale }));
1125
1128
 
1126
1129
  this.log(`Starting: ${item.toString()} from ${tool} queue`);