@link-assistant/hive-mind 2.0.6 → 2.0.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 +23 -0
- package/package.json +1 -1
- package/src/session-monitor.lib.mjs +37 -1
- package/src/solve.disk-diagnostics.lib.mjs +342 -0
- package/src/solve.mjs +9 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# @link-assistant/hive-mind
|
|
2
2
|
|
|
3
|
+
## 2.0.7
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 6d9a2bb: feat(solve): log working-tree size before/after the AI agent and warn on Telegram when disk usage exceeds 5 GB (#1945)
|
|
8
|
+
|
|
9
|
+
`/solve` now records the size of its temporary working tree at two checkpoints:
|
|
10
|
+
after the repository is cloned (before the AI agent starts) and after the AI
|
|
11
|
+
working session ends. Both checkpoints emit a structured `📊 [DISK]` marker into
|
|
12
|
+
the captured solve log, so the cloned-repo size, the AI-induced delta, and the
|
|
13
|
+
final total are visible in `tail -f`-style debugging.
|
|
14
|
+
|
|
15
|
+
The session monitor parses those markers from the captured log and appends a
|
|
16
|
+
`💾 Disk usage` block to the Telegram completion message. The block raises a
|
|
17
|
+
warning when the cloned repository exceeds 5 GB, when the working tree grew by
|
|
18
|
+
more than 5 GB during the run, or when the total disk usage for the task
|
|
19
|
+
exceeds 5 GB — exactly the three conditions called out in the issue.
|
|
20
|
+
|
|
21
|
+
Sizing uses `du -sb` (byte-accurate on Linux), falls back to `du -sk` on BSD/
|
|
22
|
+
macOS, and finally to `fs.statSync` for single-file targets — no new runtime
|
|
23
|
+
dependency. The threshold is 5 GiB and uses a strict `>` comparison, so a tree
|
|
24
|
+
that lands at exactly 5 GiB does not warn.
|
|
25
|
+
|
|
3
26
|
## 2.0.6
|
|
4
27
|
|
|
5
28
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -297,6 +297,28 @@ async function resolvePullRequestUrlFromSessionLog(logPath, ctx, { verbose = fal
|
|
|
297
297
|
}
|
|
298
298
|
}
|
|
299
299
|
|
|
300
|
+
/**
|
|
301
|
+
* Issue #1945: Parse `📊 [DISK]` checkpoint markers out of the captured solve
|
|
302
|
+
* log and, when the captured sizes cross the 5 GB threshold(s), build a
|
|
303
|
+
* Telegram extraSection that warns the operator. Returns an empty string if
|
|
304
|
+
* the log is unreadable or contains no markers.
|
|
305
|
+
*/
|
|
306
|
+
async function buildDiskDiagnosticsExtraSection(logPath, { verbose = false, readFile = fs.readFile } = {}) {
|
|
307
|
+
if (!logPath) return '';
|
|
308
|
+
try {
|
|
309
|
+
const diskLib = await import('./solve.disk-diagnostics.lib.mjs');
|
|
310
|
+
const logText = await readFile(logPath, 'utf8');
|
|
311
|
+
const parsed = diskLib.parseDiskMarkers(logText);
|
|
312
|
+
if (!parsed.afterClone && !parsed.afterAgent) return '';
|
|
313
|
+
return diskLib.formatDiskDiagnosticsBlock(parsed);
|
|
314
|
+
} catch (error) {
|
|
315
|
+
if (verbose) {
|
|
316
|
+
console.log(`[VERBOSE] Could not inspect session log ${logPath} for disk diagnostics: ${error?.message || error}`);
|
|
317
|
+
}
|
|
318
|
+
return '';
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
300
322
|
function isNonIsolationSessionActive(sessionName, sessionInfo, verbose = false) {
|
|
301
323
|
const startTime = sessionInfo.startTime instanceof Date ? sessionInfo.startTime : new Date(sessionInfo.startTime);
|
|
302
324
|
const elapsed = Date.now() - startTime.getTime();
|
|
@@ -648,6 +670,20 @@ export async function monitorSessions(bot, verbose = false, options = {}) {
|
|
|
648
670
|
}
|
|
649
671
|
}
|
|
650
672
|
|
|
673
|
+
// Issue #1945: append a "💾 Disk usage" block (with warnings when the
|
|
674
|
+
// cloned repo, the delta during the run, or the total exceed 5 GB)
|
|
675
|
+
// parsed from the captured solve log markers.
|
|
676
|
+
const diskExtraSections = [];
|
|
677
|
+
try {
|
|
678
|
+
const diskLogPath = statusResult?.logPath || sessionInfo?.logPath || null;
|
|
679
|
+
const diskBlock = await buildDiskDiagnosticsExtraSection(diskLogPath, { verbose });
|
|
680
|
+
if (diskBlock) diskExtraSections.push(diskBlock);
|
|
681
|
+
} catch (diskError) {
|
|
682
|
+
if (verbose) {
|
|
683
|
+
console.log(`[VERBOSE] Could not build disk diagnostics section for ${sessionName}: ${diskError?.message || diskError}`);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
651
687
|
const message = formatSessionCompletionMessage({
|
|
652
688
|
sessionName,
|
|
653
689
|
sessionInfo,
|
|
@@ -656,7 +692,7 @@ export async function monitorSessions(bot, verbose = false, options = {}) {
|
|
|
656
692
|
exitCode: finalExitCode,
|
|
657
693
|
infoBlock: sessionInfo?.infoBlock || '',
|
|
658
694
|
pullRequestUrl,
|
|
659
|
-
extraSections: [...limitsExtraSections, ...resumeExtraSections],
|
|
695
|
+
extraSections: [...limitsExtraSections, ...resumeExtraSections, ...diskExtraSections],
|
|
660
696
|
});
|
|
661
697
|
|
|
662
698
|
// Update the original reply message if messageId is available, otherwise send new message
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Disk-space diagnostics for the `/solve` command (issue #1945).
|
|
3
|
+
*
|
|
4
|
+
* Captures two checkpoints around the AI working session:
|
|
5
|
+
*
|
|
6
|
+
* 1. AFTER_CLONE — size of the freshly-cloned `tempDir` BEFORE the AI agent
|
|
7
|
+
* starts. Tells us how large the repository itself is.
|
|
8
|
+
* 2. AFTER_AGENT — size of the same `tempDir` AFTER the AI agent has
|
|
9
|
+
* finished, so we can see how many bytes the working session added.
|
|
10
|
+
*
|
|
11
|
+
* Both checkpoints are written to the captured solve log as a single-line
|
|
12
|
+
* structured marker. The Telegram bot's `session-monitor.lib.mjs` parses those
|
|
13
|
+
* markers and, on the completion message, surfaces a Telegram block plus
|
|
14
|
+
* warnings when any of the three thresholds from the issue are crossed:
|
|
15
|
+
*
|
|
16
|
+
* - cloned repository > WARNING_THRESHOLD_BYTES
|
|
17
|
+
* - delta during run > WARNING_THRESHOLD_BYTES
|
|
18
|
+
* - total space used > WARNING_THRESHOLD_BYTES
|
|
19
|
+
*
|
|
20
|
+
* Implementation notes:
|
|
21
|
+
*
|
|
22
|
+
* - Uses `du -sb <path>` on Linux for byte-accurate sizing, falls back to
|
|
23
|
+
* `du -sk <path>` (kilobytes ×1024) on systems without GNU coreutils
|
|
24
|
+
* (macOS BSD `du` doesn't support `-b`). A final fs.statSync fallback
|
|
25
|
+
* keeps the helper non-throwing for plain files / inaccessible dirs.
|
|
26
|
+
* - The marker format is deliberately ASCII and key=value so it survives
|
|
27
|
+
* log truncation and stays parseable with a one-line regex. We DO NOT
|
|
28
|
+
* emit JSON because the existing log is human-tailing-friendly and a
|
|
29
|
+
* stray closing brace from another logger could confuse JSON.parse.
|
|
30
|
+
*
|
|
31
|
+
* @see https://github.com/link-assistant/hive-mind/issues/1945
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import { execFileSync } from 'node:child_process';
|
|
35
|
+
import fs from 'node:fs';
|
|
36
|
+
|
|
37
|
+
/** 5 GB threshold (binary). Matches the issue body verbatim. */
|
|
38
|
+
export const WARNING_THRESHOLD_BYTES = 5 * 1024 * 1024 * 1024;
|
|
39
|
+
|
|
40
|
+
export const DISK_MARKER_PREFIX = '📊 [DISK]';
|
|
41
|
+
export const DISK_PHASE_AFTER_CLONE = 'after_clone';
|
|
42
|
+
export const DISK_PHASE_AFTER_AGENT = 'after_agent';
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Measure the size of a path in bytes. Robust to missing tools / paths.
|
|
46
|
+
*
|
|
47
|
+
* @param {string} targetPath
|
|
48
|
+
* @returns {number|null} Bytes, or null if the path is missing/unreadable.
|
|
49
|
+
*/
|
|
50
|
+
export function measureDirectorySize(targetPath) {
|
|
51
|
+
if (!targetPath) return null;
|
|
52
|
+
// Prefer `du -sb` (GNU coreutils) for byte-accurate sizing.
|
|
53
|
+
try {
|
|
54
|
+
const out = execFileSync('du', ['-sb', targetPath], {
|
|
55
|
+
encoding: 'utf8',
|
|
56
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
57
|
+
timeout: 60_000,
|
|
58
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
59
|
+
}).trim();
|
|
60
|
+
const bytes = parseInt(out.split(/\s+/)[0], 10);
|
|
61
|
+
if (Number.isFinite(bytes) && bytes >= 0) return bytes;
|
|
62
|
+
} catch {
|
|
63
|
+
// Fall through to -sk fallback for BSD du / macOS.
|
|
64
|
+
}
|
|
65
|
+
// BSD `du` (macOS) doesn't support -b but does support -sk (kilobytes).
|
|
66
|
+
try {
|
|
67
|
+
const out = execFileSync('du', ['-sk', targetPath], {
|
|
68
|
+
encoding: 'utf8',
|
|
69
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
70
|
+
timeout: 60_000,
|
|
71
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
72
|
+
}).trim();
|
|
73
|
+
const kb = parseInt(out.split(/\s+/)[0], 10);
|
|
74
|
+
if (Number.isFinite(kb) && kb >= 0) return kb * 1024;
|
|
75
|
+
} catch {
|
|
76
|
+
// Fall through to fs.statSync — last resort for single-file paths.
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
const stat = fs.statSync(targetPath);
|
|
80
|
+
return stat.size;
|
|
81
|
+
} catch {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Human-readable byte format. Two flavours:
|
|
88
|
+
* - `formatBytes(bytes)` → `"12.0 GB"` (matches limits.lib.mjs style)
|
|
89
|
+
* - `formatBytesCompact(b)` → `"12G"` (matches the issue body verbatim
|
|
90
|
+
* and cleanup.lib.mjs)
|
|
91
|
+
*
|
|
92
|
+
* @param {number|null|undefined} bytes
|
|
93
|
+
* @returns {string}
|
|
94
|
+
*/
|
|
95
|
+
export function formatBytes(bytes) {
|
|
96
|
+
if (bytes == null || Number.isNaN(bytes)) return '? B';
|
|
97
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
98
|
+
const units = ['KB', 'MB', 'GB', 'TB', 'PB'];
|
|
99
|
+
let value = bytes / 1024;
|
|
100
|
+
let unit = 0;
|
|
101
|
+
while (value >= 1024 && unit < units.length - 1) {
|
|
102
|
+
value /= 1024;
|
|
103
|
+
unit++;
|
|
104
|
+
}
|
|
105
|
+
// 1 decimal for GB and above (matches limits.lib formatBytes), none below.
|
|
106
|
+
const decimals = units[unit] === 'GB' || units[unit] === 'TB' || units[unit] === 'PB' ? 1 : 0;
|
|
107
|
+
return `${value.toFixed(decimals)} ${units[unit]}`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Signed byte delta — adds a leading "+" for positive non-zero values so a
|
|
112
|
+
* growth like 500 MB renders as "+500 MB" in both logs and Telegram.
|
|
113
|
+
*/
|
|
114
|
+
export function formatBytesDelta(bytes) {
|
|
115
|
+
if (bytes == null || Number.isNaN(bytes)) return '? B';
|
|
116
|
+
if (bytes === 0) return '±0 B';
|
|
117
|
+
const sign = bytes > 0 ? '+' : '-';
|
|
118
|
+
return `${sign}${formatBytes(Math.abs(bytes))}`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function escapeForMarker(value) {
|
|
122
|
+
// Strip newlines and the marker prefix so a path containing the literal
|
|
123
|
+
// "📊 [DISK]" cannot inject a fake marker. Paths almost never contain spaces
|
|
124
|
+
// in /tmp but we still quote with backticks for the human-readable suffix
|
|
125
|
+
// and use key=value pairs for the parseable head.
|
|
126
|
+
return String(value)
|
|
127
|
+
.replace(/[\r\n]+/g, ' ')
|
|
128
|
+
.slice(0, 2048);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Build a single-line structured log marker the parent (Telegram bot) can
|
|
133
|
+
* parse out of the captured log to surface size warnings.
|
|
134
|
+
*
|
|
135
|
+
* Example (after_clone):
|
|
136
|
+
* 📊 [DISK] phase=after_clone bytes=12884901888 path=/tmp/foo size=12.0 GB
|
|
137
|
+
*
|
|
138
|
+
* Example (after_agent):
|
|
139
|
+
* 📊 [DISK] phase=after_agent bytes=13312000000 deltaBytes=524288000 path=/tmp/foo size=12.4 GB delta=+500.0 MB
|
|
140
|
+
*
|
|
141
|
+
* @param {Object} params
|
|
142
|
+
* @param {string} params.phase - 'after_clone' | 'after_agent'
|
|
143
|
+
* @param {number|null} params.bytes - Current size of tempDir in bytes
|
|
144
|
+
* @param {number|null} [params.deltaBytes] - Bytes added since after_clone (after_agent only)
|
|
145
|
+
* @param {string} params.path - The measured path
|
|
146
|
+
* @returns {string}
|
|
147
|
+
*/
|
|
148
|
+
export function buildDiskMarker({ phase, bytes, deltaBytes = null, path: targetPath }) {
|
|
149
|
+
const head = [`phase=${phase}`];
|
|
150
|
+
if (Number.isFinite(bytes)) head.push(`bytes=${bytes}`);
|
|
151
|
+
if (Number.isFinite(deltaBytes)) head.push(`deltaBytes=${deltaBytes}`);
|
|
152
|
+
head.push(`path=${escapeForMarker(targetPath || '')}`);
|
|
153
|
+
const suffixParts = [];
|
|
154
|
+
if (Number.isFinite(bytes)) suffixParts.push(`size=${formatBytes(bytes)}`);
|
|
155
|
+
if (Number.isFinite(deltaBytes)) suffixParts.push(`delta=${formatBytesDelta(deltaBytes)}`);
|
|
156
|
+
const suffix = suffixParts.length ? ` ${suffixParts.join(' ')}` : '';
|
|
157
|
+
return `${DISK_MARKER_PREFIX} ${head.join(' ')}${suffix}`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Parse all `📊 [DISK]` markers out of a captured solve log. The LAST marker
|
|
162
|
+
* for each phase wins (sessions that restart can emit more than one).
|
|
163
|
+
*
|
|
164
|
+
* @param {string} logText
|
|
165
|
+
* @returns {{
|
|
166
|
+
* afterClone: {bytes:number|null, path:string|null} | null,
|
|
167
|
+
* afterAgent: {bytes:number|null, deltaBytes:number|null, path:string|null} | null
|
|
168
|
+
* }}
|
|
169
|
+
*/
|
|
170
|
+
export function parseDiskMarkers(logText) {
|
|
171
|
+
const result = { afterClone: null, afterAgent: null };
|
|
172
|
+
if (!logText || typeof logText !== 'string') return result;
|
|
173
|
+
// Anchor to the marker prefix so a quoted user comment containing this
|
|
174
|
+
// string mid-line is not mistakenly parsed.
|
|
175
|
+
const re = /📊 \[DISK\] ([^\n\r]+)/g;
|
|
176
|
+
let m;
|
|
177
|
+
while ((m = re.exec(logText)) !== null) {
|
|
178
|
+
const pairs = {};
|
|
179
|
+
// key=value tokens, where value runs until next " key=" or EOL.
|
|
180
|
+
const tokenRe = /(\w+)=([^\s][^\n\r]*?)(?=\s+\w+=|$)/g;
|
|
181
|
+
let t;
|
|
182
|
+
while ((t = tokenRe.exec(m[1])) !== null) {
|
|
183
|
+
pairs[t[1]] = t[2];
|
|
184
|
+
}
|
|
185
|
+
const phase = pairs.phase;
|
|
186
|
+
if (phase !== DISK_PHASE_AFTER_CLONE && phase !== DISK_PHASE_AFTER_AGENT) continue;
|
|
187
|
+
const bytes = parseInt(pairs.bytes, 10);
|
|
188
|
+
const deltaBytes = parseInt(pairs.deltaBytes, 10);
|
|
189
|
+
const entry = {
|
|
190
|
+
bytes: Number.isFinite(bytes) ? bytes : null,
|
|
191
|
+
path: pairs.path || null,
|
|
192
|
+
};
|
|
193
|
+
if (phase === DISK_PHASE_AFTER_AGENT) {
|
|
194
|
+
entry.deltaBytes = Number.isFinite(deltaBytes) ? deltaBytes : null;
|
|
195
|
+
result.afterAgent = entry;
|
|
196
|
+
} else {
|
|
197
|
+
result.afterClone = entry;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return result;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Decide which of the three issue thresholds were crossed.
|
|
205
|
+
*
|
|
206
|
+
* @param {{afterClone: object|null, afterAgent: object|null}} parsed
|
|
207
|
+
* @param {number} [threshold=WARNING_THRESHOLD_BYTES]
|
|
208
|
+
* @returns {{cloneTooLarge:boolean, deltaTooLarge:boolean, totalTooLarge:boolean}}
|
|
209
|
+
*/
|
|
210
|
+
export function computeDiskWarnings(parsed, threshold = WARNING_THRESHOLD_BYTES) {
|
|
211
|
+
const cloneBytes = parsed?.afterClone?.bytes ?? null;
|
|
212
|
+
const totalBytes = parsed?.afterAgent?.bytes ?? cloneBytes;
|
|
213
|
+
const deltaBytes = parsed?.afterAgent?.deltaBytes ?? null;
|
|
214
|
+
return {
|
|
215
|
+
cloneTooLarge: Number.isFinite(cloneBytes) && cloneBytes > threshold,
|
|
216
|
+
deltaTooLarge: Number.isFinite(deltaBytes) && deltaBytes > threshold,
|
|
217
|
+
totalTooLarge: Number.isFinite(totalBytes) && totalBytes > threshold,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Telegram block (Markdown code fence) describing the captured sizes plus,
|
|
223
|
+
* when any threshold is crossed, a `⚠️ Warnings:` tail. Returns an empty
|
|
224
|
+
* string when there are no markers in the log (no logs ⇒ no surprise output).
|
|
225
|
+
*
|
|
226
|
+
* Returned shape:
|
|
227
|
+
*
|
|
228
|
+
* 💾 Disk usage (gh-issue-solver-…)
|
|
229
|
+
* ```
|
|
230
|
+
* Cloned repository: 12.0 GB
|
|
231
|
+
* After agent: 12.4 GB (+500.0 MB)
|
|
232
|
+
* Threshold: 5.0 GB
|
|
233
|
+
*
|
|
234
|
+
* ⚠️ Cloned repository exceeds 5.0 GB
|
|
235
|
+
* ⚠️ Total disk usage exceeds 5.0 GB
|
|
236
|
+
* ```
|
|
237
|
+
*
|
|
238
|
+
* @param {{afterClone: object|null, afterAgent: object|null}} parsed
|
|
239
|
+
* @param {Object} [options]
|
|
240
|
+
* @param {number} [options.threshold=WARNING_THRESHOLD_BYTES]
|
|
241
|
+
* @param {string} [options.title='💾 Disk usage']
|
|
242
|
+
* @returns {string}
|
|
243
|
+
*/
|
|
244
|
+
export function formatDiskDiagnosticsBlock(parsed, options = {}) {
|
|
245
|
+
if (!parsed || (!parsed.afterClone && !parsed.afterAgent)) return '';
|
|
246
|
+
const threshold = Number.isFinite(options.threshold) ? options.threshold : WARNING_THRESHOLD_BYTES;
|
|
247
|
+
const title = options.title || '💾 Disk usage';
|
|
248
|
+
const warnings = computeDiskWarnings(parsed, threshold);
|
|
249
|
+
const lines = [];
|
|
250
|
+
const cloneBytes = parsed.afterClone?.bytes ?? null;
|
|
251
|
+
const totalBytes = parsed.afterAgent?.bytes ?? null;
|
|
252
|
+
const deltaBytes = parsed.afterAgent?.deltaBytes ?? null;
|
|
253
|
+
if (cloneBytes !== null) {
|
|
254
|
+
lines.push(`Cloned repository: ${formatBytes(cloneBytes)}`);
|
|
255
|
+
}
|
|
256
|
+
if (totalBytes !== null) {
|
|
257
|
+
const deltaStr = deltaBytes !== null ? ` (${formatBytesDelta(deltaBytes)})` : '';
|
|
258
|
+
lines.push(`After agent: ${formatBytes(totalBytes)}${deltaStr}`);
|
|
259
|
+
} else if (deltaBytes !== null) {
|
|
260
|
+
lines.push(`Delta during run: ${formatBytesDelta(deltaBytes)}`);
|
|
261
|
+
}
|
|
262
|
+
lines.push(`Threshold: ${formatBytes(threshold)}`);
|
|
263
|
+
const warningLines = [];
|
|
264
|
+
if (warnings.cloneTooLarge) warningLines.push(`⚠️ Cloned repository exceeds ${formatBytes(threshold)}`);
|
|
265
|
+
if (warnings.deltaTooLarge) warningLines.push(`⚠️ Folder grew by more than ${formatBytes(threshold)} during the run`);
|
|
266
|
+
if (warnings.totalTooLarge) warningLines.push(`⚠️ Total disk usage exceeds ${formatBytes(threshold)}`);
|
|
267
|
+
if (warningLines.length) {
|
|
268
|
+
lines.push('');
|
|
269
|
+
lines.push(...warningLines);
|
|
270
|
+
}
|
|
271
|
+
return `${title}\n\`\`\`\n${lines.join('\n')}\n\`\`\``;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Capture the AFTER_CLONE checkpoint and log it. Safe to call when `log`
|
|
276
|
+
* is missing; degrades to console.log so a CLI-only run still shows the size.
|
|
277
|
+
*
|
|
278
|
+
* Returns the captured size in bytes so the caller can stash it for the
|
|
279
|
+
* AFTER_AGENT delta calculation, or null if measurement failed.
|
|
280
|
+
*
|
|
281
|
+
* @param {Object} params
|
|
282
|
+
* @param {string} params.tempDir
|
|
283
|
+
* @param {Function} [params.log] - The bound `log` from solve.mjs
|
|
284
|
+
* @returns {Promise<number|null>}
|
|
285
|
+
*/
|
|
286
|
+
export async function recordAfterCloneSize({ tempDir, log }) {
|
|
287
|
+
const bytes = measureDirectorySize(tempDir);
|
|
288
|
+
const marker = buildDiskMarker({
|
|
289
|
+
phase: DISK_PHASE_AFTER_CLONE,
|
|
290
|
+
bytes,
|
|
291
|
+
path: tempDir,
|
|
292
|
+
});
|
|
293
|
+
if (log) {
|
|
294
|
+
await log(`\n${marker}`);
|
|
295
|
+
} else {
|
|
296
|
+
console.log(marker);
|
|
297
|
+
}
|
|
298
|
+
return bytes;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Capture the AFTER_AGENT checkpoint and log it (with delta versus the
|
|
303
|
+
* AFTER_CLONE checkpoint when available). Returns the captured size in bytes.
|
|
304
|
+
*
|
|
305
|
+
* @param {Object} params
|
|
306
|
+
* @param {string} params.tempDir
|
|
307
|
+
* @param {number|null} params.beforeBytes - The AFTER_CLONE size captured earlier
|
|
308
|
+
* @param {Function} [params.log]
|
|
309
|
+
* @returns {Promise<number|null>}
|
|
310
|
+
*/
|
|
311
|
+
export async function recordAfterAgentSize({ tempDir, beforeBytes, log }) {
|
|
312
|
+
const bytes = measureDirectorySize(tempDir);
|
|
313
|
+
const deltaBytes = Number.isFinite(bytes) && Number.isFinite(beforeBytes) ? bytes - beforeBytes : null;
|
|
314
|
+
const marker = buildDiskMarker({
|
|
315
|
+
phase: DISK_PHASE_AFTER_AGENT,
|
|
316
|
+
bytes,
|
|
317
|
+
deltaBytes,
|
|
318
|
+
path: tempDir,
|
|
319
|
+
});
|
|
320
|
+
if (log) {
|
|
321
|
+
await log(`\n${marker}`);
|
|
322
|
+
} else {
|
|
323
|
+
console.log(marker);
|
|
324
|
+
}
|
|
325
|
+
return bytes;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export default {
|
|
329
|
+
WARNING_THRESHOLD_BYTES,
|
|
330
|
+
DISK_MARKER_PREFIX,
|
|
331
|
+
DISK_PHASE_AFTER_CLONE,
|
|
332
|
+
DISK_PHASE_AFTER_AGENT,
|
|
333
|
+
measureDirectorySize,
|
|
334
|
+
formatBytes,
|
|
335
|
+
formatBytesDelta,
|
|
336
|
+
buildDiskMarker,
|
|
337
|
+
parseDiskMarkers,
|
|
338
|
+
computeDiskWarnings,
|
|
339
|
+
formatDiskDiagnosticsBlock,
|
|
340
|
+
recordAfterCloneSize,
|
|
341
|
+
recordAfterAgentSize,
|
|
342
|
+
};
|
package/src/solve.mjs
CHANGED
|
@@ -55,6 +55,7 @@ const { configureWorkingSession, beginWorkingSession, endWorkingSession } = awai
|
|
|
55
55
|
const getResourceSnapshot = memoryCheck.getResourceSnapshot;
|
|
56
56
|
const { handleAutoPrCreation } = await import('./solve.auto-pr.lib.mjs');
|
|
57
57
|
const { setupRepositoryAndClone, verifyDefaultBranchAndStatus } = await import('./solve.repo-setup.lib.mjs');
|
|
58
|
+
const { recordAfterCloneSize, recordAfterAgentSize } = await import('./solve.disk-diagnostics.lib.mjs');
|
|
58
59
|
const { createOrCheckoutBranch } = await import('./solve.branch.lib.mjs');
|
|
59
60
|
const { startWorkSession, endWorkSession, SESSION_TYPES } = await import('./solve.session.lib.mjs');
|
|
60
61
|
// Issue #1625: centralized markers + tracked comment posting for solve.mjs's
|
|
@@ -501,6 +502,8 @@ try {
|
|
|
501
502
|
needsClone,
|
|
502
503
|
});
|
|
503
504
|
|
|
505
|
+
cleanupContext.diskDiagnostics = { beforeBytes: await recordAfterCloneSize({ tempDir, log }) };
|
|
506
|
+
|
|
504
507
|
// Verify default branch and status using the new module
|
|
505
508
|
// Pass argv, owner, repo, issueUrl for empty repository auto-initialization (--auto-init-repository)
|
|
506
509
|
const defaultBranch = await verifyDefaultBranchAndStatus({
|
|
@@ -814,6 +817,12 @@ try {
|
|
|
814
817
|
toolResult = claudeResult;
|
|
815
818
|
}
|
|
816
819
|
|
|
820
|
+
try {
|
|
821
|
+
await recordAfterAgentSize({ tempDir, beforeBytes: cleanupContext.diskDiagnostics?.beforeBytes ?? null, log });
|
|
822
|
+
} catch (diskError) {
|
|
823
|
+
await log(`⚠️ Disk-size measurement failed: ${cleanErrorMessage(diskError)}`, { level: 'warning', verbose: true });
|
|
824
|
+
}
|
|
825
|
+
|
|
817
826
|
// Issue #1823: Mark the end of the AI working session. If a graceful-shutdown interrupt arrived
|
|
818
827
|
// during the session (deferred by the working-session guard), honor it now: auto-commit any
|
|
819
828
|
// uncommitted changes and exit gracefully — only AFTER the AI tool has fully finished its turn.
|