@link-assistant/hive-mind 2.12.5 → 2.13.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.
- package/CHANGELOG.md +12 -0
- package/package.json +1 -1
- package/src/claude.budget-stats.lib.mjs +4 -2
- package/src/claude.lib.mjs +83 -17
- package/src/claude.stream-events.lib.mjs +56 -2
- package/src/disk-guard.lib.mjs +256 -0
- package/src/github.batch.lib.mjs +31 -11
- package/src/github.lib.mjs +3 -1
- package/src/hive.mjs +136 -11
- package/src/lib.mjs +23 -3
- package/src/limits-i18n.lib.mjs +8 -0
- package/src/list-solution-drafts.lib.mjs +17 -4
- 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/session-log-rename.lib.mjs +65 -0
- package/src/session-monitor.lib.mjs +45 -1
- package/src/solve.mjs +48 -4
- package/src/solve.repository.lib.mjs +5 -1
- package/src/solve.restart-shared.lib.mjs +18 -9
- package/src/solve.results.lib.mjs +6 -3
- package/src/solve.validation.lib.mjs +7 -9
- package/src/subscription-block-telegram.lib.mjs +115 -0
- package/src/subscription-error.lib.mjs +328 -0
- package/src/tool-retry.lib.mjs +29 -0
package/src/github.batch.lib.mjs
CHANGED
|
@@ -17,19 +17,27 @@ import { wrapDollarWithGhRetry as _wrapDollarWithGhRetry, execGhWithRetry } from
|
|
|
17
17
|
export { prClosesIssue };
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
|
-
* Extract
|
|
20
|
+
* Extract pull requests that are linked to an issue with closing keywords.
|
|
21
21
|
* Draft pull requests are still open in-progress solution drafts, so they must
|
|
22
22
|
* count for /hive --skip-issues-with-prs.
|
|
23
|
+
*
|
|
24
|
+
* Issue #2160: reporting needs the opposite default from gating. `--skip-issues-with-prs` only
|
|
25
|
+
* cares about OPEN pull requests, but the end-of-run summary must also see the ones `--auto-merge`
|
|
26
|
+
* already merged, otherwise a merged solution draft is reported as "(no PR found)".
|
|
27
|
+
*
|
|
23
28
|
* @param {Object} issueData - GraphQL issue node with timelineItems
|
|
24
29
|
* @param {number} issueNum - Issue number to check
|
|
25
30
|
* @param {Function} logger - Async logger, defaults to shared log helper
|
|
26
|
-
* @
|
|
31
|
+
* @param {Object} [options]
|
|
32
|
+
* @param {Array<string>} [options.includeStates=['OPEN']] - PR states to report
|
|
33
|
+
* @returns {Promise<Array<Object>>} Linked PRs (in the requested states) that close the issue
|
|
27
34
|
*/
|
|
28
|
-
export async function extractLinkedPullRequestsForIssue(issueData, issueNum, logger = log) {
|
|
35
|
+
export async function extractLinkedPullRequestsForIssue(issueData, issueNum, logger = log, { includeStates = ['OPEN'] } = {}) {
|
|
29
36
|
const linkedPRs = [];
|
|
37
|
+
const wantedStates = new Set(includeStates);
|
|
30
38
|
|
|
31
39
|
for (const item of issueData.timelineItems?.nodes || []) {
|
|
32
|
-
if (item?.source && item.source.state
|
|
40
|
+
if (item?.source && wantedStates.has(item.source.state)) {
|
|
33
41
|
// Check if PR actually closes this issue (has "fixes #N", "closes #N", or "resolves #N")
|
|
34
42
|
const prBody = item.source.body || '';
|
|
35
43
|
const prTitle = item.source.title || '';
|
|
@@ -58,9 +66,12 @@ export async function extractLinkedPullRequestsForIssue(issueData, issueNum, log
|
|
|
58
66
|
* @param {string} owner - Repository owner
|
|
59
67
|
* @param {string} repo - Repository name
|
|
60
68
|
* @param {Array<number>} issueNumbers - Array of issue numbers to check
|
|
69
|
+
* @param {Object} [options]
|
|
70
|
+
* @param {Array<string>} [options.includeStates=['OPEN']] - PR states to report in `linkedPRs`
|
|
71
|
+
* (issue #2160). `openPRCount` always counts only OPEN pull requests.
|
|
61
72
|
* @returns {Promise<Object>} Object mapping issue numbers to their linked PRs
|
|
62
73
|
*/
|
|
63
|
-
export async function batchCheckPullRequestsForIssues(owner, repo, issueNumbers) {
|
|
74
|
+
export async function batchCheckPullRequestsForIssues(owner, repo, issueNumbers, { includeStates = ['OPEN'] } = {}) {
|
|
64
75
|
try {
|
|
65
76
|
if (!issueNumbers || issueNumbers.length === 0) {
|
|
66
77
|
return {};
|
|
@@ -138,12 +149,14 @@ export async function batchCheckPullRequestsForIssues(owner, repo, issueNumbers)
|
|
|
138
149
|
// Issue #1094: Only count PRs that explicitly fix/close/resolve this issue
|
|
139
150
|
// This prevents false positives from PRs that only mention issues without solving them
|
|
140
151
|
// Issue #1760: Draft PRs are still active solution drafts and must block duplicate work
|
|
141
|
-
const linkedPRs = await extractLinkedPullRequestsForIssue(issueData, issueNum);
|
|
152
|
+
const linkedPRs = await extractLinkedPullRequestsForIssue(issueData, issueNum, log, { includeStates });
|
|
142
153
|
|
|
143
154
|
results[issueNum] = {
|
|
144
155
|
title: issueData.title,
|
|
145
156
|
state: issueData.state,
|
|
146
|
-
|
|
157
|
+
// Issue #2160: linkedPRs may now include merged/closed PRs for reporting, so the
|
|
158
|
+
// gate count has to be derived from the open ones only.
|
|
159
|
+
openPRCount: linkedPRs.filter(pr => pr.state === 'OPEN').length,
|
|
147
160
|
linkedPRs: linkedPRs,
|
|
148
161
|
};
|
|
149
162
|
} else {
|
|
@@ -165,18 +178,25 @@ export async function batchCheckPullRequestsForIssues(owner, repo, issueNumbers)
|
|
|
165
178
|
|
|
166
179
|
for (const issueNum of batch) {
|
|
167
180
|
try {
|
|
168
|
-
|
|
181
|
+
// Issue #2160: return the PRs themselves, not just a count, so the end-of-run summary
|
|
182
|
+
// can name a merged solution draft even when GraphQL was unavailable.
|
|
183
|
+
const cmd = `gh api repos/${owner}/${repo}/issues/${issueNum}/timeline --paginate --jq '[.[] | select(.event == "cross-referenced" and .source.issue.pull_request != null) | {number: .source.issue.number, title: .source.issue.title, body: .source.issue.body, state: (if .source.issue.pull_request.merged_at then "MERGED" else (.source.issue.state | ascii_upcase) end), isDraft: (.source.issue.draft // false), url: .source.issue.html_url}]'`;
|
|
169
184
|
|
|
170
185
|
// #1756: route REST fallback through execGhWithRetry for transient 5xx + rate-limit
|
|
171
186
|
const { stdout } = await execGhWithRetry(cmd, {
|
|
172
187
|
execOptions: { encoding: 'utf8', env: process.env },
|
|
173
188
|
label: `gh api timeline (issue #${issueNum})`,
|
|
174
189
|
});
|
|
175
|
-
const
|
|
190
|
+
const wantedStates = new Set(includeStates);
|
|
191
|
+
const crossReferenced = JSON.parse(stdout.trim() || '[]');
|
|
192
|
+
const linkedPRs = crossReferenced
|
|
193
|
+
.filter(pr => wantedStates.has(pr.state))
|
|
194
|
+
.filter(pr => prClosesIssue(pr.body || '', issueNum) || prClosesIssue(pr.title || '', issueNum))
|
|
195
|
+
.map(({ number, title, state, isDraft, url }) => ({ number, title, state, isDraft: Boolean(isDraft), url }));
|
|
176
196
|
|
|
177
197
|
results[issueNum] = {
|
|
178
|
-
openPRCount:
|
|
179
|
-
linkedPRs
|
|
198
|
+
openPRCount: linkedPRs.filter(pr => pr.state === 'OPEN').length,
|
|
199
|
+
linkedPRs,
|
|
180
200
|
};
|
|
181
201
|
} catch (restError) {
|
|
182
202
|
results[issueNum] = {
|
package/src/github.lib.mjs
CHANGED
|
@@ -579,7 +579,9 @@ ${logContent}
|
|
|
579
579
|
if (useLargeFileMode) {
|
|
580
580
|
await log(` 📁 Log file too large for inline comment (${Math.round(logStats.size / 1024 / 1024)}MB), using gh-upload-log`);
|
|
581
581
|
} else {
|
|
582
|
-
|
|
582
|
+
// Issue #2160: this is the expected route for a long log, not a problem — the upload
|
|
583
|
+
// below handles it. Reporting it as a warning made every normal run look degraded.
|
|
584
|
+
await log(` ℹ️ Log comment too long (${logComment.length} chars, GitHub limit is ${githubLimits.commentMaxSize} chars), using gh-upload-log`);
|
|
583
585
|
}
|
|
584
586
|
await log(' 📎 Uploading log using gh-upload-log...');
|
|
585
587
|
try {
|
package/src/hive.mjs
CHANGED
|
@@ -36,8 +36,11 @@ if (earlyArgs.includes('--help') || earlyArgs.includes('-h')) {
|
|
|
36
36
|
}
|
|
37
37
|
export { createYargsConfig } from './hive.config.lib.mjs';
|
|
38
38
|
import { attachChildExitHandlers } from './child-exit.lib.mjs';
|
|
39
|
+
import { SUBSCRIPTION_BLOCKED_MARKER } from './subscription-error.lib.mjs'; // Issue #2161
|
|
39
40
|
import { isDirectExecution, withTimeout } from './hive.bootstrap.lib.mjs';
|
|
40
41
|
import { createShutdownManager } from './hive.shutdown.lib.mjs';
|
|
42
|
+
// Issue #2160: keep dequeuing safe when the host disk fills up mid-run.
|
|
43
|
+
import { EXIT_CODE_INSUFFICIENT_DISK_SPACE, ensureDiskSpaceForWorker, extractSolverWorkspacePaths } from './disk-guard.lib.mjs';
|
|
41
44
|
const isRunningDirectly = isDirectExecution(process.argv[1], import.meta.url);
|
|
42
45
|
if (isRunningDirectly) {
|
|
43
46
|
console.log('🐝 Hive Mind - AI-powered issue solver');
|
|
@@ -602,6 +605,7 @@ if (isRunningDirectly) {
|
|
|
602
605
|
this.processing = new Set();
|
|
603
606
|
this.completed = new Set();
|
|
604
607
|
this.failed = new Set();
|
|
608
|
+
this.deferrals = new Map(); // Issue #2160: issueUrl -> environment deferral count
|
|
605
609
|
this.workers = [];
|
|
606
610
|
this.isRunning = true;
|
|
607
611
|
}
|
|
@@ -632,6 +636,18 @@ if (isRunningDirectly) {
|
|
|
632
636
|
this.processing.delete(issueUrl);
|
|
633
637
|
this.failed.add(issueUrl);
|
|
634
638
|
}
|
|
639
|
+
// Issue #2160: put an issue back at the head of the queue after an *environment* block (a
|
|
640
|
+
// full host disk). It is neither completed nor failed — the task was never attempted.
|
|
641
|
+
// Returns how many times this issue has been deferred so the caller can stop looping.
|
|
642
|
+
requeue(issueUrl) {
|
|
643
|
+
this.processing.delete(issueUrl);
|
|
644
|
+
const deferrals = (this.deferrals.get(issueUrl) || 0) + 1;
|
|
645
|
+
this.deferrals.set(issueUrl, deferrals);
|
|
646
|
+
if (!this.completed.has(issueUrl) && !this.queue.includes(issueUrl)) {
|
|
647
|
+
this.queue.unshift(issueUrl);
|
|
648
|
+
}
|
|
649
|
+
return deferrals;
|
|
650
|
+
}
|
|
635
651
|
// Get queue statistics
|
|
636
652
|
getStats() {
|
|
637
653
|
return {
|
|
@@ -653,6 +669,32 @@ if (isRunningDirectly) {
|
|
|
653
669
|
// controlled SIGTERM to each (they run in their own detached process group, so the
|
|
654
670
|
// terminal's SIGINT never reaches them); a *second* interrupt force-kills the groups.
|
|
655
671
|
const activeSolveChildren = new Set();
|
|
672
|
+
// Issue #2160: workspaces owned by in-flight workers, learned from the solver's own output.
|
|
673
|
+
// The disk guard must never reclaim these — they hold work in progress.
|
|
674
|
+
const workerWorkspaces = new Map(); // workerId -> Set<workspace path>
|
|
675
|
+
const getProtectedWorkspacePaths = () => new Set(Array.from(workerWorkspaces.values()).flatMap(paths => Array.from(paths)));
|
|
676
|
+
// How long a worker waits for in-flight work to release disk space before deferring its task,
|
|
677
|
+
// and how many deferrals of one task are tolerated before hive stops: with nothing else
|
|
678
|
+
// running, no amount of waiting will free space.
|
|
679
|
+
const DISK_SPACE_WAIT_MS = 10 * 60 * 1000;
|
|
680
|
+
const MAX_DISK_SPACE_DEFERRALS = 3;
|
|
681
|
+
let diskSpaceHalt = null;
|
|
682
|
+
// Issue #2161: an account/subscription block is hive-wide, not per-issue. The
|
|
683
|
+
// credentials every worker shares have been refused, so each remaining issue
|
|
684
|
+
// would spin up a full solve run only to die the same way — burning clones,
|
|
685
|
+
// containers and PR comments while the queue drains into "failed". The first
|
|
686
|
+
// worker to see the marker in its child's output records it here and stops the
|
|
687
|
+
// queue; the rest exit as soon as their current child returns.
|
|
688
|
+
let subscriptionBlock = null;
|
|
689
|
+
const noteSubscriptionBlock = (workerId, line) => {
|
|
690
|
+
if (subscriptionBlock) return;
|
|
691
|
+
subscriptionBlock = { workerId, line: line.trim() };
|
|
692
|
+
log(`\n${SUBSCRIPTION_BLOCKED_MARKER} — worker ${workerId} reported that the account can no longer use the tool:`, { level: 'error' }).catch(() => {});
|
|
693
|
+
log(` ${subscriptionBlock.line}`, { level: 'error' }).catch(() => {});
|
|
694
|
+
log(' Stopping the hive: every remaining issue would fail the same way until access is restored.', { level: 'error' }).catch(() => {});
|
|
695
|
+
log(' In-flight workers finish (and auto-commit their work) before the run ends.', { level: 'error' }).catch(() => {});
|
|
696
|
+
issueQueue.stop();
|
|
697
|
+
};
|
|
656
698
|
// Worker function to process issues from queue
|
|
657
699
|
async function worker(workerId) {
|
|
658
700
|
await log(`🔧 Worker ${workerId} started`, { verbose: true });
|
|
@@ -673,8 +715,35 @@ if (isRunningDirectly) {
|
|
|
673
715
|
await log(` 📊 Queue: ${stats.queued} waiting, ${stats.processing} processing, ${stats.completed} completed, ${stats.failed} failed`);
|
|
674
716
|
continue;
|
|
675
717
|
}
|
|
718
|
+
// Issue #2160: re-check free disk space before every task. hive used to check it once at
|
|
719
|
+
// startup, so a run whose kept workspaces filled the disk kept spawning solvers that died
|
|
720
|
+
// in their own pre-flight check — and each of those was counted as a *task* failure
|
|
721
|
+
// (`❌ 4 task(s) failed (completed: 6)`). Reclaim idle workspaces, wait for in-flight ones,
|
|
722
|
+
// and defer the task rather than burn it.
|
|
723
|
+
if (!argv.dryRun) {
|
|
724
|
+
const requiredDiskSpaceMB = argv.minDiskSpace || 10240;
|
|
725
|
+
const otherWorkInFlight = issueQueue.getStats().processing > 1;
|
|
726
|
+
const diskGuard = await ensureDiskSpaceForWorker({
|
|
727
|
+
requiredMB: requiredDiskSpaceMB,
|
|
728
|
+
protectedPaths: getProtectedWorkspacePaths(),
|
|
729
|
+
maxWaitMs: otherWorkInFlight ? DISK_SPACE_WAIT_MS : 0,
|
|
730
|
+
log,
|
|
731
|
+
});
|
|
732
|
+
if (!diskGuard.ok) {
|
|
733
|
+
const deferrals = issueQueue.requeue(issueUrl);
|
|
734
|
+
await log(` ⏸️ Worker ${workerId} deferred ${issueUrl}: ${diskGuard.freeMB}MB free, ${requiredDiskSpaceMB}MB required (deferral ${deferrals}/${MAX_DISK_SPACE_DEFERRALS}, not a task failure)`, { level: 'warning' });
|
|
735
|
+
if (deferrals >= MAX_DISK_SPACE_DEFERRALS && issueQueue.getStats().processing === 0) {
|
|
736
|
+
diskSpaceHalt = `Insufficient disk space: ${diskGuard.freeMB}MB free, ${requiredDiskSpaceMB}MB required`;
|
|
737
|
+
await log(' 🛑 Stopping: no in-flight work can release disk space. Free space on this host (or enable --auto-cleanup) and rerun.', { level: 'error' });
|
|
738
|
+
issueQueue.stop();
|
|
739
|
+
}
|
|
740
|
+
continue;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
676
743
|
// Track if this issue failed
|
|
677
744
|
let issueFailed = false;
|
|
745
|
+
// Issue #2160: an environment block (full disk) reported by solve itself — requeue, don't fail.
|
|
746
|
+
let environmentDeferral = false;
|
|
678
747
|
// Issue #1823: Track a graceful shutdown stop so it is neither failed nor completed.
|
|
679
748
|
let gracefulStop = false;
|
|
680
749
|
// Process the issue multiple times if needed
|
|
@@ -752,12 +821,20 @@ if (isRunningDirectly) {
|
|
|
752
821
|
});
|
|
753
822
|
// Issue #1823: register the in-flight child for optional force-kill on a 2nd signal
|
|
754
823
|
activeSolveChildren.add(child);
|
|
824
|
+
// Issue #2160: start collecting the workspaces this worker owns so the disk guard
|
|
825
|
+
// (running in the other workers) never reclaims a directory that is still in use.
|
|
826
|
+
const ownedWorkspaces = new Set();
|
|
827
|
+
workerWorkspaces.set(workerId, ownedWorkspaces);
|
|
755
828
|
log(` 🧒 Spawned ${solveCommand} worker-${workerId} (pid ${child.pid}, detached process group)`, { verbose: true }).catch(() => {});
|
|
756
829
|
// Handle stdout data - stream output in real-time
|
|
757
830
|
child.stdout.on('data', data => {
|
|
758
831
|
const lines = data.toString().split('\n');
|
|
759
832
|
for (const line of lines) {
|
|
760
833
|
if (line.trim()) {
|
|
834
|
+
for (const workspacePath of extractSolverWorkspacePaths(line)) ownedWorkspaces.add(workspacePath);
|
|
835
|
+
// Issue #2161: solve prints SUBSCRIPTION_BLOCKED_MARKER on a terminal
|
|
836
|
+
// account block. Seen here, it stops the whole hive (see noteSubscriptionBlock).
|
|
837
|
+
if (line.includes(SUBSCRIPTION_BLOCKED_MARKER)) noteSubscriptionBlock(workerId, line);
|
|
761
838
|
log(` [${solveCommand} worker-${workerId}] ${line}`).catch(logError => {
|
|
762
839
|
reportError(logError, {
|
|
763
840
|
context: 'worker_stdout_log',
|
|
@@ -777,6 +854,7 @@ if (isRunningDirectly) {
|
|
|
777
854
|
const lines = data.toString().split('\n');
|
|
778
855
|
for (const line of lines) {
|
|
779
856
|
if (line.trim()) {
|
|
857
|
+
if (line.includes(SUBSCRIPTION_BLOCKED_MARKER)) noteSubscriptionBlock(workerId, line); // Issue #2161
|
|
780
858
|
log(` [${solveCommand} worker-${workerId} stderr] ${line}`).catch(logError => {
|
|
781
859
|
reportError(logError, {
|
|
782
860
|
context: 'worker_stderr_log',
|
|
@@ -798,6 +876,8 @@ if (isRunningDirectly) {
|
|
|
798
876
|
onLogError: (logError, operation) => reportError(logError, { context: 'worker_child_exit_log', workerId, operation }),
|
|
799
877
|
onExit: result => {
|
|
800
878
|
activeSolveChildren.delete(child); // Issue #1823: no longer in-flight
|
|
879
|
+
// Issue #2160: the worker released its workspaces — they become reclaimable.
|
|
880
|
+
workerWorkspaces.delete(workerId);
|
|
801
881
|
exitCode = result.exitCode;
|
|
802
882
|
resolve();
|
|
803
883
|
},
|
|
@@ -813,6 +893,20 @@ if (isRunningDirectly) {
|
|
|
813
893
|
await log(` 🛑 Worker ${workerId} stopped gracefully during shutdown on ${issueUrl} (exit ${exitCode}, ${duration}s)`);
|
|
814
894
|
gracefulStop = true;
|
|
815
895
|
break; // stop processing more PRs for this issue
|
|
896
|
+
} else if (exitCode === EXIT_CODE_INSUFFICIENT_DISK_SPACE) {
|
|
897
|
+
// Issue #2160: solve refused to start because this host is out of disk space. The
|
|
898
|
+
// issue was never attempted, so it must not be counted as a task failure.
|
|
899
|
+
await log(` ⏸️ Worker ${workerId} could not start ${issueUrl}: the host is out of disk space (exit ${exitCode}, ${duration}s) — requeued, not a task failure`, { level: 'warning' });
|
|
900
|
+
environmentDeferral = true;
|
|
901
|
+
break;
|
|
902
|
+
} else if (subscriptionBlock) {
|
|
903
|
+
// Issue #2161: the run did not fail because of this issue — the account
|
|
904
|
+
// lost access mid-flight. Report the real reason and stop; solve has
|
|
905
|
+
// already auto-committed whatever work existed.
|
|
906
|
+
await log(` ${SUBSCRIPTION_BLOCKED_MARKER} Worker ${workerId} stopped on ${issueUrl} after ${duration}s: the tool account can no longer be used (exit ${exitCode}).`, { level: 'error' });
|
|
907
|
+
await log(` Restore access, then re-run the hive — this issue stays queued, not failed.`, { level: 'error' });
|
|
908
|
+
gracefulStop = true;
|
|
909
|
+
break;
|
|
816
910
|
} else {
|
|
817
911
|
throw new Error(`${solveCommand} exited with code ${exitCode}`);
|
|
818
912
|
}
|
|
@@ -838,7 +932,16 @@ if (isRunningDirectly) {
|
|
|
838
932
|
// Only mark as completed if it didn't fail and wasn't gracefully stopped mid-shutdown.
|
|
839
933
|
// Issue #1823: a graceful stop is neither a success nor a failure — leave it in
|
|
840
934
|
// "processing" so it is not miscounted as completed (which would also trigger cleanup).
|
|
841
|
-
if (
|
|
935
|
+
if (environmentDeferral) {
|
|
936
|
+
// Issue #2160: back to the queue (neither completed nor failed). Stop the run when
|
|
937
|
+
// nothing else is in flight, since no other worker can release disk space.
|
|
938
|
+
const deferrals = issueQueue.requeue(issueUrl);
|
|
939
|
+
if (deferrals >= MAX_DISK_SPACE_DEFERRALS && issueQueue.getStats().processing === 0) {
|
|
940
|
+
diskSpaceHalt = 'Insufficient disk space reported by the solver pre-flight check';
|
|
941
|
+
await log(' 🛑 Stopping: no in-flight work can release disk space. Free space on this host (or enable --auto-cleanup) and rerun.', { level: 'error' });
|
|
942
|
+
issueQueue.stop();
|
|
943
|
+
}
|
|
944
|
+
} else if (!issueFailed && !gracefulStop) {
|
|
842
945
|
issueQueue.markCompleted(issueUrl);
|
|
843
946
|
}
|
|
844
947
|
// Show queue stats
|
|
@@ -1256,10 +1359,22 @@ if (isRunningDirectly) {
|
|
|
1256
1359
|
// Perform cleanup if enabled and there were successful completions
|
|
1257
1360
|
const finalStats = issueQueue.getStats();
|
|
1258
1361
|
if (finalStats.completed > 0) {
|
|
1259
|
-
|
|
1362
|
+
// Issue #2160: argv must be forwarded — cleanupTempDirectories returns immediately
|
|
1363
|
+
// without it, so this branch used to be a silent no-op even with --auto-cleanup.
|
|
1364
|
+
await cleanupTempDirectories(argv);
|
|
1260
1365
|
}
|
|
1261
1366
|
await log('\n👋 Hive Mind monitoring stopped');
|
|
1262
1367
|
await log(` 📁 Full log file: ${absoluteLogPath}`);
|
|
1368
|
+
// Issue #2161: the hive did not simply "finish" — it was cut short because the
|
|
1369
|
+
// account lost access. Say so last (that is what a human scrolls to) and exit
|
|
1370
|
+
// non-zero so supervisors and the Telegram monitor report a failure, not a
|
|
1371
|
+
// clean completion.
|
|
1372
|
+
if (subscriptionBlock) {
|
|
1373
|
+
await log(`\n${SUBSCRIPTION_BLOCKED_MARKER} Hive stopped early: the tool account can no longer be used.`, { level: 'error' });
|
|
1374
|
+
await log(` Reported by worker ${subscriptionBlock.workerId}: ${subscriptionBlock.line}`, { level: 'error' });
|
|
1375
|
+
await log(' Restore subscription/account access, then start the hive again.', { level: 'error' });
|
|
1376
|
+
await safeExit(1, 'Subscription/account access blocked');
|
|
1377
|
+
}
|
|
1263
1378
|
}
|
|
1264
1379
|
// Issue #1823: Graceful-shutdown + force-kill logic lives in hive.shutdown.lib.mjs.
|
|
1265
1380
|
// gracefulShutdown waits (uncapped) for in-flight solve workers to finish on the first
|
|
@@ -1292,16 +1407,20 @@ if (isRunningDirectly) {
|
|
|
1292
1407
|
verbose: true,
|
|
1293
1408
|
});
|
|
1294
1409
|
} else {
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1410
|
+
// Issue #2160: reclaim idle solver workspaces left behind by earlier runs before refusing to
|
|
1411
|
+
// start, and report an exhausted disk as the environment condition it is (exit 75) instead of
|
|
1412
|
+
// a generic error. `exitOnFailure` is deliberately not used: it calls process.exit(1)
|
|
1413
|
+
// directly, which skips the log-flushing safeExit path and printed no actionable reason.
|
|
1414
|
+
const startupRequiredDiskSpaceMB = argv.minDiskSpace || 10240;
|
|
1415
|
+
const startupDiskGuard = await ensureDiskSpaceForWorker({ requiredMB: startupRequiredDiskSpaceMB, log });
|
|
1416
|
+
if (!startupDiskGuard.ok) {
|
|
1417
|
+
await log(`❌ Insufficient disk space to start: ${startupDiskGuard.freeMB}MB available, ${startupRequiredDiskSpaceMB}MB required`, { level: 'error' });
|
|
1418
|
+
await log(' Free space on this host, or run with --auto-cleanup so workspaces are removed after each task.', { level: 'error' });
|
|
1419
|
+
await safeExit(EXIT_CODE_INSUFFICIENT_DISK_SPACE, `Insufficient disk space (${startupDiskGuard.freeMB}MB available, ${startupRequiredDiskSpaceMB}MB required)`);
|
|
1420
|
+
}
|
|
1421
|
+
const systemCheck = await checkSystem({ minDiskSpaceMB: startupRequiredDiskSpaceMB, minMemoryMB: 256 }, { log });
|
|
1303
1422
|
if (!systemCheck.success) {
|
|
1304
|
-
await safeExit(1, '
|
|
1423
|
+
await safeExit(1, 'System resource check failed');
|
|
1305
1424
|
}
|
|
1306
1425
|
// Validate the selected AI tool connection before starting monitoring with the same model that will be used
|
|
1307
1426
|
const isToolConnected = await validateToolConnection({ tool: argv.tool, model: argv.model, verbose: argv.verbose, validateClaudeConnection });
|
|
@@ -1326,6 +1445,12 @@ if (isRunningDirectly) {
|
|
|
1326
1445
|
}
|
|
1327
1446
|
const finalStats = issueQueue.getStats(); // Issue #1718: surface worker failures via exit code
|
|
1328
1447
|
if (finalStats.failed > 0) await safeExit(1, `${finalStats.failed} task(s) failed (completed: ${finalStats.completed})`);
|
|
1448
|
+
// Issue #2160: report an exhausted host disk as the environment problem it is, with its own
|
|
1449
|
+
// exit code, instead of letting it be counted as "N task(s) failed". Genuine task failures are
|
|
1450
|
+
// reported first above, because they say more about the run than the environment condition.
|
|
1451
|
+
if (diskSpaceHalt) {
|
|
1452
|
+
await safeExit(EXIT_CODE_INSUFFICIENT_DISK_SPACE, `${diskSpaceHalt} — ${finalStats.completed} task(s) completed, ${finalStats.queued} left queued (no task failures)`);
|
|
1453
|
+
}
|
|
1329
1454
|
} catch (fatalError) {
|
|
1330
1455
|
// Handle fatal errors during initialization or execution
|
|
1331
1456
|
console.error('\n❌ Fatal error occurred during hive initialization or execution');
|
package/src/lib.mjs
CHANGED
|
@@ -950,9 +950,16 @@ export const displayFormattedError = async options => {
|
|
|
950
950
|
};
|
|
951
951
|
|
|
952
952
|
/**
|
|
953
|
-
* Clean up temporary directories
|
|
953
|
+
* Clean up temporary directories.
|
|
954
|
+
*
|
|
955
|
+
* Issue #2160: this used to run `sudo rm -rf /tmp/* /var/tmp/*`, which also wipes the workspaces,
|
|
956
|
+
* lock directories and logs of any *concurrent* hive/solve process on the same host. The entries
|
|
957
|
+
* are now enumerated first and anything a live process is sitting in (or that the caller protects)
|
|
958
|
+
* is left alone.
|
|
959
|
+
*
|
|
954
960
|
* @param {Object} argv - Command line arguments
|
|
955
961
|
* @param {boolean} [argv.autoCleanup] - Whether auto-cleanup is enabled
|
|
962
|
+
* @param {Iterable<string>} [argv.protectedTempPaths] - Paths this run still needs
|
|
956
963
|
* @returns {Promise<void>}
|
|
957
964
|
*/
|
|
958
965
|
export const cleanupTempDirectories = async argv => {
|
|
@@ -962,13 +969,26 @@ export const cleanupTempDirectories = async argv => {
|
|
|
962
969
|
|
|
963
970
|
// Dynamic import for command-stream
|
|
964
971
|
const { $ } = await use('command-stream');
|
|
972
|
+
const { listCleanableTempEntries } = await import('./disk-guard.lib.mjs');
|
|
973
|
+
const path = await use('path');
|
|
965
974
|
|
|
966
975
|
try {
|
|
967
976
|
await log('\n🧹 Auto-cleanup enabled, removing temporary directories...');
|
|
968
|
-
|
|
977
|
+
const protectedPaths = new Set(argv.protectedTempPaths || []);
|
|
978
|
+
const currentLogFile = getLogFile();
|
|
979
|
+
if (currentLogFile) protectedPaths.add(path.resolve(currentLogFile));
|
|
980
|
+
const { remove, keep } = await listCleanableTempEntries({ protectedPaths });
|
|
981
|
+
for (const kept of keep) {
|
|
982
|
+
await log(` 🔒 Keeping ${kept.path} (${kept.reason === 'process_cwd' ? 'a live process is using it' : 'needed by this run'})`, { verbose: true });
|
|
983
|
+
}
|
|
984
|
+
if (remove.length === 0) {
|
|
985
|
+
await log(' ✅ Nothing to clean: every temporary entry is still in use');
|
|
986
|
+
return;
|
|
987
|
+
}
|
|
988
|
+
await log(` ⚠️ Executing: sudo rm -rf on ${remove.length} temporary entr${remove.length === 1 ? 'y' : 'ies'} (${keep.length} kept)`, { verbose: true });
|
|
969
989
|
|
|
970
990
|
// Execute cleanup command using command-stream
|
|
971
|
-
const cleanupCommand = $`sudo rm -rf
|
|
991
|
+
const cleanupCommand = $`sudo rm -rf ${remove}`;
|
|
972
992
|
|
|
973
993
|
let exitCode = 0;
|
|
974
994
|
for await (const chunk of cleanupCommand.stream()) {
|
package/src/limits-i18n.lib.mjs
CHANGED
|
@@ -85,6 +85,14 @@ const ENGLISH_LIMITS = {
|
|
|
85
85
|
subscription_detail_trial_ends: 'trial ends {{time}}',
|
|
86
86
|
subscription_detail_trial_ends_in: 'trial ends in {{duration}}; {{time}}',
|
|
87
87
|
subscription_status: 'Subscription: {{status}}',
|
|
88
|
+
subscription_blocked_title: 'Subscription/account access blocked',
|
|
89
|
+
subscription_blocked_provider: 'Provider said',
|
|
90
|
+
subscription_blocked_code: 'Error code',
|
|
91
|
+
subscription_blocked_reason: 'Why the run stopped',
|
|
92
|
+
subscription_blocked_note: 'This is not a usage limit: waiting, retrying or switching model cannot fix it.',
|
|
93
|
+
subscription_blocked_steps: 'What to do',
|
|
94
|
+
subscription_blocked_preserved: 'Uncommitted work was auto-committed before stopping.',
|
|
95
|
+
subscription_blocked_resume: 'Resume after access is restored',
|
|
88
96
|
telegram_api: 'Telegram Bot API',
|
|
89
97
|
telegram_flood_control: 'flood control',
|
|
90
98
|
telegram_last_rate_limit: 'Last 429: {{method}}',
|
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Solution Drafts Listing Module
|
|
3
3
|
* Displays completed issues with their linked pull requests
|
|
4
|
+
*
|
|
5
|
+
* Issue #2160: this listing used to ask only for OPEN pull requests, so every issue whose draft
|
|
6
|
+
* `--auto-merge` had already merged was reported as "(no PR found)" — a false negative in the
|
|
7
|
+
* summary a human reads to judge the run. Merged and closed drafts are now listed with their state.
|
|
4
8
|
*/
|
|
5
9
|
|
|
10
|
+
/** Pull request states worth reporting at the end of a run, in the order they are most useful. */
|
|
11
|
+
const REPORTED_PULL_REQUEST_STATES = ['OPEN', 'MERGED', 'CLOSED'];
|
|
12
|
+
|
|
6
13
|
/**
|
|
7
14
|
* Lists all completed issues with their solution drafts (PRs)
|
|
8
15
|
* @param {Object} issueQueue - The issue queue containing completed issues
|
|
@@ -10,10 +17,12 @@
|
|
|
10
17
|
* @param {Function} batchCheckPullRequestsForIssues - Function to batch check PRs for issues
|
|
11
18
|
*/
|
|
12
19
|
export async function listSolutionDrafts(issueQueue, log, batchCheckPullRequestsForIssues) {
|
|
13
|
-
|
|
20
|
+
// `completed` is a Set in hive.mjs, but callers/tests may pass an array.
|
|
21
|
+
const completedUrls = issueQueue?.completed ? Array.from(issueQueue.completed) : [];
|
|
22
|
+
if (completedUrls.length === 0) return;
|
|
14
23
|
await log('\n📋 Issues with solution drafts:');
|
|
15
24
|
const byRepo = {};
|
|
16
|
-
for (const url of
|
|
25
|
+
for (const url of completedUrls) {
|
|
17
26
|
const m = url.match(/github\.com\/([^/]+)\/([^/]+)\/issues\/(\d+)/);
|
|
18
27
|
if (m) (byRepo[`${m[1]}/${m[2]}`] ||= { owner: m[1], repo: m[2], iss: [] }).iss.push({ n: +m[3], url });
|
|
19
28
|
}
|
|
@@ -21,12 +30,16 @@ export async function listSolutionDrafts(issueQueue, log, batchCheckPullRequests
|
|
|
21
30
|
const prs = await batchCheckPullRequestsForIssues(
|
|
22
31
|
r.owner,
|
|
23
32
|
r.repo,
|
|
24
|
-
r.iss.map(i => i.n)
|
|
33
|
+
r.iss.map(i => i.n),
|
|
34
|
+
{ includeStates: REPORTED_PULL_REQUEST_STATES }
|
|
25
35
|
);
|
|
26
36
|
for (const i of r.iss)
|
|
27
37
|
if (prs[i.n]?.linkedPRs?.length) {
|
|
28
38
|
await log(` - ${i.url}`);
|
|
29
|
-
for (const p of prs[i.n].linkedPRs)
|
|
39
|
+
for (const p of prs[i.n].linkedPRs) {
|
|
40
|
+
const state = p.state && p.state !== 'OPEN' ? ` (${p.state.toLowerCase()})` : '';
|
|
41
|
+
await log(` → PR #${p.number}${state}: ${p.url}`);
|
|
42
|
+
}
|
|
30
43
|
} else await log(` - ${i.url} (no PR found)`);
|
|
31
44
|
}
|
|
32
45
|
}
|
package/src/locales/en.lino
CHANGED
|
@@ -322,6 +322,15 @@ en
|
|
|
322
322
|
session "session"
|
|
323
323
|
start "Start"
|
|
324
324
|
subscription
|
|
325
|
+
blocked
|
|
326
|
+
title "Subscription/account access blocked"
|
|
327
|
+
provider "Provider said"
|
|
328
|
+
code "Error code"
|
|
329
|
+
reason "Why the run stopped"
|
|
330
|
+
note "This is not a usage limit: waiting, retrying or switching model cannot fix it."
|
|
331
|
+
steps "What to do"
|
|
332
|
+
preserved "Uncommitted work was auto-committed before stopping."
|
|
333
|
+
resume "Resume after access is restored"
|
|
325
334
|
detail
|
|
326
335
|
ends
|
|
327
336
|
label "ends {{time}}"
|
|
@@ -746,6 +755,7 @@ en
|
|
|
746
755
|
- When you execute commands and the output becomes large, save the logs to files for easier review.
|
|
747
756
|
- When running commands, avoid setting a timeout yourself. Let them run as long as needed. The default timeout of 2 minutes is usually enough, and once commands finish, review the logs in the file.
|
|
748
757
|
- When running sudo commands, especially package installations like apt-get, yum, or npm install, run them in the background to avoid timeout issues and permission errors when the process needs to be killed. Use the run_in_background parameter or append & to the command.
|
|
758
|
+
- When you need to wait for something (CI checks, a background job, a deploy), do not run a long foreground `sleep`: agent harnesses block it ("Blocked: sleep 240 followed by: ..."). Poll instead with a short until-loop (e.g. `until <check>; do sleep 10; done`) or re-check the condition on your next step.
|
|
749
759
|
"""
|
|
750
760
|
purpose
|
|
751
761
|
subagent " - When the task is large and requires processing many files or folders, use `general-purpose` sub-agents to delegate work. Each separate file or folder can be delegated to a sub-agent for more efficient processing."
|
package/src/locales/hi.lino
CHANGED
|
@@ -322,6 +322,15 @@ hi
|
|
|
322
322
|
session "सत्र"
|
|
323
323
|
start "शुरुआत"
|
|
324
324
|
subscription
|
|
325
|
+
blocked
|
|
326
|
+
title "सदस्यता/खाता पहुँच अवरुद्ध"
|
|
327
|
+
provider "प्रदाता ने कहा"
|
|
328
|
+
code "त्रुटि कोड"
|
|
329
|
+
reason "रन क्यों रुका"
|
|
330
|
+
note "यह उपयोग सीमा नहीं है: प्रतीक्षा, पुनः प्रयास या मॉडल बदलना इसे ठीक नहीं करेगा।"
|
|
331
|
+
steps "क्या करें"
|
|
332
|
+
preserved "रुकने से पहले बिना कमिट किए बदलाव स्वतः कमिट कर दिए गए।"
|
|
333
|
+
resume "पहुँच बहाल होने पर फिर से शुरू करें"
|
|
325
334
|
detail
|
|
326
335
|
ends
|
|
327
336
|
label "{{time}} को समाप्त होगी"
|
|
@@ -746,6 +755,7 @@ hi
|
|
|
746
755
|
- जब आप कमांड चलाते हैं और आउटपुट बड़ा हो जाता है, तो आसान समीक्षा के लिए लॉग को फ़ाइलों में सहेजें।
|
|
747
756
|
- जब आप कमांड चलाते हैं, तो स्वयं टाइमआउट सेट न करें। उन्हें जितना आवश्यक हो उतना चलने दें। डिफ़ॉल्ट 2 मिनट का टाइमआउट आमतौर पर पर्याप्त होता है, और कमांड समाप्त होने के बाद फ़ाइल में लॉग की समीक्षा करें।
|
|
748
757
|
- जब आप sudo कमांड चलाते हैं, विशेष रूप से apt-get, yum, या npm install जैसी पैकेज स्थापना, तो टाइमआउट और प्रोसेस को मारने पर अनुमति त्रुटियों से बचने के लिए उन्हें पृष्ठभूमि में चलाएँ। run_in_background पैरामीटर का उपयोग करें या कमांड के अंत में & जोड़ें।
|
|
758
|
+
- जब आपको किसी चीज़ की प्रतीक्षा करनी हो (CI जाँच, पृष्ठभूमि कार्य, डिप्लॉय), तो लंबा `sleep` अग्रभूमि में न चलाएँ: एजेंट हार्नेस उसे रोक देते हैं ("Blocked: sleep 240 followed by: ...")। इसके बजाय छोटे अंतराल वाले until लूप से पोलिंग करें (उदाहरण: `until <जाँच>; do sleep 10; done`) या अगले चरण में स्थिति फिर से जाँचें।
|
|
749
759
|
"""
|
|
750
760
|
purpose
|
|
751
761
|
subagent " - जब कार्य बड़ा हो और कई फ़ाइलों या फ़ोल्डरों के प्रसंस्करण की आवश्यकता हो, तो कार्य सौंपने के लिए `general-purpose` sub-agents का उपयोग करें। प्रत्येक अलग फ़ाइल या फ़ोल्डर को अधिक कुशल प्रसंस्करण के लिए एक sub-agent को सौंपा जा सकता है।"
|
package/src/locales/ru.lino
CHANGED
|
@@ -322,6 +322,15 @@ ru
|
|
|
322
322
|
session "сеанс"
|
|
323
323
|
start "Начало"
|
|
324
324
|
subscription
|
|
325
|
+
blocked
|
|
326
|
+
title "Доступ по подписке/аккаунту заблокирован"
|
|
327
|
+
provider "Ответ провайдера"
|
|
328
|
+
code "Код ошибки"
|
|
329
|
+
reason "Почему запуск остановлен"
|
|
330
|
+
note "Это не лимит использования: ожидание, повтор или смена модели не помогут."
|
|
331
|
+
steps "Что делать"
|
|
332
|
+
preserved "Незакоммиченные изменения были автоматически закоммичены перед остановкой."
|
|
333
|
+
resume "Продолжить после восстановления доступа"
|
|
325
334
|
detail
|
|
326
335
|
ends
|
|
327
336
|
label "заканчивается {{time}}"
|
|
@@ -746,6 +755,7 @@ ru
|
|
|
746
755
|
- Когда выполняешь команды и вывод становится большим, сохраняй логи в файлы для удобного просмотра.
|
|
747
756
|
- Когда запускаешь команды, не задавай таймаут самостоятельно. Дай им работать столько, сколько нужно. Стандартного таймаута в 2 минуты обычно достаточно, и после завершения команды просмотри логи в файле.
|
|
748
757
|
- Когда запускаешь команды sudo, особенно установку пакетов вроде apt-get, yum или npm install, запускай их в фоне, чтобы избежать таймаутов и ошибок прав доступа при необходимости остановить процесс. Используй параметр run_in_background или добавляй & в конце команды.
|
|
758
|
+
- Когда нужно чего-то дождаться (проверок CI, фоновой задачи, деплоя), не запускай длинный `sleep` в основном потоке: агентные окружения блокируют его ("Blocked: sleep 240 followed by: ..."). Вместо этого опрашивай состояние коротким циклом until (например, `until <проверка>; do sleep 10; done`) или проверяй условие заново на следующем шаге.
|
|
749
759
|
"""
|
|
750
760
|
purpose
|
|
751
761
|
subagent " - Когда задача большая и требует обработки многих файлов или папок, используй `general-purpose` суб-агентов, чтобы делегировать работу. Каждый отдельный файл или папка может быть делегирован суб-агенту для более эффективной обработки."
|
package/src/locales/zh.lino
CHANGED
|
@@ -322,6 +322,15 @@ zh
|
|
|
322
322
|
session "会话"
|
|
323
323
|
start "开始"
|
|
324
324
|
subscription
|
|
325
|
+
blocked
|
|
326
|
+
title "订阅/账号访问被阻止"
|
|
327
|
+
provider "服务方提示"
|
|
328
|
+
code "错误代码"
|
|
329
|
+
reason "运行停止的原因"
|
|
330
|
+
note "这不是用量限制:等待、重试或切换模型都无法解决。"
|
|
331
|
+
steps "如何处理"
|
|
332
|
+
preserved "停止前已自动提交未提交的改动。"
|
|
333
|
+
resume "恢复访问后继续"
|
|
325
334
|
detail
|
|
326
335
|
ends
|
|
327
336
|
label "结束于 {{time}}"
|
|
@@ -746,6 +755,7 @@ zh
|
|
|
746
755
|
- 当你执行的命令输出变得很大时,将日志保存到文件以便复查。
|
|
747
756
|
- 当你运行命令时,不要自行设置超时。让它们按需运行。默认 2 分钟超时通常足够,命令完成后再查看文件中的日志。
|
|
748
757
|
- 当你运行 sudo 命令(尤其是 apt-get、yum 或 npm install 等包安装)时,请在后台运行以避免超时和需要终止进程时的权限错误。使用 run_in_background 参数或在命令末尾追加 &。
|
|
758
|
+
- 当你需要等待某件事(CI 检查、后台任务、部署)时,不要在前台运行长时间的 `sleep`:智能体运行环境会拦截它("Blocked: sleep 240 followed by: ...")。请改用短间隔的 until 轮询循环(例如 `until <检查>; do sleep 10; done`),或在下一步重新检查条件。
|
|
749
759
|
"""
|
|
750
760
|
purpose
|
|
751
761
|
subagent " - 当任务很大且需要处理许多文件或文件夹时,使用 `general-purpose` 子代理来委派工作。每个单独的文件或文件夹都可以委派给一个子代理以更高效地处理。"
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Session log renaming (Issue #2160)
|
|
5
|
+
*
|
|
6
|
+
* When an AI tool reports its session id, the run log is renamed to `<sessionId>.log` so that
|
|
7
|
+
* the log file can be correlated with the tool session. The logic used to live inline in
|
|
8
|
+
* src/claude.lib.mjs and depended on `getLogFile`/`setLogFile` being forwarded through every
|
|
9
|
+
* caller. Restart/watch iterations (src/solve.restart-shared.lib.mjs) either omitted those
|
|
10
|
+
* parameters or passed no-op stubs, which produced this on every restart iteration:
|
|
11
|
+
*
|
|
12
|
+
* ⚠️ Could not rename log file: getLogFile is not a function
|
|
13
|
+
*
|
|
14
|
+
* Extracting the logic makes the failure mode explicit (a named reason instead of a TypeError)
|
|
15
|
+
* and testable.
|
|
16
|
+
*
|
|
17
|
+
* @see https://github.com/link-assistant/hive-mind/issues/2160
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { promises as fs } from 'node:fs';
|
|
21
|
+
import path from 'node:path';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Rename the current log file to `<sessionId>.log`.
|
|
25
|
+
*
|
|
26
|
+
* Never throws: every failure is returned as `{ ok: false, reason }` so callers can log it.
|
|
27
|
+
*
|
|
28
|
+
* @param {Object} params
|
|
29
|
+
* @param {string} params.sessionId - Session id reported by the AI tool
|
|
30
|
+
* @param {Function} params.getLogFile - Accessor returning the current log file path
|
|
31
|
+
* @param {Function} params.setLogFile - Accessor updating the current log file path
|
|
32
|
+
* @param {Function} [params.log] - Async logger
|
|
33
|
+
* @param {Object} [params.fileSystem] - Injectable fs.promises replacement (tests)
|
|
34
|
+
* @returns {Promise<{ok: boolean, reason?: string, error?: Error, sessionLogFile?: string}>}
|
|
35
|
+
*/
|
|
36
|
+
export const renameLogToSessionId = async ({ sessionId, getLogFile, setLogFile, log, fileSystem = fs }) => {
|
|
37
|
+
if (!sessionId) return { ok: false, reason: 'missing_session_id' };
|
|
38
|
+
if (typeof getLogFile !== 'function' || typeof setLogFile !== 'function') {
|
|
39
|
+
// Issue #2160: a caller that forgot to forward the accessors. Report it as a real defect
|
|
40
|
+
// instead of surfacing "getLogFile is not a function" as a mysterious warning.
|
|
41
|
+
if (log) await log('⚠️ Could not rename log file: log file accessors were not provided by the caller', { verbose: true });
|
|
42
|
+
return { ok: false, reason: 'missing_log_file_accessors' };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const currentLogFile = getLogFile();
|
|
46
|
+
if (!currentLogFile) {
|
|
47
|
+
if (log) await log('⚠️ Could not rename log file: no current log file is configured', { verbose: true });
|
|
48
|
+
return { ok: false, reason: 'no_current_log_file' };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const sessionLogFile = path.join(path.dirname(currentLogFile), `${sessionId}.log`);
|
|
52
|
+
if (sessionLogFile === currentLogFile) return { ok: true, reason: 'already_named', sessionLogFile };
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
await fileSystem.rename(currentLogFile, sessionLogFile);
|
|
56
|
+
setLogFile(sessionLogFile);
|
|
57
|
+
if (log) await log(`📁 Log renamed to: ${sessionLogFile}`);
|
|
58
|
+
return { ok: true, sessionLogFile };
|
|
59
|
+
} catch (error) {
|
|
60
|
+
if (log) await log(`⚠️ Could not rename log file: ${error.message}`, { verbose: true });
|
|
61
|
+
return { ok: false, reason: 'rename_failed', error, sessionLogFile };
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export default { renameLogToSessionId };
|