@link-assistant/hive-mind 2.10.2 → 2.10.4

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,17 @@
1
1
  # @link-assistant/hive-mind
2
2
 
3
+ ## 2.10.4
4
+
5
+ ### Patch Changes
6
+
7
+ - f1d5026: Recover use-m dependency imports when an installed package is missing an internal module or alias cleanup hits a transient filesystem race by removing the incomplete alias with bounded retries and reloading it. The CDN bootstrap fallback is also repinned from use-m 8.13.8 to 8.14.4 so a CDN outage no longer downgrades dependency loading to a use-m without corrupt-alias recovery.
8
+
9
+ ## 2.10.3
10
+
11
+ ### Patch Changes
12
+
13
+ - 3cfd6c2: Show cost and token/context budget facts in automated working-session summaries.
14
+
3
15
  ## 2.10.2
4
16
 
5
17
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "2.10.2",
3
+ "version": "2.10.4",
4
4
  "description": "AI-powered issue solver and hive mind for collaborative problem solving",
5
5
  "main": "src/hive.mjs",
6
6
  "type": "module",
@@ -1068,6 +1068,10 @@ No further AI sessions will be started automatically for this run. Please review
1068
1068
  prNumber,
1069
1069
  issueNumber,
1070
1070
  success: true,
1071
+ publicPricingEstimate: toolResult.publicPricingEstimate,
1072
+ anthropicTotalCostUSD: latestAnthropicCost,
1073
+ pricingInfo: toolResult.pricingInfo,
1074
+ budgetStatsData: autoMergeBudgetStatsData,
1071
1075
  });
1072
1076
  } catch (summaryError) {
1073
1077
  reportError(summaryError, {
package/src/solve.mjs CHANGED
@@ -1228,13 +1228,8 @@ try {
1228
1228
 
1229
1229
  // Issue #2048: commit+push dev log BEFORE any PR readiness signal so its CI gates readiness (was last, breaking CI post-signal; PR #2046, docs/case-studies/issue-2048). Idempotent: trailing call is a no-op. prettier-ignore
1230
1230
  await finalizeDevelopmentLog();
1231
- // Issue #1263 / #1728: Working session summary attachment.
1232
- // Routed through the shared maybeAttachWorkingSessionSummary helper so that
1233
- // top-level solve, auto-restart-until-mergeable, and watch-mode iterations
1234
- // all use identical attach logic. The helper internally honours
1235
- // --attach-solution-summary (always attach) and --auto-attach-solution-summary
1236
- // (attach only if no AI comment was posted during the session).
1237
- await maybeAttachWorkingSessionSummary({
1231
+ // Issue #1263 / #1728 / #2115: shared summary attachment and usage display.
1232
+ const { budgetStatsData: workingSessionBudgetStatsData = null } = await maybeAttachWorkingSessionSummary({
1238
1233
  argv,
1239
1234
  resultSummary,
1240
1235
  workStartTime,
@@ -1243,10 +1238,14 @@ try {
1243
1238
  prNumber,
1244
1239
  issueNumber,
1245
1240
  success,
1241
+ publicPricingEstimate,
1242
+ anthropicTotalCostUSD,
1243
+ pricingInfo,
1244
+ sessionUsage: { sessionId, tempDir, resultModelUsage, streamTokenUsage, subAgentCalls },
1246
1245
  });
1247
1246
 
1248
1247
  // Search for newly created pull requests and comments
1249
- const verifyResult = await verifyResults(owner, repo, branchName, issueNumber, prNumber, prUrl, referenceTime, argv, shouldAttachLogs, shouldRestart, sessionId, tempDir, anthropicTotalCostUSD, publicPricingEstimate, pricingInfo, errorDuringExecution, sessionType, resultModelUsage, streamTokenUsage, subAgentCalls);
1248
+ const verifyResult = await verifyResults(owner, repo, branchName, issueNumber, prNumber, prUrl, referenceTime, argv, shouldAttachLogs, shouldRestart, sessionId, tempDir, anthropicTotalCostUSD, publicPricingEstimate, pricingInfo, errorDuringExecution, sessionType, resultModelUsage, streamTokenUsage, subAgentCalls, workingSessionBudgetStatsData);
1250
1249
  const logsAlreadyUploaded = verifyResult?.logUploadSuccess || false;
1251
1250
 
1252
1251
  // Issue #1162: Auto-restart when PR title/description still has placeholder content
@@ -28,6 +28,8 @@ import { safeExit } from './exit-handler.lib.mjs';
28
28
  // Import GitHub-related functions
29
29
  const githubLib = await import('./github.lib.mjs');
30
30
  const { sanitizeLogContent, attachLogToGitHub } = githubLib;
31
+ const { buildCostInfoString } = await import('./github-cost-info.lib.mjs');
32
+ const { buildBudgetStatsString } = await import('./claude.budget-stats.lib.mjs');
31
33
 
32
34
  // Issue #1745: process-wide sanitization counters used to print a one-line
33
35
  // "we masked N secrets" summary at the end of each run.
@@ -663,11 +665,9 @@ export const showSessionSummary = async (sessionId, limitReached, argv, issueUrl
663
665
  }
664
666
  };
665
667
 
666
- // Verify results by searching for new PRs and comments
667
- export const verifyResults = async (owner, repo, branchName, issueNumber, prNumber, prUrl, referenceTime, argv, shouldAttachLogs, shouldRestart = false, sessionId = null, tempDir = null, anthropicTotalCostUSD = null, publicPricingEstimate = null, pricingInfo = null, errorDuringExecution = false, sessionType = 'new', resultModelUsage = null, streamTokenUsage = null, subAgentCalls = null) => {
668
- await log('\nšŸ” Searching for created pull requests or comments...');
669
-
670
- // Issue #1491, #1526: Build budget stats data for GitHub comment (computed once, used in both PR and issue paths)
668
+ // Build token/context data once so every end-of-session publication can use the
669
+ // same observed facts (working-session summary and attached log alike).
670
+ export const buildSessionBudgetStatsData = async ({ argv, sessionId = null, tempDir = null, resultModelUsage = null, streamTokenUsage = null, subAgentCalls = null, pricingInfo = null }) => {
671
671
  let budgetStatsData = null;
672
672
  if (argv.tokensBudgetStats && sessionId && tempDir) {
673
673
  try {
@@ -692,6 +692,26 @@ export const verifyResults = async (owner, repo, branchName, issueNumber, prNumb
692
692
  if (argv.verbose) await log(` āš ļø Could not build agent budget stats: ${agentBudgetError.message}`, { verbose: true });
693
693
  }
694
694
  }
695
+ return budgetStatsData;
696
+ };
697
+
698
+ // Verify results by searching for new PRs and comments
699
+ export const verifyResults = async (owner, repo, branchName, issueNumber, prNumber, prUrl, referenceTime, argv, shouldAttachLogs, shouldRestart = false, sessionId = null, tempDir = null, anthropicTotalCostUSD = null, publicPricingEstimate = null, pricingInfo = null, errorDuringExecution = false, sessionType = 'new', resultModelUsage = null, streamTokenUsage = null, subAgentCalls = null, precomputedBudgetStatsData = null) => {
700
+ await log('\nšŸ” Searching for created pull requests or comments...');
701
+
702
+ // Issue #1491, #1526, #2115: reuse data already calculated for the working
703
+ // session summary; retain the fallback for callers that do not precompute it.
704
+ const budgetStatsData =
705
+ precomputedBudgetStatsData ??
706
+ (await buildSessionBudgetStatsData({
707
+ argv,
708
+ sessionId,
709
+ tempDir,
710
+ resultModelUsage,
711
+ streamTokenUsage,
712
+ subAgentCalls,
713
+ pricingInfo,
714
+ }));
695
715
 
696
716
  try {
697
717
  // Get the current user's GitHub username
@@ -1241,7 +1261,15 @@ export const checkForAiCreatedComments = async (sessionStartTime, owner, repo, p
1241
1261
  * @param {string} options.repo - Repository name
1242
1262
  * @returns {Promise<boolean>} - True if comment was posted successfully
1243
1263
  */
1244
- export const attachSolutionSummary = async ({ resultSummary, prNumber, issueNumber, owner, repo }) => {
1264
+ export const buildWorkingSessionSummaryDetails = ({ publicPricingEstimate = null, anthropicTotalCostUSD = null, pricingInfo = null, budgetStatsData = null } = {}) => {
1265
+ const costInfo = buildCostInfoString(publicPricingEstimate, anthropicTotalCostUSD, pricingInfo, {
1266
+ includeTokenUsage: false,
1267
+ });
1268
+ const budgetStats = budgetStatsData ? buildBudgetStatsString(budgetStatsData.tokenUsage, budgetStatsData.subAgentCalls) : '';
1269
+ return `${costInfo}${budgetStats}`.trim();
1270
+ };
1271
+
1272
+ export const attachSolutionSummary = async ({ resultSummary, prNumber, issueNumber, owner, repo, publicPricingEstimate = null, anthropicTotalCostUSD = null, pricingInfo = null, budgetStatsData = null }) => {
1245
1273
  if (!resultSummary || typeof resultSummary !== 'string') {
1246
1274
  await log('āš ļø No working session summary available to attach', { verbose: true });
1247
1275
  return false;
@@ -1256,10 +1284,16 @@ export const attachSolutionSummary = async ({ resultSummary, prNumber, issueNumb
1256
1284
  }
1257
1285
 
1258
1286
  try {
1287
+ const usageDetails = buildWorkingSessionSummaryDetails({
1288
+ publicPricingEstimate,
1289
+ anthropicTotalCostUSD,
1290
+ pricingInfo,
1291
+ budgetStatsData,
1292
+ });
1259
1293
  const comment = `${toolComments.WORKING_SESSION_SUMMARY_AUTOMATION_MARKER}
1260
1294
  ## ${toolComments.WORKING_SESSION_SUMMARY_MARKER}
1261
1295
 
1262
- ${resultSummary}
1296
+ ${resultSummary}${usageDetails ? `\n\n${usageDetails}` : ''}
1263
1297
 
1264
1298
  ---
1265
1299
  *${toolComments.WORKING_SESSION_SUMMARY_AUTOMATED_FOOTER}*`;
@@ -1316,7 +1350,7 @@ ${resultSummary}
1316
1350
  * @param {boolean} [options.success=true] - skip attachment for failed iterations
1317
1351
  * @returns {Promise<{attached: boolean, reason: string}>}
1318
1352
  */
1319
- export const maybeAttachWorkingSessionSummary = async ({ argv, resultSummary, workStartTime, owner, repo, prNumber, issueNumber, success = true }) => {
1353
+ export const maybeAttachWorkingSessionSummary = async ({ argv, resultSummary, workStartTime, owner, repo, prNumber, issueNumber, success = true, publicPricingEstimate = null, anthropicTotalCostUSD = null, pricingInfo = null, budgetStatsData = null, sessionUsage = null }) => {
1320
1354
  if (!success) {
1321
1355
  return { attached: false, reason: 'iteration_failed' };
1322
1356
  }
@@ -1352,6 +1386,25 @@ export const maybeAttachWorkingSessionSummary = async ({ argv, resultSummary, wo
1352
1386
  return { attached: false, reason: 'no_attach_decision' };
1353
1387
  }
1354
1388
 
1355
- const ok = await attachSolutionSummary({ resultSummary, prNumber, issueNumber, owner, repo });
1356
- return { attached: !!ok, reason: ok ? 'attached' : 'post_failed' };
1389
+ const resolvedBudgetStatsData =
1390
+ budgetStatsData ??
1391
+ (sessionUsage
1392
+ ? await buildSessionBudgetStatsData({
1393
+ argv,
1394
+ pricingInfo,
1395
+ ...sessionUsage,
1396
+ })
1397
+ : null);
1398
+ const ok = await attachSolutionSummary({
1399
+ resultSummary,
1400
+ prNumber,
1401
+ issueNumber,
1402
+ owner,
1403
+ repo,
1404
+ publicPricingEstimate,
1405
+ anthropicTotalCostUSD,
1406
+ pricingInfo,
1407
+ budgetStatsData: resolvedBudgetStatsData,
1408
+ });
1409
+ return { attached: !!ok, reason: ok ? 'attached' : 'post_failed', budgetStatsData: resolvedBudgetStatsData };
1357
1410
  };
@@ -580,6 +580,10 @@ export const watchForFeedback = async params => {
580
580
  prNumber,
581
581
  issueNumber,
582
582
  success: true,
583
+ publicPricingEstimate: toolResult.publicPricingEstimate,
584
+ anthropicTotalCostUSD: latestAnthropicCost,
585
+ pricingInfo: toolResult.pricingInfo,
586
+ budgetStatsData: autoRestartBudgetStatsData,
583
587
  });
584
588
  } catch (summaryError) {
585
589
  reportError(summaryError, {
@@ -3,7 +3,14 @@
3
3
  import { wrapUseWithRetry } from './use-with-retry.lib.mjs';
4
4
 
5
5
  export const USE_M_BOOTSTRAP_URL = 'https://unpkg.com/use-m/use.js';
6
- export const USE_M_BOOTSTRAP_FALLBACK_URL = 'https://unpkg.com/use-m@8.13.8/use.js';
6
+ // Issue #2113: the fallback is only reached when unpkg cannot serve the `latest`
7
+ // bundle, but until now it pinned 8.13.8 — the last release *without* any
8
+ // corrupt-alias self-healing. A CDN hiccup therefore silently downgraded every
9
+ // dependency import to the least resilient loader available. 8.14.4 is the first
10
+ // release that both repairs corrupt aliases (8.14.3, use-m #66/#67) and removes
11
+ // them with a retry budget (8.14.4, use-m #68), so the degraded path now keeps
12
+ // upstream recovery instead of losing it.
13
+ export const USE_M_BOOTSTRAP_FALLBACK_URL = 'https://unpkg.com/use-m@8.14.4/use.js';
7
14
 
8
15
  const isMissingUseMBundle = code => /^Not found: \/use-m@[^/]+\/use\.js\s*$/.test(code.trim());
9
16
 
@@ -16,13 +16,21 @@
16
16
  * 3. Node throws `Invalid package config <dir>/package.json.` with
17
17
  * `code: 'ERR_INVALID_PACKAGE_CONFIG'` — the package.json itself
18
18
  * is corrupt/truncated and cannot even be parsed (issue #1712).
19
+ * 4. Node throws `ERR_MODULE_NOT_FOUND` for a file imported by the package
20
+ * entry point — npm left an incomplete package tree (issue #2113).
21
+ * 5. use-m's self-heal throws `Failed to remove corrupt npm alias ...`
22
+ * because a concurrent filesystem mutation made recursive removal fail
23
+ * with a retryable code such as `ENOTEMPTY` (issue #2113).
19
24
  *
20
- * The recovery is identical for all three: delete the broken alias install
25
+ * The recovery is identical for all five: delete the broken alias install
21
26
  * directory and ask use-m to re-fetch. A clean reinstall almost always
22
27
  * succeeds. This helper centralises that retry so every call site picks
23
28
  * it up.
24
29
  */
25
30
 
31
+ const ALIAS_CLEANUP_ERROR = /^Failed to remove (?:corrupt|incomplete) npm alias '([^']+)'\.$/;
32
+ const RETRYABLE_RM_CODES = new Set(['EBUSY', 'EMFILE', 'ENFILE', 'ENOTEMPTY', 'EPERM']);
33
+
26
34
  /**
27
35
  * @param {(specifier: string) => Promise<unknown>} use - the use-m loader.
28
36
  * @param {string} specifier - the npm specifier to load (e.g. `'getenv'`).
@@ -94,8 +102,9 @@ export const useWithRetry = async (use, specifier, options = {}) => {
94
102
  // Remember the file so the next attempt can bypass Node's poisoned
95
103
  // module cache if use-m hands us the same path again.
96
104
  cleanedImportPath = /Failed to import module from '/.test(error?.message ?? '') ? corruptedPath : null;
97
- } catch {
105
+ } catch (cleanupError) {
98
106
  // Best-effort cleanup; fall through to retry regardless.
107
+ log(`cleanup of ${resolveAliasDir(corruptedPath)} failed: ${cleanupError?.message}`);
99
108
  }
100
109
  }
101
110
  }
@@ -128,9 +137,23 @@ export const isCorruptInstallError = error => {
128
137
  // resolve/import logic gets a chance to run.
129
138
  if (error?.code === 'ERR_INVALID_PACKAGE_CONFIG') return true;
130
139
  if (cause?.code === 'ERR_INVALID_PACKAGE_CONFIG') return true;
140
+ // Mode 4 (issue #2113): the installed entry point exists, but one of the
141
+ // files it imports does not. Restrict this to use-m's import wrapper so an
142
+ // unrelated application-level ERR_MODULE_NOT_FOUND is not retried.
143
+ const message = typeof error?.message === 'string' ? error.message : '';
144
+ if (/^Failed to import module from '/.test(message) && cause?.code === 'ERR_MODULE_NOT_FOUND') return true;
145
+ // Mode 5 (issue #2113): use-m@8.14.3 added its own alias self-heal, but its
146
+ // recursive rm used Node's default maxRetries=0, so a concurrent mutation
147
+ // escaped as ENOTEMPTY (or another rm-retry code). use-m@8.14.4 fixed this
148
+ // upstream (use-m #68) by giving removePackageAlias its own retry budget.
149
+ // The downstream guard is deliberately kept: the bootstrap can still land on
150
+ // an older use-m (a pinned CDN fallback, a preinstalled global, or a cached
151
+ // bundle), and even 8.14.4 rethrows this exact wrapper once its own budget is
152
+ // exhausted — in which case a fresh removal plus reinstall is still the right
153
+ // recovery.
154
+ if (ALIAS_CLEANUP_ERROR.test(message) && RETRYABLE_RM_CODES.has(cause?.code)) return true;
131
155
  // Mode 2 (also seen on hosted CI): npm install completes but the package
132
156
  // tree is incomplete, so use-m can't resolve the entry point.
133
- const message = typeof error?.message === 'string' ? error.message : '';
134
157
  if (/^Failed to resolve the path to /.test(message)) return true;
135
158
  // Fallback string match for ERR_INVALID_PACKAGE_CONFIG (in case the error
136
159
  // bubbles through use-m without preserving the `code` property).
@@ -150,7 +173,10 @@ export const extractCorruptedFilePath = error => {
150
173
  // extract the package.json path so the caller's cleanup() walks up to
151
174
  // the alias dir.
152
175
  const invalidConfigMatch = message.match(/Invalid package config (\S+?package\.json)/);
153
- return invalidConfigMatch ? invalidConfigMatch[1] : null;
176
+ if (invalidConfigMatch) return invalidConfigMatch[1];
177
+ // Mode 5 (issue #2113): use-m already reports the whole alias directory.
178
+ const cleanupMatch = message.match(ALIAS_CLEANUP_ERROR);
179
+ return cleanupMatch ? cleanupMatch[1] : null;
154
180
  };
155
181
 
156
182
  /**
@@ -176,11 +202,15 @@ export const resolveAliasDir = corruptedPath => {
176
202
  return segments.slice(0, -1).join('/') || corruptedPath;
177
203
  };
178
204
 
179
- const defaultCleanup = async path => {
180
- const { rm } = await import('node:fs/promises');
181
- await rm(path, { recursive: true, force: true });
205
+ export const removeAliasWithRetry = async (path, options = {}) => {
206
+ const remove = options.rm ?? (await import('node:fs/promises')).rm;
207
+ const maxRetries = options.maxRetries ?? 5;
208
+ const retryDelay = options.retryDelay ?? 100;
209
+ await remove(path, { recursive: true, force: true, maxRetries, retryDelay });
182
210
  };
183
211
 
212
+ const defaultCleanup = removeAliasWithRetry;
213
+
184
214
  // Cache-busting import: a query string makes Node treat the URL as a distinct
185
215
  // module, so the freshly reinstalled file is evaluated instead of the cached
186
216
  // SyntaxError from the corrupt one.