@link-assistant/hive-mind 1.35.5 → 1.35.6

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.35.6
4
+
5
+ ### Patch Changes
6
+
7
+ - 4b0beaf: Fix interactive mode PR comment output: use stdin for GitHub API calls to prevent shell quoting corruption, flush comment queue before tool result timeout to prevent stuck "Waiting for result..." comments, and guard against duplicate session started comments from late system.init events
8
+
3
9
  ## 1.35.5
4
10
 
5
11
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "1.35.5",
3
+ "version": "1.35.6",
4
4
  "description": "AI-powered issue solver and hive mind for collaborative problem solving",
5
5
  "main": "src/hive.mjs",
6
6
  "type": "module",
@@ -52,6 +52,13 @@ const CONFIG = {
52
52
  // See: https://github.com/link-assistant/hive-mind/issues/1324
53
53
  import { sanitizeUnicode } from './unicode-sanitization.lib.mjs';
54
54
 
55
+ // Use child_process for stdin-based API calls to avoid shell quoting issues
56
+ // with large/complex comment bodies containing backticks, quotes, etc.
57
+ // See: https://github.com/link-assistant/hive-mind/issues/1458
58
+ import { execFile } from 'node:child_process';
59
+ import { promisify } from 'node:util';
60
+ const execFileAsync = promisify(execFile);
61
+
55
62
  /**
56
63
  * Truncate content in the middle, keeping start and end
57
64
  * This helps show context while reducing size for large outputs
@@ -237,7 +244,9 @@ const getToolIcon = toolName => {
237
244
  * @returns {Object} Handler object with event processing methods
238
245
  */
239
246
  export const createInteractiveHandler = options => {
240
- const { owner, repo, prNumber, $, log, verbose = false } = options;
247
+ const { owner, repo, prNumber, log, verbose = false, execFile: execFileFn } = options;
248
+ // Use injected execFile for testability, or the real one by default
249
+ const runGhApi = execFileFn || execFileAsync;
241
250
 
242
251
  // State tracking for the handler
243
252
  const state = {
@@ -291,24 +300,35 @@ export const createInteractiveHandler = options => {
291
300
  }
292
301
 
293
302
  try {
294
- // Post comment and capture the output to get the comment URL/ID
295
- const result = await $`gh pr comment ${prNumber} --repo ${owner}/${repo} --body ${body}`;
303
+ // Post comment via gh api with stdin to avoid shell quoting issues
304
+ // with complex markdown bodies containing backticks, quotes, etc.
305
+ // See: https://github.com/link-assistant/hive-mind/issues/1458
306
+ const apiUrl = `repos/${owner}/${repo}/issues/${prNumber}/comments`;
307
+ const jsonPayload = JSON.stringify({ body });
308
+ const { stdout } = await runGhApi('gh', ['api', apiUrl, '-X', 'POST', '--input', '-'], {
309
+ input: jsonPayload,
310
+ maxBuffer: 10 * 1024 * 1024, // 10MB
311
+ });
296
312
  state.lastCommentTime = Date.now();
297
313
 
298
- // Extract comment ID from the result (gh outputs the comment URL)
299
- // Format: https://github.com/owner/repo/pull/123#issuecomment-1234567890
300
- // Note: command-stream returns stdout as a Buffer, so we need to call .toString()
301
- const output = result.stdout?.toString() || result.toString() || '';
302
- const match = output.match(/issuecomment-(\d+)/);
303
- const commentId = match ? match[1] : null;
314
+ // Extract comment ID from the API response JSON
315
+ let commentId = null;
316
+ try {
317
+ const response = JSON.parse(stdout);
318
+ commentId = response.id ? String(response.id) : null;
319
+ } catch {
320
+ // Fallback: try to extract from URL pattern
321
+ const match = stdout.match(/issuecomment-(\d+)|"id":\s*(\d+)/);
322
+ commentId = match ? match[1] || match[2] : null;
323
+ }
304
324
 
305
325
  if (verbose) {
306
- await log(`✅ Interactive mode: Comment posted${commentId ? ` (ID: ${commentId})` : ''}`, { verbose: true });
326
+ await log(`✅ Interactive mode: Comment posted${commentId ? ` (ID: ${commentId})` : ''} (body: ${body.length} chars)`, { verbose: true });
307
327
  }
308
328
  return commentId;
309
329
  } catch (error) {
310
330
  if (verbose) {
311
- await log(`⚠️ Interactive mode: Failed to post comment: ${error.message}`, { verbose: true });
331
+ await log(`⚠️ Interactive mode: Failed to post comment: ${error.message} (body: ${body.length} chars)`, { verbose: true });
312
332
  }
313
333
  return null;
314
334
  }
@@ -330,14 +350,22 @@ export const createInteractiveHandler = options => {
330
350
  }
331
351
 
332
352
  try {
333
- await $`gh api repos/${owner}/${repo}/issues/comments/${commentId} -X PATCH -f body=${body}`;
353
+ // Edit comment via gh api with stdin to avoid shell quoting issues
354
+ // with complex markdown bodies containing backticks, quotes, etc.
355
+ // See: https://github.com/link-assistant/hive-mind/issues/1458
356
+ const apiUrl = `repos/${owner}/${repo}/issues/comments/${commentId}`;
357
+ const jsonPayload = JSON.stringify({ body });
358
+ await runGhApi('gh', ['api', apiUrl, '-X', 'PATCH', '--input', '-'], {
359
+ input: jsonPayload,
360
+ maxBuffer: 10 * 1024 * 1024, // 10MB
361
+ });
334
362
  if (verbose) {
335
- await log(`✅ Interactive mode: Comment ${commentId} updated`, { verbose: true });
363
+ await log(`✅ Interactive mode: Comment ${commentId} updated (body: ${body.length} chars, payload: ${jsonPayload.length} chars)`, { verbose: true });
336
364
  }
337
365
  return true;
338
366
  } catch (error) {
339
367
  if (verbose) {
340
- await log(`⚠️ Interactive mode: Failed to edit comment: ${error.message}`, { verbose: true });
368
+ await log(`⚠️ Interactive mode: Failed to edit comment ${commentId}: ${error.message} (body: ${body.length} chars)`, { verbose: true });
341
369
  }
342
370
  return false;
343
371
  }
@@ -398,6 +426,16 @@ export const createInteractiveHandler = options => {
398
426
  * @param {Object} data - Event data
399
427
  */
400
428
  const handleSystemInit = async data => {
429
+ // Guard against duplicate init events (e.g., when a late task_notification
430
+ // arrives after the result event and triggers a new conversation turn)
431
+ // See: https://github.com/link-assistant/hive-mind/issues/1458
432
+ if (state.sessionId) {
433
+ if (verbose) {
434
+ await log(`⚠️ Interactive mode: Ignoring duplicate system.init event (session already initialized: ${state.sessionId})`, { verbose: true });
435
+ }
436
+ return;
437
+ }
438
+
401
439
  state.sessionId = data.session_id;
402
440
  state.startTime = Date.now();
403
441
 
@@ -672,21 +710,44 @@ ${createRawJsonSection(data)}`;
672
710
  // If comment ID is not yet available (comment was queued), wait for it
673
711
  // But use a timeout to avoid blocking forever
674
712
  if (!commentId && commentIdPromise) {
675
- if (verbose) {
676
- await log(`⏳ Interactive mode: Waiting for tool use comment to be posted (tool: ${toolUseId})`, {
677
- verbose: true,
678
- });
713
+ // First, try to flush the queue — the tool_use comment may still be
714
+ // waiting for rate-limit clearance. Processing it here avoids the 30s
715
+ // timeout that previously caused many comments to stay stuck on
716
+ // "Waiting for result...".
717
+ // See: https://github.com/link-assistant/hive-mind/issues/1458
718
+ if (state.commentQueue.length > 0) {
719
+ if (verbose) {
720
+ await log(`🔄 Interactive mode: Flushing comment queue (${state.commentQueue.length} items) before waiting for tool use comment`, {
721
+ verbose: true,
722
+ });
723
+ }
724
+ // Temporarily reset isProcessing to allow processQueue to run
725
+ const wasProcessing = state.isProcessing;
726
+ state.isProcessing = false;
727
+ await processQueue();
728
+ state.isProcessing = wasProcessing;
679
729
  }
680
- // Wait for the comment to be posted (with 30 second timeout)
681
- const timeoutPromise = new Promise(resolve => setTimeout(() => resolve(null), 30000));
682
- commentId = await Promise.race([commentIdPromise, timeoutPromise]);
730
+
731
+ // Check again after queue flush
732
+ commentId = pendingCall.commentId;
683
733
 
684
734
  if (!commentId) {
685
735
  if (verbose) {
686
- await log('⚠️ Interactive mode: Timeout waiting for tool use comment, posting result separately', {
736
+ await log(`⏳ Interactive mode: Waiting for tool use comment to be posted (tool: ${toolUseId})`, {
687
737
  verbose: true,
688
738
  });
689
739
  }
740
+ // Wait for the comment to be posted (with 30 second timeout)
741
+ const timeoutPromise = new Promise(resolve => setTimeout(() => resolve(null), 30000));
742
+ commentId = await Promise.race([commentIdPromise, timeoutPromise]);
743
+
744
+ if (!commentId) {
745
+ if (verbose) {
746
+ await log('⚠️ Interactive mode: Timeout waiting for tool use comment, posting result separately', {
747
+ verbose: true,
748
+ });
749
+ }
750
+ }
690
751
  }
691
752
  }
692
753