@link-assistant/hive-mind 2.11.7 → 2.11.9
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 +36 -0
- package/package.json +1 -1
- package/src/bidirectional-interactive.lib.mjs +6 -2
- package/src/child-exit.lib.mjs +107 -0
- package/src/contributing-guidelines.lib.mjs +19 -6
- package/src/development-log.lib.mjs +82 -5
- package/src/fix.ci-cd-issue.lib.mjs +5 -3
- package/src/fix.mjs +5 -2
- package/src/github-entity-validation.lib.mjs +4 -1
- package/src/github.lib.mjs +3 -3
- package/src/hive.mjs +15 -21
- package/src/isolation-runner.lib.mjs +5 -2
- package/src/lib.mjs +21 -0
- package/src/locales/en.lino +10 -0
- package/src/locales/hi.lino +10 -0
- package/src/locales/ru.lino +10 -0
- package/src/locales/zh.lino +10 -0
- package/src/log-growth.lib.mjs +94 -0
- package/src/option-suggestions.lib.mjs +2 -0
- package/src/pull-request-changes.lib.mjs +94 -24
- package/src/review.mjs +12 -3
- package/src/session-kill-diagnostics.lib.mjs +388 -0
- package/src/session-kill-policy.lib.mjs +96 -0
- package/src/session-kill-recovery.lib.mjs +256 -0
- package/src/session-kill-resume.lib.mjs +175 -0
- package/src/session-monitor.kill-sections.lib.mjs +198 -0
- package/src/session-monitor.lib.mjs +97 -2
- package/src/session-monitor.oom.lib.mjs +148 -0
- package/src/session-monitor.stale-executing.lib.mjs +6 -27
- package/src/session-resume.lib.mjs +28 -2
- package/src/solve.auto-continue.lib.mjs +6 -2
- package/src/solve.auto-merge.lib.mjs +1 -1
- package/src/solve.config.lib.mjs +14 -0
- package/src/solve.keep-working.lib.mjs +7 -2
- package/src/solve.minimal-restart-prompt.lib.mjs +11 -3
- package/src/solve.preparation.lib.mjs +5 -1
- package/src/solve.progress-monitoring.lib.mjs +5 -1
- package/src/solve.repository.lib.mjs +5 -2
- package/src/solve.results.lib.mjs +13 -8
- package/src/task.mjs +5 -3
- package/src/telegram-bot.mjs +3 -1
- package/src/telegram-command-execution.lib.mjs +5 -2
package/src/locales/ru.lino
CHANGED
|
@@ -658,6 +658,16 @@ ru
|
|
|
658
658
|
label "Длительность"
|
|
659
659
|
session
|
|
660
660
|
label "Сеанс"
|
|
661
|
+
recovered
|
|
662
|
+
oom "восстановлено после нехватки памяти"
|
|
663
|
+
kill "восстановлено после принудительного завершения"
|
|
664
|
+
at "Событие зафиксировано в {{observedAt}}; рабочая сессия продолжилась и завершилась."
|
|
665
|
+
resumed "Для восстановления после завершения запущена новая рабочая сессия."
|
|
666
|
+
kill
|
|
667
|
+
cause "Причина"
|
|
668
|
+
diagnostics "Диагностика завершения"
|
|
669
|
+
resumed "🔄 Запущена новая рабочая сессия для восстановления после этого завершения: {{sessionId}}"
|
|
670
|
+
resumed_attempt "🔄 Запущена новая рабочая сессия для восстановления после этого завершения (попытка {{attempt}}): {{sessionId}}"
|
|
661
671
|
isolation
|
|
662
672
|
label "Изоляция"
|
|
663
673
|
error
|
package/src/locales/zh.lino
CHANGED
|
@@ -658,6 +658,16 @@ zh
|
|
|
658
658
|
label "耗时"
|
|
659
659
|
session
|
|
660
660
|
label "会话"
|
|
661
|
+
recovered
|
|
662
|
+
oom "已从内存不足中恢复"
|
|
663
|
+
kill "已从强制终止中恢复"
|
|
664
|
+
at "事件发生于 {{observedAt}};工作会话继续运行并已完成。"
|
|
665
|
+
resumed "已启动新的工作会话以从终止中恢复。"
|
|
666
|
+
kill
|
|
667
|
+
cause "原因"
|
|
668
|
+
diagnostics "终止诊断"
|
|
669
|
+
resumed "🔄 已启动新的工作会话以从此次终止中恢复:{{sessionId}}"
|
|
670
|
+
resumed_attempt "🔄 已启动新的工作会话以从此次终止中恢复(第 {{attempt}} 次尝试):{{sessionId}}"
|
|
661
671
|
isolation
|
|
662
672
|
label "隔离"
|
|
663
673
|
error
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Notice a session log that is running away (issue #2135).
|
|
5
|
+
*
|
|
6
|
+
* The log captured for that issue reached 286 MB / 1,354,845 lines because a
|
|
7
|
+
* single `gh pr diff` answer was mirrored to stdout, copied into the session
|
|
8
|
+
* log by the stdio interceptor, committed as the development log, and so
|
|
9
|
+
* included in the *next* run's diff - each round bigger than the last. Nothing
|
|
10
|
+
* said a word about it until the solve process died on the V8 heap limit.
|
|
11
|
+
*
|
|
12
|
+
* This module keeps a running count of the bytes written to the log and hands
|
|
13
|
+
* back a warning the first time the total crosses each threshold, so the log
|
|
14
|
+
* itself carries the evidence of its own growth.
|
|
15
|
+
*
|
|
16
|
+
* @module log-growth
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const MEGABYTE = 1024 * 1024;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Sizes a session log has no business reaching.
|
|
23
|
+
*
|
|
24
|
+
* A busy solve session writes single-digit megabytes; the captured runaway
|
|
25
|
+
* session was two orders of magnitude past that. Warning three times (rather
|
|
26
|
+
* than once) keeps the trail readable: the distance between the warnings says
|
|
27
|
+
* how fast the log is growing.
|
|
28
|
+
*/
|
|
29
|
+
export const LOG_GROWTH_THRESHOLDS = [64 * MEGABYTE, 256 * MEGABYTE, 1024 * MEGABYTE];
|
|
30
|
+
|
|
31
|
+
const formatBytes = bytes => {
|
|
32
|
+
if (bytes >= 1024 * MEGABYTE) return `${(bytes / (1024 * MEGABYTE)).toFixed(1)} GB`;
|
|
33
|
+
if (bytes >= MEGABYTE) return `${(bytes / MEGABYTE).toFixed(1)} MB`;
|
|
34
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Create an independent growth tracker.
|
|
39
|
+
*
|
|
40
|
+
* @param {object} [params]
|
|
41
|
+
* @param {number[]} [params.thresholds] - ascending byte counts to warn at.
|
|
42
|
+
* @returns {{record: (bytes: number) => string|null, reset: () => void, total: () => number}}
|
|
43
|
+
* `record` returns a warning message the first time the running total reaches
|
|
44
|
+
* the next threshold, and null otherwise.
|
|
45
|
+
*/
|
|
46
|
+
export const createLogGrowthTracker = ({ thresholds = LOG_GROWTH_THRESHOLDS } = {}) => {
|
|
47
|
+
let total = 0;
|
|
48
|
+
let nextIndex = 0;
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
record(bytes) {
|
|
52
|
+
if (!Number.isFinite(bytes) || bytes <= 0) return null;
|
|
53
|
+
total += bytes;
|
|
54
|
+
if (nextIndex >= thresholds.length || total < thresholds[nextIndex]) return null;
|
|
55
|
+
|
|
56
|
+
// Skip past every threshold this write blew through, so a single huge
|
|
57
|
+
// append produces one warning naming the size actually reached.
|
|
58
|
+
while (nextIndex < thresholds.length && total >= thresholds[nextIndex]) nextIndex += 1;
|
|
59
|
+
|
|
60
|
+
return `⚠️ Session log has grown to ${formatBytes(total)}. Something is writing very large output into it - mirrored command output (for example a "gh pr diff" of a branch that has logs committed to it) is the usual cause. See docs/case-studies/issue-2135.`;
|
|
61
|
+
},
|
|
62
|
+
reset() {
|
|
63
|
+
total = 0;
|
|
64
|
+
nextIndex = 0;
|
|
65
|
+
},
|
|
66
|
+
total() {
|
|
67
|
+
return total;
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const defaultTracker = createLogGrowthTracker();
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Count bytes written to the current session log.
|
|
76
|
+
*
|
|
77
|
+
* @param {number} bytes
|
|
78
|
+
* @returns {string|null} A warning to emit once, or null.
|
|
79
|
+
*/
|
|
80
|
+
export const recordLogBytes = bytes => defaultTracker.record(bytes);
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Start counting again - called when a new log file is set.
|
|
84
|
+
*/
|
|
85
|
+
export const resetLogGrowth = () => defaultTracker.reset();
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Bytes written to the current session log so far.
|
|
89
|
+
*
|
|
90
|
+
* @returns {number}
|
|
91
|
+
*/
|
|
92
|
+
export const getLoggedBytes = () => defaultTracker.total();
|
|
93
|
+
|
|
94
|
+
export default { createLogGrowthTracker, recordLogBytes, resetLogGrowth, getLoggedBytes, LOG_GROWTH_THRESHOLDS };
|
|
@@ -194,6 +194,8 @@ const KNOWN_OPTION_NAMES = [
|
|
|
194
194
|
'auto-resume-on-limit-reset',
|
|
195
195
|
'auto-resume-on-errors',
|
|
196
196
|
'auto-close-pull-request-on-fail',
|
|
197
|
+
'on-session-kill',
|
|
198
|
+
'session-kill-resume-attempts',
|
|
197
199
|
'auto-pull-request-creation',
|
|
198
200
|
'auto-commit-uncommitted-changes',
|
|
199
201
|
'auto-restart-on-uncommitted-changes',
|
|
@@ -36,6 +36,16 @@
|
|
|
36
36
|
*/
|
|
37
37
|
|
|
38
38
|
import { ghWithRateLimitRetry } from './github-rate-limit.lib.mjs';
|
|
39
|
+
import { quietProbe } from './quiet-probe.lib.mjs';
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Size at which a pull-request diff is worth complaining about (issue #2135).
|
|
43
|
+
*
|
|
44
|
+
* A diff this large is never the AI's source change: in the captured run it was
|
|
45
|
+
* CI logs and the solver's own development log committed into the branch. The
|
|
46
|
+
* warning is the early signal that was missing while the log grew to 286 MB.
|
|
47
|
+
*/
|
|
48
|
+
const LARGE_DIFF_WARNING_BYTES = 8 * 1024 * 1024;
|
|
39
49
|
|
|
40
50
|
/**
|
|
41
51
|
* The solver's own scaffolding files, recognised by the content it writes into
|
|
@@ -52,24 +62,76 @@ const PLACEHOLDER_CONTENT_PATTERNS = new Map([
|
|
|
52
62
|
['CLAUDE.md', [/^\+Issue to solve: \S+/m, /^\+Your prepared branch: \S+/m]],
|
|
53
63
|
]);
|
|
54
64
|
|
|
55
|
-
/**
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
65
|
+
/**
|
|
66
|
+
* Measure a unified diff in a single pass.
|
|
67
|
+
*
|
|
68
|
+
* Issue #2135: the previous implementation split the whole diff into an array
|
|
69
|
+
* of lines, concatenated every line back into a per-file `body` string, and
|
|
70
|
+
* then counted additions with `body.match(/^\+[^+]/gm)` - a regex whose result
|
|
71
|
+
* is an array holding one string per added line. For the 60 MB pull-request
|
|
72
|
+
* diff captured in that run (the AI had committed CI logs and the solver's own
|
|
73
|
+
* development log into the branch) those three copies of the diff, plus a
|
|
74
|
+
* multi-million-entry match array, were a large part of the heap that ended the
|
|
75
|
+
* session with `FATAL ERROR: Reached heap limit`.
|
|
76
|
+
*
|
|
77
|
+
* This pass keeps no copy of the diff: it walks the string by line offsets,
|
|
78
|
+
* counts as it goes, and retains section text only for the two paths that can
|
|
79
|
+
* possibly be the solver's placeholder.
|
|
80
|
+
*
|
|
81
|
+
* The counting rules are unchanged: a line is an addition when it starts with
|
|
82
|
+
* `+` followed by a character other than `+` (so the `+++ b/path` header is not
|
|
83
|
+
* counted), and a deletion when it starts with `-` followed by a character
|
|
84
|
+
* other than `-`. Lines before the first `diff --git` header belong to no file
|
|
85
|
+
* and are ignored, exactly as they were when sections were built by splitting.
|
|
86
|
+
*
|
|
87
|
+
* @param {string} diff - unified diff text, possibly empty.
|
|
88
|
+
* @returns {{filesChanged: number, additions: number, deletions: number, placeholderSections: number}}
|
|
89
|
+
*/
|
|
90
|
+
const measureDiff = diff => {
|
|
91
|
+
let filesChanged = 0;
|
|
92
|
+
let additions = 0;
|
|
93
|
+
let deletions = 0;
|
|
94
|
+
let placeholderSections = 0;
|
|
95
|
+
let section = null;
|
|
96
|
+
|
|
97
|
+
const closeSection = () => {
|
|
98
|
+
if (!section) return;
|
|
99
|
+
const isPlaceholder = Boolean(section.patterns) && section.patterns.every(pattern => pattern.test(section.body));
|
|
100
|
+
if (isPlaceholder) placeholderSections += 1;
|
|
101
|
+
else {
|
|
102
|
+
filesChanged += 1;
|
|
103
|
+
additions += section.additions;
|
|
104
|
+
deletions += section.deletions;
|
|
105
|
+
}
|
|
106
|
+
section = null;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
for (let start = 0; start < diff.length; ) {
|
|
110
|
+
let end = diff.indexOf('\n', start);
|
|
111
|
+
if (end === -1) end = diff.length;
|
|
112
|
+
const line = diff.slice(start, end);
|
|
113
|
+
start = end + 1;
|
|
114
|
+
|
|
59
115
|
if (line.startsWith('diff --git ')) {
|
|
116
|
+
closeSection();
|
|
60
117
|
const match = /^diff --git a\/(.+) b\/(.+)$/.exec(line);
|
|
61
|
-
|
|
118
|
+
const path = match ? match[2] : '';
|
|
119
|
+
const patterns = PLACEHOLDER_CONTENT_PATTERNS.get(path) || null;
|
|
120
|
+
section = { patterns, body: '', additions: 0, deletions: 0 };
|
|
62
121
|
continue;
|
|
63
122
|
}
|
|
64
|
-
if (
|
|
123
|
+
if (!section) continue;
|
|
124
|
+
// Only a placeholder candidate needs its text kept; every other file is
|
|
125
|
+
// reduced to two counters as it streams past.
|
|
126
|
+
if (section.patterns) section.body += `${line}\n`;
|
|
127
|
+
if (line.length > 1) {
|
|
128
|
+
if (line[0] === '+' && line[1] !== '+') section.additions += 1;
|
|
129
|
+
else if (line[0] === '-' && line[1] !== '-') section.deletions += 1;
|
|
130
|
+
}
|
|
65
131
|
}
|
|
66
|
-
|
|
67
|
-
};
|
|
132
|
+
closeSection();
|
|
68
133
|
|
|
69
|
-
|
|
70
|
-
const isPlaceholderSection = section => {
|
|
71
|
-
const patterns = PLACEHOLDER_CONTENT_PATTERNS.get(section.path);
|
|
72
|
-
return Boolean(patterns) && patterns.every(pattern => pattern.test(section.body));
|
|
134
|
+
return { filesChanged, additions, deletions, placeholderSections };
|
|
73
135
|
};
|
|
74
136
|
|
|
75
137
|
/**
|
|
@@ -84,17 +146,25 @@ const isPlaceholderSection = section => {
|
|
|
84
146
|
* @param {string} params.repo
|
|
85
147
|
* @param {number} params.prNumber
|
|
86
148
|
* @param {Function} params.$ command-stream tagged-template executor
|
|
87
|
-
* @
|
|
149
|
+
* @param {Function} [params.log] - optional logger for the size diagnostic
|
|
150
|
+
* @returns {Promise<{hasChanges: boolean, filesChanged: number, additions: number, deletions: number, placeholderOnly: boolean, measured: boolean, diffBytes: number}>}
|
|
88
151
|
* The counts cover the AI's own work: the solver's placeholder file is
|
|
89
152
|
* excluded and reported through `placeholderOnly` instead. `measured` is
|
|
90
153
|
* false when the diff could not be fetched, in which case callers must not
|
|
91
|
-
* treat the pull request as empty.
|
|
154
|
+
* treat the pull request as empty. `diffBytes` is the size of the diff that
|
|
155
|
+
* was measured, so a caller can see a runaway pull request growing.
|
|
92
156
|
*/
|
|
93
|
-
export const getPullRequestChangeStats = async ({ owner, repo, prNumber,
|
|
157
|
+
export const getPullRequestChangeStats = async ({ owner, repo, prNumber, $, log = null }) => {
|
|
94
158
|
let diffOutput = '';
|
|
95
159
|
let measured = false;
|
|
96
160
|
try {
|
|
97
|
-
|
|
161
|
+
// Issue #2135: `mirror: false`. This diff is read to answer one yes/no
|
|
162
|
+
// question, and every caller reports the answer in words - but it was being
|
|
163
|
+
// echoed into the log that the solver then attaches to the pull request and
|
|
164
|
+
// (with --development-log) commits into the branch, which put the previous
|
|
165
|
+
// copy of the diff inside the next one. Seven such copies grew one session
|
|
166
|
+
// log to 286 MB and ended it with a V8 out-of-memory abort.
|
|
167
|
+
const result = await ghWithRateLimitRetry(() => quietProbe($)`gh pr diff ${prNumber} --repo ${owner}/${repo}`, { label: `pr diff ${owner}/${repo}#${prNumber}` });
|
|
98
168
|
if (result.code === 0) {
|
|
99
169
|
diffOutput = result.stdout.toString();
|
|
100
170
|
measured = true;
|
|
@@ -103,22 +173,22 @@ export const getPullRequestChangeStats = async ({ owner, repo, prNumber, $ }) =>
|
|
|
103
173
|
// Leave measured false: an unreachable API must not read as "no changes".
|
|
104
174
|
}
|
|
105
175
|
|
|
106
|
-
const
|
|
107
|
-
const
|
|
108
|
-
const realSections = sections.filter(section => !isPlaceholderSection(section));
|
|
176
|
+
const { filesChanged, additions, deletions, placeholderSections } = measureDiff(diffOutput);
|
|
177
|
+
const diffBytes = diffOutput.length;
|
|
109
178
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
179
|
+
if (measured && diffBytes >= LARGE_DIFF_WARNING_BYTES && typeof log === 'function') {
|
|
180
|
+
// Always megabytes: the threshold itself is 8 MB, so no unit choice is needed.
|
|
181
|
+
await log(`⚠️ Pull request #${prNumber} diff is ${(diffBytes / (1024 * 1024)).toFixed(1)} MB - measuring it is slow and memory-hungry; check whether logs or build output were committed to the branch`, { level: 'warning' });
|
|
182
|
+
}
|
|
114
183
|
|
|
115
184
|
return {
|
|
116
185
|
hasChanges: filesChanged > 0,
|
|
117
186
|
filesChanged,
|
|
118
187
|
additions,
|
|
119
188
|
deletions,
|
|
120
|
-
placeholderOnly: filesChanged === 0 && placeholderSections
|
|
189
|
+
placeholderOnly: filesChanged === 0 && placeholderSections > 0,
|
|
121
190
|
measured,
|
|
191
|
+
diffBytes,
|
|
122
192
|
};
|
|
123
193
|
};
|
|
124
194
|
|
package/src/review.mjs
CHANGED
|
@@ -48,6 +48,7 @@ const fs = (await use('fs')).promises;
|
|
|
48
48
|
|
|
49
49
|
// Import shared functions from lib.mjs to follow DRY principle
|
|
50
50
|
import { parseCliArgumentsWithLino } from './cli-arguments.lib.mjs';
|
|
51
|
+
import { QUIET_PROBE } from './quiet-probe.lib.mjs';
|
|
51
52
|
import { reportError } from './sentry.lib.mjs';
|
|
52
53
|
import * as memoryCheck from './memory-check.mjs';
|
|
53
54
|
|
|
@@ -222,7 +223,11 @@ let limitReached = false;
|
|
|
222
223
|
try {
|
|
223
224
|
// Get PR details first
|
|
224
225
|
await log('📊 Getting pull request details...');
|
|
225
|
-
|
|
226
|
+
// Issue #2135: `mirror: false`. The answer is a JSON object holding the whole
|
|
227
|
+
// description and every changed file - it is written to a file for the AI
|
|
228
|
+
// tool and summarised in words below, so echoing it into the log only grew
|
|
229
|
+
// the log that is later attached to the pull request.
|
|
230
|
+
const prDetailsResult = await $(QUIET_PROBE)`gh pr view ${prUrl} --json title,body,headRefName,baseRefName,author,number,state,files`;
|
|
226
231
|
|
|
227
232
|
if (prDetailsResult.code !== 0) {
|
|
228
233
|
await log('Error: Failed to get PR details', { level: 'error' });
|
|
@@ -271,7 +276,11 @@ try {
|
|
|
271
276
|
|
|
272
277
|
// Get the diff for the PR
|
|
273
278
|
await log('📝 Getting PR diff...');
|
|
274
|
-
|
|
279
|
+
// Issue #2135: `mirror: false`. The diff is saved to a file (below) and its
|
|
280
|
+
// size is reported in words; mirroring it copied a whole pull-request diff
|
|
281
|
+
// into the session log, which is exactly the growth that ended one run with
|
|
282
|
+
// an out-of-memory abort.
|
|
283
|
+
const diffResult = await $(QUIET_PROBE)`gh pr diff ${prUrl}`;
|
|
275
284
|
|
|
276
285
|
if (diffResult.code !== 0) {
|
|
277
286
|
await log('Error: Failed to get PR diff', { level: 'error' });
|
|
@@ -426,7 +435,7 @@ Review this pull request thoroughly.`;
|
|
|
426
435
|
|
|
427
436
|
try {
|
|
428
437
|
// Get reviews for the PR
|
|
429
|
-
const reviewsResult = await
|
|
438
|
+
const reviewsResult = await $(QUIET_PROBE)`gh api repos/${owner}/${repo}/pulls/${prNumber}/reviews --paginate --jq '.[] | select(.user.login == "'$(gh api user --jq .login)'") | {state, submitted_at}'`;
|
|
430
439
|
|
|
431
440
|
if (reviewsResult.code === 0 && reviewsResult.stdout.toString().trim()) {
|
|
432
441
|
await log(`✅ Review has been submitted to PR #${prNumber}`);
|