@link-assistant/hive-mind 2.11.12 → 2.12.0
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 +51 -0
- package/package.json +1 -1
- package/src/agent-command.lib.mjs +74 -0
- package/src/agent.lib.mjs +59 -34
- package/src/agentic-cli-updater.lib.mjs +241 -0
- package/src/automation-stop-reporting.lib.mjs +272 -0
- package/src/formal-ai-isolation.lib.mjs +62 -0
- package/src/formal-ai-maintenance.lib.mjs +106 -0
- package/src/formal-ai-model.lib.mjs +25 -0
- package/src/formal-ai-runtime.lib.mjs +10 -0
- package/src/formal-ai-sidecar.lib.mjs +565 -0
- package/src/formal-ai-updater.lib.mjs +294 -0
- package/src/formal-ai-version.lib.mjs +100 -0
- package/src/formal-ai.lib.mjs +11 -16
- package/src/github-terminal-state.lib.mjs +50 -13
- package/src/isolation-runner.lib.mjs +32 -1
- package/src/model-args.lib.mjs +32 -0
- package/src/models/index.mjs +5 -4
- package/src/solve.auto-merge-attempt.lib.mjs +245 -0
- package/src/solve.auto-merge.lib.mjs +54 -111
- package/src/solve.results.lib.mjs +2 -2
- package/src/solve.watch.lib.mjs +35 -1
- package/src/state-lock.lib.mjs +82 -0
- package/src/telegram-bot.mjs +18 -0
- package/src/telegram-fix-command.lib.mjs +1 -8
- package/src/telegram-task-command.lib.mjs +1 -8
- package/src/tool-comments.lib.mjs +12 -1
- package/src/working-session-summary.lib.mjs +47 -1
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-process exclusive locks over the durable bot state directory.
|
|
3
|
+
*
|
|
4
|
+
* Issue #2146 introduced two background maintainers — the Formal AI sidecar
|
|
5
|
+
* lifecycle and the agentic-CLI refresh — that must never interleave with the
|
|
6
|
+
* work they maintain. Both need the same primitive, so it lives here once.
|
|
7
|
+
*
|
|
8
|
+
* `mkdir` is the lock: it is atomic on every filesystem the bot runs on, unlike
|
|
9
|
+
* "check then create" with a regular file. A lock older than `staleMs` is
|
|
10
|
+
* broken so a killed process cannot deadlock the bot forever.
|
|
11
|
+
*
|
|
12
|
+
* @see https://github.com/link-assistant/hive-mind/issues/2146
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import fs from 'node:fs';
|
|
16
|
+
import path from 'node:path';
|
|
17
|
+
|
|
18
|
+
import { resolveBotStateDir } from './session-store.lib.mjs';
|
|
19
|
+
|
|
20
|
+
export const DEFAULT_STATE_LOCK_TIMEOUT_MS = 10 * 60 * 1000;
|
|
21
|
+
export const DEFAULT_STATE_LOCK_STALE_MS = 15 * 60 * 1000;
|
|
22
|
+
export const DEFAULT_STATE_LOCK_POLL_MS = 250;
|
|
23
|
+
|
|
24
|
+
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
|
|
25
|
+
|
|
26
|
+
/** Absolute path of the lock directory backing `name`. */
|
|
27
|
+
export const resolveStateLockPath = (name, env = process.env) => path.join(resolveBotStateDir(env), `${name}.lock`);
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Run `fn` while holding the named state lock.
|
|
31
|
+
*
|
|
32
|
+
* @param {string} name - Lock name, e.g. `formal-ai-sidecar`.
|
|
33
|
+
* @param {() => Promise<any>} fn - Critical section.
|
|
34
|
+
* @returns {Promise<any>} Whatever `fn` resolves to.
|
|
35
|
+
*/
|
|
36
|
+
export const withStateLock = async (name, fn, { env = process.env, fsImpl = fs, now = () => Date.now(), timeoutMs = DEFAULT_STATE_LOCK_TIMEOUT_MS, staleMs = DEFAULT_STATE_LOCK_STALE_MS, pollMs = DEFAULT_STATE_LOCK_POLL_MS, sleepImpl = sleep, log = null } = {}) => {
|
|
37
|
+
const lockPath = resolveStateLockPath(name, env);
|
|
38
|
+
fsImpl.mkdirSync(path.dirname(lockPath), { recursive: true });
|
|
39
|
+
|
|
40
|
+
const deadline = now() + timeoutMs;
|
|
41
|
+
for (;;) {
|
|
42
|
+
try {
|
|
43
|
+
fsImpl.mkdirSync(lockPath);
|
|
44
|
+
break;
|
|
45
|
+
} catch (error) {
|
|
46
|
+
if (error?.code !== 'EEXIST') throw error;
|
|
47
|
+
|
|
48
|
+
let age;
|
|
49
|
+
try {
|
|
50
|
+
age = now() - fsImpl.statSync(lockPath).mtimeMs;
|
|
51
|
+
} catch {
|
|
52
|
+
// The holder released it between mkdir and stat; retry immediately.
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (age > staleMs) {
|
|
57
|
+
if (log) await log(`⚠️ ${name} lock held for ${Math.round(age / 1000)}s with no owner; breaking it`);
|
|
58
|
+
try {
|
|
59
|
+
fsImpl.rmSync(lockPath, { recursive: true, force: true });
|
|
60
|
+
} catch {
|
|
61
|
+
// Another waiter won the race; retry normally.
|
|
62
|
+
}
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (now() >= deadline) throw new Error(`Timed out after ${Math.round(timeoutMs / 1000)}s waiting for the ${name} lock at ${lockPath}`, { cause: error });
|
|
67
|
+
await sleepImpl(pollMs);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
return await fn();
|
|
73
|
+
} finally {
|
|
74
|
+
try {
|
|
75
|
+
fsImpl.rmSync(lockPath, { recursive: true, force: true });
|
|
76
|
+
} catch {
|
|
77
|
+
// Losing the release is recoverable through the stale-lock path above.
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export default { DEFAULT_STATE_LOCK_POLL_MS, DEFAULT_STATE_LOCK_STALE_MS, DEFAULT_STATE_LOCK_TIMEOUT_MS, resolveStateLockPath, withStateLock };
|
package/src/telegram-bot.mjs
CHANGED
|
@@ -263,6 +263,7 @@ const { createSessionStore } = await import('./session-store.lib.mjs');
|
|
|
263
263
|
const { createHeartbeat, resumeSessionsOnLaunch, createShutdownHandler } = await import('./bot-lifecycle.lib.mjs');
|
|
264
264
|
const { formatExecutingWorkSessionMessage, formatStartingWorkSessionMessage } = await import('./work-session-formatting.lib.mjs');
|
|
265
265
|
const { buildTelegramHelpMessage, buildTelegramInfoBlock, buildSolveQueuedMessage } = await import('./telegram-ui-messages.lib.mjs');
|
|
266
|
+
const { startFormalAiMaintenance } = await import('./formal-ai-maintenance.lib.mjs');
|
|
266
267
|
|
|
267
268
|
// Initialize Sentry for error tracking
|
|
268
269
|
await initializeSentry({
|
|
@@ -1244,6 +1245,7 @@ if (VERBOSE) {
|
|
|
1244
1245
|
// Non-retryable errors (401 Unauthorized) cause immediate exit.
|
|
1245
1246
|
const launchAbortController = new AbortController();
|
|
1246
1247
|
let sessionMonitoringTimer = null;
|
|
1248
|
+
let formalAiMaintenance = null;
|
|
1247
1249
|
let launchAnnouncementShown = false;
|
|
1248
1250
|
|
|
1249
1251
|
function startSessionMonitoringOnce() {
|
|
@@ -1253,6 +1255,20 @@ function startSessionMonitoringOnce() {
|
|
|
1253
1255
|
sessionMonitoringTimer = startSessionMonitoring(bot, VERBOSE, 30000, { isolationRunner });
|
|
1254
1256
|
}
|
|
1255
1257
|
|
|
1258
|
+
// Issue #2146 (PR #2147 review): stop the Formal AI sidecar once no Formal AI
|
|
1259
|
+
// task holds a lease, then — while the host is idle — update its image (with a
|
|
1260
|
+
// non-destructive memory migration) and refresh the agentic CLIs. Every step is
|
|
1261
|
+
// best-effort and never blocks the bot.
|
|
1262
|
+
function startFormalAiMaintenanceOnce() {
|
|
1263
|
+
if (formalAiMaintenance) return;
|
|
1264
|
+
formalAiMaintenance = startFormalAiMaintenance({
|
|
1265
|
+
verbose: VERBOSE,
|
|
1266
|
+
log: async message => {
|
|
1267
|
+
console.log(message);
|
|
1268
|
+
},
|
|
1269
|
+
});
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1256
1272
|
// Issue #1927 (requirements #3/#4): a periodic timestamped heartbeat so the "last
|
|
1257
1273
|
// time the bot was alive" is always discoverable from the log. The heartbeat
|
|
1258
1274
|
// logic lives in bot-lifecycle.lib.mjs so it can be unit tested.
|
|
@@ -1274,6 +1290,7 @@ async function onBotLaunched() {
|
|
|
1274
1290
|
await resumeSessionsOnLaunch({ resumeTrackedSessions, botStartTime: BOT_START_TIME, verbose: VERBOSE, logger: botLogger });
|
|
1275
1291
|
|
|
1276
1292
|
startSessionMonitoringOnce();
|
|
1293
|
+
startFormalAiMaintenanceOnce();
|
|
1277
1294
|
heartbeat.start();
|
|
1278
1295
|
|
|
1279
1296
|
if (VERBOSE) {
|
|
@@ -1372,6 +1389,7 @@ const handleShutdownSignal = createShutdownHandler({
|
|
|
1372
1389
|
cleanup: () => {
|
|
1373
1390
|
launchAbortController.abort();
|
|
1374
1391
|
if (sessionMonitoringTimer) clearInterval(sessionMonitoringTimer);
|
|
1392
|
+
formalAiMaintenance?.stop();
|
|
1375
1393
|
heartbeat.stop();
|
|
1376
1394
|
stopSolveQueue();
|
|
1377
1395
|
},
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import { buildUserMention } from './buildUserMention.lib.mjs';
|
|
11
11
|
import { validateModelName } from './models/index.mjs';
|
|
12
12
|
import { parseFixRepository } from './fix.ci-cd.lib.mjs';
|
|
13
|
+
import { getModelFromArgs } from './model-args.lib.mjs';
|
|
13
14
|
import { escapeMarkdown } from './telegram-markdown.lib.mjs';
|
|
14
15
|
import { extractIsolationFromArgs, isValidPerCommandIsolation } from './telegram-isolation.lib.mjs';
|
|
15
16
|
import { mergeArgsWithOverrides } from './args-overrides.lib.mjs';
|
|
@@ -47,14 +48,6 @@ export function getFixToolFromArgs(args) {
|
|
|
47
48
|
return 'claude';
|
|
48
49
|
}
|
|
49
50
|
|
|
50
|
-
function getModelFromArgs(args) {
|
|
51
|
-
for (let i = 0; i < args.length; i++) {
|
|
52
|
-
if ((args[i] === '--model' || args[i] === '-m') && i + 1 < args.length) return args[i + 1];
|
|
53
|
-
if (args[i].startsWith('--model=')) return args[i].substring('--model='.length);
|
|
54
|
-
}
|
|
55
|
-
return null;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
51
|
function validateFixModel(args) {
|
|
59
52
|
const model = getModelFromArgs(args);
|
|
60
53
|
if (!model) return null;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { buildUserMention } from './buildUserMention.lib.mjs';
|
|
2
2
|
import { validateModelName } from './models/index.mjs';
|
|
3
3
|
import { getLinoYargsFactory } from './cli-arguments.lib.mjs';
|
|
4
|
+
import { getModelFromArgs } from './model-args.lib.mjs';
|
|
4
5
|
import { createYargsConfig as createTaskYargsConfig } from './task.config.lib.mjs';
|
|
5
6
|
import { createCiCdIssue } from './fix.ci-cd-issue.lib.mjs';
|
|
6
7
|
import { parseFixRepository } from './fix.ci-cd.lib.mjs';
|
|
@@ -47,14 +48,6 @@ export function getTaskToolFromArgs(args) {
|
|
|
47
48
|
return 'claude';
|
|
48
49
|
}
|
|
49
50
|
|
|
50
|
-
function getModelFromArgs(args) {
|
|
51
|
-
for (let i = 0; i < args.length; i++) {
|
|
52
|
-
if ((args[i] === '--model' || args[i] === '-m') && i + 1 < args.length) return args[i + 1];
|
|
53
|
-
if (args[i].startsWith('--model=')) return args[i].substring('--model='.length);
|
|
54
|
-
}
|
|
55
|
-
return null;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
51
|
function validateTaskModel(args) {
|
|
59
52
|
const model = getModelFromArgs(args);
|
|
60
53
|
if (!model) return null;
|
|
@@ -62,6 +62,17 @@ export const BILLING_LIMIT_MARKER = 'GitHub Actions Billing Limit';
|
|
|
62
62
|
// solve.auto-merge.lib.mjs — cancelled/stale CI needs manual review
|
|
63
63
|
export const CANCELLED_CI_REVIEW_MARKER = 'Cancelled CI/CD Requires Review';
|
|
64
64
|
|
|
65
|
+
// automation-stop-reporting.lib.mjs — Issue #2144: every automation stop is
|
|
66
|
+
// announced on GitHub with the exact reason. Before this, watch mode and
|
|
67
|
+
// auto-restart-until-mergeable exited silently on terminal states and tool
|
|
68
|
+
// failures, leaving the pull request with no explanation at all.
|
|
69
|
+
export const AUTOMATION_STOPPED_MARKER = 'Automation stopped';
|
|
70
|
+
|
|
71
|
+
// automation-stop-reporting.lib.mjs — Issue #2144: the pull request is
|
|
72
|
+
// mergeable but `--auto-merge` cannot complete because the linked issue is
|
|
73
|
+
// closed or unavailable. The user is asked to reopen it or merge manually.
|
|
74
|
+
export const AUTO_MERGE_BLOCKED_MARKER = 'Auto-merge blocked';
|
|
75
|
+
|
|
65
76
|
// solve.results.lib.mjs — working session summary comments posted by
|
|
66
77
|
// --attach-solution-summary / --auto-attach-solution-summary at the end of
|
|
67
78
|
// every working session (top-level solve, auto-restart-until-mergeable
|
|
@@ -111,7 +122,7 @@ export const USAGE_LIMIT_REACHED_MARKER = 'Usage Limit Reached';
|
|
|
111
122
|
* named constants above so that adding a new marker only requires adding
|
|
112
123
|
* the constant and appending it here.
|
|
113
124
|
*/
|
|
114
|
-
export const TOOL_GENERATED_COMMENT_MARKERS = [AI_WORK_SESSION_STARTED_MARKER, AI_WORK_SESSION_COMPLETED_MARKER, AI_WORK_SESSION_RESUMED_MARKER, AUTO_RESUME_ON_LIMIT_RESET_MARKER, AUTO_RESTART_ON_LIMIT_RESET_MARKER, SOLUTION_DRAFT_LOG_MARKER, AUTO_RESTART_MARKER, AUTO_RESTART_UNTIL_MERGEABLE_LOG_MARKER, READY_TO_MERGE_MARKER, READY_FOR_REVIEW_MARKER, AUTO_MERGED_MARKER, BILLING_LIMIT_MARKER, CANCELLED_CI_REVIEW_MARKER, MAINTAINER_ACCESS_REQUEST_MARKER, LIVE_PROGRESS_SECTION_START_MARKER, SESSION_FORCE_KILLED_MARKER, REPOSITORY_INITIALIZATION_REQUIRED_MARKER, INTERACTIVE_SESSION_STARTED_MARKER, INTERACTIVE_SESSION_ENDED_MARKER, NOW_WORKING_SESSION_IS_ENDED_MARKER, SOLUTION_DRAFT_FAILED_MARKER, SOLUTION_DRAFT_FINISHED_WITH_ERRORS_MARKER, USAGE_LIMIT_REACHED_MARKER, WORKING_SESSION_SUMMARY_AUTOMATION_MARKER];
|
|
125
|
+
export const TOOL_GENERATED_COMMENT_MARKERS = [AI_WORK_SESSION_STARTED_MARKER, AI_WORK_SESSION_COMPLETED_MARKER, AI_WORK_SESSION_RESUMED_MARKER, AUTO_RESUME_ON_LIMIT_RESET_MARKER, AUTO_RESTART_ON_LIMIT_RESET_MARKER, SOLUTION_DRAFT_LOG_MARKER, AUTO_RESTART_MARKER, AUTO_RESTART_UNTIL_MERGEABLE_LOG_MARKER, READY_TO_MERGE_MARKER, READY_FOR_REVIEW_MARKER, AUTO_MERGED_MARKER, BILLING_LIMIT_MARKER, CANCELLED_CI_REVIEW_MARKER, AUTOMATION_STOPPED_MARKER, AUTO_MERGE_BLOCKED_MARKER, MAINTAINER_ACCESS_REQUEST_MARKER, LIVE_PROGRESS_SECTION_START_MARKER, SESSION_FORCE_KILLED_MARKER, REPOSITORY_INITIALIZATION_REQUIRED_MARKER, INTERACTIVE_SESSION_STARTED_MARKER, INTERACTIVE_SESSION_ENDED_MARKER, NOW_WORKING_SESSION_IS_ENDED_MARKER, SOLUTION_DRAFT_FAILED_MARKER, SOLUTION_DRAFT_FINISHED_WITH_ERRORS_MARKER, USAGE_LIMIT_REACHED_MARKER, WORKING_SESSION_SUMMARY_AUTOMATION_MARKER];
|
|
115
126
|
|
|
116
127
|
/**
|
|
117
128
|
* Markers that indicate the end of a working session. Used by
|
|
@@ -49,6 +49,52 @@ export const redactWorkspacePaths = text => {
|
|
|
49
49
|
return text.replace(WORKSPACE_PATH_PATTERN, WORKSPACE_PATH_PLACEHOLDER);
|
|
50
50
|
};
|
|
51
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Fence structured text emitted after a Markdown lead-in ending in `:`.
|
|
54
|
+
* Formal AI plan events use a root record followed by space/tab-indented Lino;
|
|
55
|
+
* GitHub otherwise collapses that layout into ordinary prose. Existing fences
|
|
56
|
+
* and ordinary Markdown are preserved, making this safe to apply at the final
|
|
57
|
+
* GitHub-comment boundary.
|
|
58
|
+
*/
|
|
59
|
+
export const formatWorkingSessionSummaryMarkdown = text => {
|
|
60
|
+
if (typeof text !== 'string' || !text) return text;
|
|
61
|
+
|
|
62
|
+
const lines = text.split('\n');
|
|
63
|
+
const output = [];
|
|
64
|
+
let inFence = false;
|
|
65
|
+
let previousNonEmpty = '';
|
|
66
|
+
|
|
67
|
+
for (let index = 0; index < lines.length; ) {
|
|
68
|
+
const line = lines[index];
|
|
69
|
+
if (/^\s*(```|~~~)/.test(line)) {
|
|
70
|
+
inFence = !inFence;
|
|
71
|
+
output.push(line);
|
|
72
|
+
if (line.trim()) previousNonEmpty = line;
|
|
73
|
+
index += 1;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (!inFence && line.trim() && previousNonEmpty.trimEnd().endsWith(':')) {
|
|
78
|
+
let end = index;
|
|
79
|
+
while (end < lines.length && lines[end].trim()) end += 1;
|
|
80
|
+
const block = lines.slice(index, end);
|
|
81
|
+
const looksStructured = block.length >= 2 && block.some(blockLine => /^(?:\t| {2,})/.test(blockLine));
|
|
82
|
+
if (looksStructured) {
|
|
83
|
+
output.push('```text', ...block, '```');
|
|
84
|
+
previousNonEmpty = '```';
|
|
85
|
+
index = end;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
output.push(line);
|
|
91
|
+
if (line.trim()) previousNonEmpty = line;
|
|
92
|
+
index += 1;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return output.join('\n');
|
|
96
|
+
};
|
|
97
|
+
|
|
52
98
|
/**
|
|
53
99
|
* The line appended when the pull request still has an empty diff.
|
|
54
100
|
*
|
|
@@ -62,4 +108,4 @@ export const buildNoChangesNotice = changeStats => {
|
|
|
62
108
|
return '> ⚠️ This pull request still contains no changes - nothing was implemented yet.';
|
|
63
109
|
};
|
|
64
110
|
|
|
65
|
-
export default { buildNoChangesNotice, redactWorkspacePaths, WORKSPACE_PATH_PLACEHOLDER };
|
|
111
|
+
export default { buildNoChangesNotice, formatWorkingSessionSummaryMarkdown, redactWorkspacePaths, WORKSPACE_PATH_PLACEHOLDER };
|