@link-assistant/hive-mind 2.1.6 → 2.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/package.json +1 -1
- package/src/argument-normalization.lib.mjs +23 -0
- package/src/cli-arguments.lib.mjs +3 -1
- package/src/hive.mjs +5 -5
- package/src/queue-start-reservation.lib.mjs +41 -0
- package/src/solve.config.lib.mjs +3 -3
- package/src/telegram-bot.mjs +7 -6
- package/src/telegram-solve-command.lib.mjs +4 -6
- package/src/telegram-solve-queue.lib.mjs +13 -10
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# @link-assistant/hive-mind
|
|
2
2
|
|
|
3
|
+
## 2.1.8
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 11dab3c: Handle Telegram and CLI commands where a GitHub issue or pull request URL is immediately followed by a long option marker.
|
|
8
|
+
|
|
9
|
+
## 2.1.7
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 0ce779b: Enforce the solve queue minimum start interval for immediate Telegram `/solve`
|
|
14
|
+
launches so direct starts consume the same global pacing slot as queued starts.
|
|
15
|
+
|
|
3
16
|
## 2.1.6
|
|
4
17
|
|
|
5
18
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const TYPOGRAPHIC_LONG_OPTION_DASHES = /[\u2013\u2014]/g;
|
|
2
|
+
|
|
3
|
+
const GITHUB_ISSUE_OR_PR_WITH_JOINED_OPTION = new RegExp(['^(', '(?:https?://)?', '(?:www\\.)?', '(?:github\\.com/)?', '[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+/(?:issues|pull)/\\d+', ')', '(--[A-Za-z][A-Za-z0-9-]*(?:=.*)?)', '$'].join(''));
|
|
4
|
+
|
|
5
|
+
export const normalizeTypographicOptionDashes = value => {
|
|
6
|
+
if (typeof value !== 'string') return value;
|
|
7
|
+
return value.replace(TYPOGRAPHIC_LONG_OPTION_DASHES, '--');
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const splitJoinedGitHubLongOptionArg = value => {
|
|
11
|
+
const normalized = normalizeTypographicOptionDashes(value);
|
|
12
|
+
if (typeof normalized !== 'string') return [normalized];
|
|
13
|
+
|
|
14
|
+
const match = normalized.match(GITHUB_ISSUE_OR_PR_WITH_JOINED_OPTION);
|
|
15
|
+
if (!match) return [normalized];
|
|
16
|
+
|
|
17
|
+
return [match[1], match[2]];
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const normalizeCliArgs = args => {
|
|
21
|
+
if (!Array.isArray(args)) return [];
|
|
22
|
+
return args.flatMap(splitJoinedGitHubLongOptionArg);
|
|
23
|
+
};
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { getenv, makeConfig, yargs as linoYargs } from 'lino-arguments';
|
|
2
2
|
|
|
3
|
+
import { normalizeCliArgs } from './argument-normalization.lib.mjs';
|
|
3
4
|
import { enhanceUnknownArgumentError } from './option-suggestions.lib.mjs';
|
|
4
5
|
|
|
5
6
|
export { getenv };
|
|
7
|
+
export { normalizeCliArgs, normalizeTypographicOptionDashes, splitJoinedGitHubLongOptionArg } from './argument-normalization.lib.mjs';
|
|
6
8
|
|
|
7
9
|
export const hideBin = argv => argv.slice(2);
|
|
8
10
|
|
|
@@ -53,7 +55,7 @@ export function addCliCompatibilityAliases(parsed, { positionalAliases = [] } =
|
|
|
53
55
|
}
|
|
54
56
|
|
|
55
57
|
export function parseCliArgumentsWithLino({ argv = process.argv, commandName = 'cli', createYargsConfig, positionalAliases = [], lenv = { enabled: true }, env = { enabled: false }, getenv: getenvOptions = { enabled: true } } = {}) {
|
|
56
|
-
const fullArgv = ensureFullArgv(argv, commandName);
|
|
58
|
+
const fullArgv = normalizeCliArgs(ensureFullArgv(argv, commandName));
|
|
57
59
|
let configuredParser = null;
|
|
58
60
|
let parsed;
|
|
59
61
|
|
package/src/hive.mjs
CHANGED
|
@@ -18,9 +18,9 @@ if (earlyArgs.includes('--version')) {
|
|
|
18
18
|
if (earlyArgs.includes('--help') || earlyArgs.includes('-h')) {
|
|
19
19
|
try {
|
|
20
20
|
// Load minimal modules needed for help
|
|
21
|
-
const { getLinoYargsFactory, hideBin } = await import('./cli-arguments.lib.mjs');
|
|
21
|
+
const { getLinoYargsFactory, hideBin, normalizeCliArgs } = await import('./cli-arguments.lib.mjs');
|
|
22
22
|
const yargs = getLinoYargsFactory();
|
|
23
|
-
const rawArgs = hideBin(process.argv);
|
|
23
|
+
const rawArgs = normalizeCliArgs(hideBin(process.argv));
|
|
24
24
|
// Reuse createYargsConfig from shared module to avoid duplication
|
|
25
25
|
const { createYargsConfig } = await import('./hive.config.lib.mjs');
|
|
26
26
|
const helpYargs = createYargsConfig(yargs(rawArgs)).version(false);
|
|
@@ -62,7 +62,7 @@ if (isRunningDirectly) {
|
|
|
62
62
|
30000, // 30 second timeout
|
|
63
63
|
'loading command-stream'
|
|
64
64
|
);
|
|
65
|
-
const { parseCliArgumentsWithLino, hideBin } = await import('./cli-arguments.lib.mjs');
|
|
65
|
+
const { parseCliArgumentsWithLino, hideBin, normalizeCliArgs } = await import('./cli-arguments.lib.mjs');
|
|
66
66
|
const path = (await withTimeout(use('path'), 30000, 'loading path')).default;
|
|
67
67
|
const fs = (await withTimeout(use('fs'), 30000, 'loading fs')).promises;
|
|
68
68
|
// Import shared library functions
|
|
@@ -225,7 +225,7 @@ if (isRunningDirectly) {
|
|
|
225
225
|
}
|
|
226
226
|
|
|
227
227
|
// Configure command line arguments - GitHub URL as positional argument
|
|
228
|
-
const rawArgs = hideBin(process.argv);
|
|
228
|
+
const rawArgs = normalizeCliArgs(hideBin(process.argv));
|
|
229
229
|
// Use .parse() instead of .argv to ensure .strict() mode works correctly
|
|
230
230
|
// When you use .argv, strict mode doesn't trigger properly
|
|
231
231
|
// See: https://github.com/yargs/yargs/issues - .strict() only works with .parse()
|
|
@@ -248,7 +248,7 @@ if (isRunningDirectly) {
|
|
|
248
248
|
|
|
249
249
|
try {
|
|
250
250
|
argv = parseCliArgumentsWithLino({
|
|
251
|
-
argv:
|
|
251
|
+
argv: ['node', 'hive', ...rawArgs],
|
|
252
252
|
commandName: 'hive',
|
|
253
253
|
createYargsConfig,
|
|
254
254
|
positionalAliases: ['github-url'],
|
|
@@ -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
|
+
}
|
package/src/solve.config.lib.mjs
CHANGED
|
@@ -11,7 +11,7 @@ import { enhanceErrorMessage, detectMalformedFlags } from './option-suggestions.
|
|
|
11
11
|
import { defaultModels, buildModelOptionDescription, resolveDefaultFallbackModel, resolveRuntimeDefaultModel } from './models/index.mjs';
|
|
12
12
|
import { validateBranchName } from './solve.branch.lib.mjs';
|
|
13
13
|
import { resolveEscalationConfig, isEscalateEnabled, DEFAULT_ESCALATE_RANGE } from './solve.escalate.lib.mjs';
|
|
14
|
-
import { getLinoYargsFactory, hideBin, parseCliArgumentsWithLino } from './cli-arguments.lib.mjs';
|
|
14
|
+
import { getLinoYargsFactory, hideBin, normalizeCliArgs, parseCliArgumentsWithLino } from './cli-arguments.lib.mjs';
|
|
15
15
|
|
|
16
16
|
// Re-export for use by telegram-bot.mjs (avoids extra import lines there)
|
|
17
17
|
export { detectMalformedFlags };
|
|
@@ -737,7 +737,7 @@ export const createYargsConfig = yargsInstance => {
|
|
|
737
737
|
|
|
738
738
|
// Parse command line arguments - now needs yargs and hideBin passed in
|
|
739
739
|
export const parseArguments = async (yargs = getLinoYargsFactory(), hideBinFn = hideBin) => {
|
|
740
|
-
const rawArgs = hideBinFn(process.argv);
|
|
740
|
+
const rawArgs = normalizeCliArgs(hideBinFn(process.argv));
|
|
741
741
|
|
|
742
742
|
// Issue #1092: Detect malformed flag patterns BEFORE yargs parsing
|
|
743
743
|
// This catches cases like "-- model" which yargs silently treats as positional arguments
|
|
@@ -776,7 +776,7 @@ export const parseArguments = async (yargs = getLinoYargsFactory(), hideBinFn =
|
|
|
776
776
|
try {
|
|
777
777
|
yargsInstance = createYargsConfig(yargs());
|
|
778
778
|
argv = parseCliArgumentsWithLino({
|
|
779
|
-
argv:
|
|
779
|
+
argv: ['node', 'solve', ...rawArgs],
|
|
780
780
|
commandName: 'solve',
|
|
781
781
|
createYargsConfig,
|
|
782
782
|
positionalAliases: ['issue-url'],
|
package/src/telegram-bot.mjs
CHANGED
|
@@ -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 &&
|
|
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
|
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* @see https://github.com/link-assistant/hive-mind/issues/1618
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
import { normalizeCliArgs } from './argument-normalization.lib.mjs';
|
|
11
12
|
import { enhanceUnknownArgumentError } from './option-suggestions.lib.mjs';
|
|
12
13
|
|
|
13
14
|
export const TOOL_SOLVE_COMMAND_ALIASES = Object.freeze({
|
|
@@ -29,16 +30,13 @@ export function parseCommandArgs(text) {
|
|
|
29
30
|
return [];
|
|
30
31
|
}
|
|
31
32
|
|
|
32
|
-
// Replace em-dash with double-dash to fix Telegram auto-replacement.
|
|
33
|
-
const normalizedArgsText = argsText.replace(/—/g, '--');
|
|
34
|
-
|
|
35
33
|
const args = [];
|
|
36
34
|
let currentArg = '';
|
|
37
35
|
let inQuotes = false;
|
|
38
36
|
let quoteChar = null;
|
|
39
37
|
|
|
40
|
-
for (let i = 0; i <
|
|
41
|
-
const char =
|
|
38
|
+
for (let i = 0; i < argsText.length; i++) {
|
|
39
|
+
const char = argsText[i];
|
|
42
40
|
|
|
43
41
|
if ((char === '"' || char === "'") && !inQuotes) {
|
|
44
42
|
inQuotes = true;
|
|
@@ -60,7 +58,7 @@ export function parseCommandArgs(text) {
|
|
|
60
58
|
args.push(currentArg);
|
|
61
59
|
}
|
|
62
60
|
|
|
63
|
-
return args;
|
|
61
|
+
return normalizeCliArgs(args);
|
|
64
62
|
}
|
|
65
63
|
|
|
66
64
|
function toCamelCaseOptionName(name) {
|
|
@@ -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
|
-
* -
|
|
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
|
-
|
|
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`);
|