@link-assistant/hive-mind 1.73.0 → 1.73.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 CHANGED
@@ -1,5 +1,11 @@
1
1
  # @link-assistant/hive-mind
2
2
 
3
+ ## 1.73.1
4
+
5
+ ### Patch Changes
6
+
7
+ - df8b776: Stop the auto-restart-until-mergeable and watch loops from treating the AI agent's own session comments (e.g. free-form "CI now green" status updates posted through the authenticated account) as new human feedback, which caused an endless restart loop until the iteration limit (issue #1827). The check window is now advanced monotonically, every comment the authenticated account posts during a session is tracked by ID, and watch-mode feedback counting excludes tool-generated comments by marker and tracked ID.
8
+
3
9
  ## 1.73.0
4
10
 
5
11
  ### Minor Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "1.73.0",
3
+ "version": "1.73.1",
4
4
  "description": "AI-powered issue solver and hive mind for collaborative problem solving",
5
5
  "main": "src/hive.mjs",
6
6
  "type": "module",
@@ -76,7 +76,7 @@ const formatRunLine = run => {
76
76
  // search scope for checkForExistingComment() stays in lock-step with the
77
77
  // markers actually embedded in tool-posted comments.
78
78
  const toolComments = await import('./tool-comments.lib.mjs');
79
- const { SESSION_ENDING_MARKERS, isToolGeneratedComment, isToolTrackedCommentId } = toolComments;
79
+ const { SESSION_ENDING_MARKERS, isToolGeneratedComment, isToolTrackedCommentId, trackToolCommentId } = toolComments;
80
80
 
81
81
  /**
82
82
  * Issue #1323: Check if a comment with specific content already exists on the PR
@@ -292,6 +292,121 @@ export const checkForNonBotComments = async (owner, repo, prNumber, issueNumber,
292
292
  }
293
293
  };
294
294
 
295
+ /**
296
+ * Issue #1827: Compute the next monotonic check-window cutoff for the
297
+ * auto-restart-until-mergeable loop. The cutoff must never move backwards:
298
+ * after an AI session, lastCheckTime is set to a moment *after* the agent's own
299
+ * comments, so rewinding it to the iteration's start time (captured before the
300
+ * AI ran) would re-detect those comments as new feedback — the root cause of
301
+ * the restart loop in #1827. Returns whichever timestamp is later.
302
+ *
303
+ * @param {Date} lastCheckTime - current cutoff
304
+ * @param {Date} candidate - proposed new cutoff (usually the iteration start time)
305
+ * @returns {Date} the later of the two timestamps
306
+ */
307
+ export const nextMonotonicCheckTime = (lastCheckTime, candidate) => {
308
+ if (!(lastCheckTime instanceof Date)) return candidate;
309
+ if (!(candidate instanceof Date)) return lastCheckTime;
310
+ return candidate.getTime() > lastCheckTime.getTime() ? candidate : lastCheckTime;
311
+ };
312
+
313
+ /**
314
+ * Issue #1827: Register every comment authored by the authenticated GitHub
315
+ * account during an AI working session as a tool-generated comment.
316
+ *
317
+ * During a session, the AI agent can post free-form status comments through the
318
+ * authenticated account (e.g. "✅ CI now green", "✅ Verification pass"). These
319
+ * are NOT routed through postTrackedComment(), so their IDs were never captured,
320
+ * and they match none of the tool markers. Once issue #1821 made the watch loop
321
+ * trust same-account comments as human feedback, the very next iteration
322
+ * re-detected these comments as fresh feedback and triggered an endless
323
+ * auto-restart loop until the limit was hit.
324
+ *
325
+ * Because the authenticated account is busy running the AI for the whole
326
+ * session window, any comment it authored within that window is the tool's own,
327
+ * not human feedback. Tracking those IDs makes checkForNonBotComments filter
328
+ * them by ID regardless of timestamps — a defense that also survives clock skew
329
+ * between the local clock and GitHub's `created_at` (which a purely
330
+ * time-based cutoff cannot).
331
+ *
332
+ * @param {string} owner - Repository owner
333
+ * @param {string} repo - Repository name
334
+ * @param {number} prNumber - Pull request number
335
+ * @param {number} issueNumber - Issue number (may equal prNumber)
336
+ * @param {Date|string|number} sinceTime - Start of the session window
337
+ * @param {Function} commandRunner - Tagged-template command runner, injectable for tests
338
+ * @param {Object} options
339
+ * @param {boolean} [options.verbose=false]
340
+ * @param {string} [options.currentUser] - Pre-resolved authenticated login (skips the `gh api user` call)
341
+ * @returns {Promise<string[]>} Newly tracked comment IDs (as strings)
342
+ */
343
+ export const trackAuthenticatedUserCommentsSince = async (owner, repo, prNumber, issueNumber, sinceTime, commandRunner = $, options = {}) => {
344
+ const { verbose = false, currentUser: providedUser } = options;
345
+ const trackedIds = [];
346
+
347
+ try {
348
+ let currentUser = providedUser || null;
349
+ if (!currentUser) {
350
+ try {
351
+ const userResult = await commandRunner`gh api user --jq .login`;
352
+ if (userResult.code === 0) {
353
+ currentUser = userResult.stdout.toString().trim();
354
+ }
355
+ } catch {
356
+ // Without the authenticated login we cannot attribute comments; bail out.
357
+ }
358
+ }
359
+ if (!currentUser) return trackedIds;
360
+
361
+ const since = sinceTime instanceof Date ? sinceTime : new Date(sinceTime);
362
+
363
+ const fetchComments = async path => {
364
+ try {
365
+ const result = await commandRunner`gh api ${path} --paginate`;
366
+ if (result.code === 0 && result.stdout) {
367
+ return JSON.parse(result.stdout.toString() || '[]');
368
+ }
369
+ } catch {
370
+ // Ignore fetch/parse failures for an individual endpoint.
371
+ }
372
+ return [];
373
+ };
374
+
375
+ const prComments = await fetchComments(`repos/${owner}/${repo}/issues/${prNumber}/comments`);
376
+ const prReviewComments = await fetchComments(`repos/${owner}/${repo}/pulls/${prNumber}/comments`);
377
+ let issueComments = [];
378
+ if (issueNumber && issueNumber !== prNumber) {
379
+ issueComments = await fetchComments(`repos/${owner}/${repo}/issues/${issueNumber}/comments`);
380
+ }
381
+
382
+ const allComments = [...prComments, ...prReviewComments, ...issueComments];
383
+ for (const comment of allComments) {
384
+ const login = comment.user?.login;
385
+ if (!login || login !== currentUser) continue;
386
+ // Inclusive lower bound: a comment posted at the exact session start is
387
+ // still the tool's own. created_at uses GitHub's clock, so allow equality.
388
+ const createdAt = new Date(comment.created_at);
389
+ if (createdAt < since) continue;
390
+ if (isToolTrackedCommentId(comment.id)) continue;
391
+ trackToolCommentId(comment.id);
392
+ trackedIds.push(String(comment.id));
393
+ if (verbose) {
394
+ console.log(`[VERBOSE] Tracking authenticated-user session comment ${comment.id} from ${login} at ${comment.created_at}`);
395
+ }
396
+ }
397
+ } catch (error) {
398
+ reportError(error, {
399
+ context: 'track_authenticated_user_comments',
400
+ owner,
401
+ repo,
402
+ prNumber,
403
+ operation: 'track_session_comments',
404
+ });
405
+ }
406
+
407
+ return trackedIds;
408
+ };
409
+
295
410
  /**
296
411
  * Get the reasons why PR is not mergeable
297
412
  * Issue #1314: Comprehensive CI/CD status handling covering all possible states:
@@ -53,7 +53,7 @@ import { limitReset } from './config.lib.mjs';
53
53
 
54
54
  // Import helper functions extracted for file size management (Issue #1593)
55
55
  const autoMergeHelpers = await import('./solve.auto-merge-helpers.lib.mjs');
56
- const { checkForExistingComment, checkForNonBotComments, getMergeBlockers } = autoMergeHelpers;
56
+ const { checkForExistingComment, checkForNonBotComments, getMergeBlockers, trackAuthenticatedUserCommentsSince, nextMonotonicCheckTime } = autoMergeHelpers;
57
57
 
58
58
  // Issue #1769: cancelled/stale CI re-run failures need a human action stop, not polling forever.
59
59
  const cancelledCiRerunLib = await import('./cancelled-ci-rerun.lib.mjs');
@@ -1031,6 +1031,26 @@ No further AI sessions will be started automatically for this run. Please review
1031
1031
  await log(formatAligned('✅', `${argv.tool.toUpperCase()} execution completed:`, 'Checking if PR is now mergeable...'));
1032
1032
  }
1033
1033
 
1034
+ // Issue #1827: Register every comment the authenticated account posted
1035
+ // during this AI session (free-form status comments like "✅ CI now
1036
+ // green" the agent writes itself, which bypass postTrackedComment and
1037
+ // match no tool marker). Tracking their IDs stops the next iteration's
1038
+ // checkForNonBotComments from mistaking them for fresh human feedback.
1039
+ try {
1040
+ const tracked = await trackAuthenticatedUserCommentsSince(owner, repo, prNumber, issueNumber, iterationStartTime, $, { verbose: argv.verbose });
1041
+ if (argv.verbose && tracked.length > 0) {
1042
+ await log(formatAligned('🧷', 'Tracked own session comments:', `${tracked.length} (won't count as new feedback)`, 2));
1043
+ }
1044
+ } catch (trackError) {
1045
+ reportError(trackError, {
1046
+ context: 'track_authenticated_user_session_comments',
1047
+ prNumber,
1048
+ owner,
1049
+ repo,
1050
+ operation: 'track_session_comments',
1051
+ });
1052
+ }
1053
+
1034
1054
  // Update last check time after restart
1035
1055
  lastCheckTime = new Date();
1036
1056
  } else if (blockers.length > 0) {
@@ -1071,8 +1091,16 @@ No further AI sessions will be started automatically for this run. Please review
1071
1091
  await log(formatAligned('', 'No action needed', 'Continuing to monitor...', 2));
1072
1092
  }
1073
1093
 
1074
- // Update last check time
1075
- lastCheckTime = currentTime;
1094
+ // Issue #1827: Advance the check window monotonically — never move it
1095
+ // backwards. In the restart branch above, lastCheckTime was already set
1096
+ // to a moment *after* the AI session (and after any comments the agent
1097
+ // posted). currentTime was captured at the *start* of this iteration,
1098
+ // before the AI ran, so assigning it unconditionally here would rewind
1099
+ // the window and re-detect the agent's own comments as new feedback
1100
+ // (the root cause of the auto-restart loop in #1827). In the non-restart
1101
+ // branches lastCheckTime is still the previous iteration's value, which
1102
+ // is < currentTime, so this correctly advances it.
1103
+ lastCheckTime = nextMonotonicCheckTime(lastCheckTime, currentTime);
1076
1104
  } catch (error) {
1077
1105
  reportError(error, {
1078
1106
  context: 'watch_until_mergeable',
@@ -7,6 +7,9 @@
7
7
  import { reportError } from './sentry.lib.mjs';
8
8
 
9
9
  import { wrapDollarWithGhRetry as _wrapDollarWithGhRetry } from './github-rate-limit.lib.mjs'; // rate-limit marker (#1726): gh API calls flow through $ wrapped by caller
10
+ // Issue #1827: tool-generated comments (markers + in-memory tracked IDs) must
11
+ // not count as feedback in watch/continue mode, mirroring checkForNonBotComments.
12
+ import { isToolGeneratedComment, isToolTrackedCommentId } from './tool-comments.lib.mjs';
10
13
  export const detectAndCountFeedback = async params => {
11
14
  const { prNumber, branchName, owner, repo, issueNumber, isContinueMode, argv, mergeStateStatus, prState, workStartTime, log, formatAligned, cleanErrorMessage, $, repositoryPath = null } = params;
12
15
 
@@ -93,6 +96,14 @@ export const detectAndCountFeedback = async params => {
93
96
  // Define log patterns to filter out comments containing logs from solve.mjs
94
97
  const logPatterns = [/📊.*Log file|solution\s+draft.*log/i, /🔗.*Link:|💻.*Session:/i, /Generated with.*solve\.mjs/i, /Session ID:|Log file available:/i];
95
98
 
99
+ // Issue #1827: A comment is tool-generated if its ID was tracked in
100
+ // memory during this run (system status comments AND the agent's own
101
+ // session comments) or if its body carries a known tool marker (catches
102
+ // comments from previous runs whose IDs are gone). These must never
103
+ // count as feedback — otherwise the agent's own "CI now green" / status
104
+ // comments trigger an endless restart loop (see PR link-foundation/rust-web-box#34).
105
+ const isToolComment = comment => isToolTrackedCommentId(comment.id) || isToolGeneratedComment(comment.body);
106
+
96
107
  // Count new PR comments after last commit (both code review comments and conversation comments)
97
108
  let prReviewComments = [];
98
109
  let prConversationComments = [];
@@ -112,6 +123,10 @@ export const detectAndCountFeedback = async params => {
112
123
 
113
124
  // Helper function to filter comments based on time and log patterns
114
125
  const filterComment = comment => {
126
+ // Issue #1827: never count tool-generated comments as feedback.
127
+ if (isToolComment(comment)) {
128
+ return false;
129
+ }
115
130
  const commentTime = new Date(comment.created_at);
116
131
  const isAfterCommit = commentTime > lastCommitTime;
117
132
  const isNotLogPattern = !logPatterns.some(pattern => pattern.test(comment.body || ''));
@@ -145,6 +160,10 @@ export const detectAndCountFeedback = async params => {
145
160
  if (issueCommentsResult.code === 0) {
146
161
  const issueComments = JSON.parse(issueCommentsResult.stdout.toString());
147
162
  const filteredIssueComments = issueComments.filter(comment => {
163
+ // Issue #1827: never count tool-generated comments as feedback.
164
+ if (isToolComment(comment)) {
165
+ return false;
166
+ }
148
167
  const commentTime = new Date(comment.created_at);
149
168
  const isAfterCommit = commentTime > lastCommitTime;
150
169
  const isNotLogPattern = !logPatterns.some(pattern => pattern.test(comment.body || ''));
@@ -46,6 +46,12 @@ const { formatAutoIterationLimit, hasReachedAutoIterationLimit, normalizeAutoIte
46
46
  const toolComments = await import('./tool-comments.lib.mjs');
47
47
  const { AUTO_RESTART_MARKER, postTrackedComment } = toolComments;
48
48
 
49
+ // Issue #1827: After each AI session, register the authenticated account's own
50
+ // comments (free-form status updates the agent posts itself) so the next
51
+ // detectAndCountFeedback() call doesn't mistake them for new human feedback.
52
+ const autoMergeHelpers = await import('./solve.auto-merge-helpers.lib.mjs');
53
+ const { trackAuthenticatedUserCommentsSince } = autoMergeHelpers;
54
+
49
55
  // Issue #1728: Per-iteration working session summary attachment helper
50
56
  // Issue #1763: Per-iteration PR ↔ issue link verification (in case the AI
51
57
  // agent overwrites the PR body without a closing keyword and the iteration
@@ -340,6 +346,24 @@ export const watchForFeedback = async params => {
340
346
  global.previousSessionId = toolResult.sessionId;
341
347
  }
342
348
 
349
+ // Issue #1827: Track the authenticated account's own comments posted
350
+ // during this session window so they are filtered (by ID) on the next
351
+ // feedback check instead of re-triggering a restart.
352
+ try {
353
+ const tracked = await trackAuthenticatedUserCommentsSince(owner, repo, prNumber, issueNumber, iterationStartTime, $, { verbose: argv.verbose });
354
+ if (argv.verbose && tracked.length > 0) {
355
+ await log(formatAligned('🧷', 'Tracked own session comments:', `${tracked.length} (won't count as feedback)`, 2));
356
+ }
357
+ } catch (trackError) {
358
+ reportError(trackError, {
359
+ context: 'track_authenticated_user_session_comments',
360
+ prNumber,
361
+ owner,
362
+ repo,
363
+ operation: 'track_session_comments',
364
+ });
365
+ }
366
+
343
367
  if (!toolResult.success) {
344
368
  // Check if this is an API error using shared utility
345
369
  if (isApiError(toolResult)) {