@link-assistant/hive-mind 2.11.13 → 2.12.1

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.
Files changed (41) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/package.json +4 -1
  3. package/src/agent-command.lib.mjs +74 -0
  4. package/src/agent.lib.mjs +59 -34
  5. package/src/agentic-cli-updater.lib.mjs +241 -0
  6. package/src/claude.connection.lib.mjs +209 -0
  7. package/src/claude.lib.mjs +6 -202
  8. package/src/codex.lib.mjs +0 -128
  9. package/src/formal-ai-isolation.lib.mjs +62 -0
  10. package/src/formal-ai-maintenance.lib.mjs +106 -0
  11. package/src/formal-ai-model.lib.mjs +25 -0
  12. package/src/formal-ai-runtime.lib.mjs +10 -0
  13. package/src/formal-ai-sidecar.lib.mjs +565 -0
  14. package/src/formal-ai-updater.lib.mjs +294 -0
  15. package/src/formal-ai-version.lib.mjs +100 -0
  16. package/src/formal-ai.lib.mjs +11 -16
  17. package/src/github-rate-limit.lib.mjs +3 -0
  18. package/src/github-url-parser.lib.mjs +255 -0
  19. package/src/github.lib.mjs +22 -343
  20. package/src/hive.mjs +0 -152
  21. package/src/interactive-mode.lib.mjs +0 -43
  22. package/src/isolation-runner.lib.mjs +44 -173
  23. package/src/limits.lib.mjs +0 -89
  24. package/src/model-args.lib.mjs +32 -0
  25. package/src/models/index.mjs +5 -19
  26. package/src/session-monitor.lib.mjs +14 -172
  27. package/src/solve.auto-merge.lib.mjs +70 -164
  28. package/src/solve.mjs +31 -193
  29. package/src/solve.repository.lib.mjs +0 -83
  30. package/src/solve.results.lib.mjs +2 -92
  31. package/src/solve.session.lib.mjs +52 -19
  32. package/src/solve.tool-uncommitted.lib.mjs +22 -0
  33. package/src/state-lock.lib.mjs +82 -0
  34. package/src/telegram-bot.mjs +17 -65
  35. package/src/telegram-fix-command.lib.mjs +1 -8
  36. package/src/telegram-merge-queue.lib.mjs +3 -155
  37. package/src/telegram-solve-queue.lib.mjs +9 -168
  38. package/src/telegram-task-command.lib.mjs +1 -8
  39. package/src/use-m-bootstrap.lib.mjs +6 -5
  40. package/src/use-with-retry.lib.mjs +128 -2
  41. package/src/working-session-summary.lib.mjs +47 -1
@@ -62,6 +62,48 @@ function getSessionCommentContent(sessionType, timestamp) {
62
62
  }
63
63
  }
64
64
 
65
+ /**
66
+ * Post the tracked comment that marks a work-session start.
67
+ *
68
+ * This is separate from startWorkSession because an in-process continuation
69
+ * after a usage-limit wait is already inside a work session: the PR is already
70
+ * draft, but reviewers still need an explicit boundary for the resumed tool
71
+ * execution.
72
+ *
73
+ * @param {Object} options
74
+ * @param {string} options.owner
75
+ * @param {string} options.repo
76
+ * @param {number} options.prNumber
77
+ * @param {Function} options.$
78
+ * @param {Function} options.log
79
+ * @param {Function} options.formatAligned
80
+ * @param {string} options.sessionType
81
+ * @param {Date} [options.timestamp]
82
+ */
83
+ export async function postWorkSessionStartComment({ owner, repo, prNumber, $, log, formatAligned, sessionType, timestamp = new Date() }) {
84
+ try {
85
+ const { emoji, header, description } = getSessionCommentContent(sessionType, timestamp);
86
+ const startComment = `${emoji} **${header}**\n\n${description}`;
87
+ const { ok, commentId, stderr } = await postTrackedComment({ $, owner, repo, targetNumber: prNumber, body: startComment });
88
+ if (ok) {
89
+ await log(formatAligned('💬', 'Posted:', `${header} comment${commentId ? ` (id=${commentId})` : ''}`, 2));
90
+ } else {
91
+ await log(`Warning: Could not post work start comment: ${stderr || 'unknown error'}`, { level: 'warning' });
92
+ }
93
+ } catch (error) {
94
+ const sentryLib = await import('./sentry.lib.mjs');
95
+ const { reportError } = sentryLib;
96
+ reportError(error, {
97
+ context: 'post_start_comment',
98
+ owner,
99
+ repo,
100
+ prNumber,
101
+ operation: 'create_pr_comment',
102
+ });
103
+ await log('Warning: Could not post work start comment', { level: 'warning' });
104
+ }
105
+ }
106
+
65
107
  /**
66
108
  * Start a work session and post appropriate comment
67
109
  * @param {Object} options - Session options
@@ -101,25 +143,16 @@ export async function startWorkSession({ isContinueMode, prNumber, argv, log, fo
101
143
  // Post a comment marking the start of work session with appropriate header based on session type.
102
144
  // Issue #1625: Use postTrackedComment so the comment ID is registered in-memory and can be
103
145
  // excluded from the "did the AI post anything?" check in checkForAiCreatedComments().
104
- try {
105
- const { emoji, header, description } = getSessionCommentContent(sessionType, workStartTime);
106
- const startComment = `${emoji} **${header}**\n\n${description}`;
107
- const { ok, commentId, stderr } = await postTrackedComment({ $, owner: global.owner, repo: global.repo, targetNumber: prNumber, body: startComment });
108
- if (ok) {
109
- await log(formatAligned('💬', 'Posted:', `${header} comment${commentId ? ` (id=${commentId})` : ''}`, 2));
110
- } else {
111
- await log(`Warning: Could not post work start comment: ${stderr || 'unknown error'}`, { level: 'warning' });
112
- }
113
- } catch (error) {
114
- const sentryLib = await import('./sentry.lib.mjs');
115
- const { reportError } = sentryLib;
116
- reportError(error, {
117
- context: 'post_start_comment',
118
- prNumber,
119
- operation: 'create_pr_comment',
120
- });
121
- await log('Warning: Could not post work start comment', { level: 'warning' });
122
- }
146
+ await postWorkSessionStartComment({
147
+ owner: global.owner,
148
+ repo: global.repo,
149
+ prNumber,
150
+ $,
151
+ log,
152
+ formatAligned,
153
+ sessionType,
154
+ timestamp: workStartTime,
155
+ });
123
156
  }
124
157
 
125
158
  return workStartTime;
@@ -0,0 +1,22 @@
1
+ const TOOL_MODULES = {
2
+ agent: './agent.lib.mjs',
3
+ codex: './codex.lib.mjs',
4
+ gemini: './gemini.lib.mjs',
5
+ opencode: './opencode.lib.mjs',
6
+ qwen: './qwen.lib.mjs',
7
+ };
8
+
9
+ export async function resolveUncommittedChangesTool({ argv, claudeLib, importModule = specifier => import(specifier) }) {
10
+ if (argv.useAgentCommander) {
11
+ const agentCommanderLib = await importModule('./agent-commander.lib.mjs');
12
+ return { agentCommanderLib, checkForUncommittedChanges: agentCommanderLib.checkForUncommittedChanges };
13
+ }
14
+
15
+ const toolModule = TOOL_MODULES[argv.tool];
16
+ if (toolModule) {
17
+ const module = await importModule(toolModule);
18
+ return { agentCommanderLib: null, checkForUncommittedChanges: module.checkForUncommittedChanges };
19
+ }
20
+
21
+ return { agentCommanderLib: null, checkForUncommittedChanges: claudeLib.checkForUncommittedChanges };
22
+ }
@@ -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 };