@link-assistant/hive-mind 1.77.1 → 1.78.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.
Files changed (60) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/package.json +1 -1
  3. package/src/agent.lib.mjs +2 -1
  4. package/src/claude.lib.mjs +2 -2
  5. package/src/claude.runtime-switch.lib.mjs +2 -1
  6. package/src/codex.lib.mjs +2 -1
  7. package/src/config.lib.mjs +2 -1
  8. package/src/contributing-guidelines.lib.mjs +2 -1
  9. package/src/gemini.lib.mjs +2 -1
  10. package/src/github-entity-validation.lib.mjs +2 -1
  11. package/src/github-error-reporter.lib.mjs +2 -1
  12. package/src/github.batch.lib.mjs +2 -1
  13. package/src/github.lib.mjs +2 -1
  14. package/src/handoff-skill.lib.mjs +2 -1
  15. package/src/hive.mjs +11 -10
  16. package/src/interactive-codex-events.lib.mjs +167 -0
  17. package/src/interactive-mode.lib.mjs +62 -152
  18. package/src/interactive-mode.shared.lib.mjs +1 -0
  19. package/src/interactive-system-events.lib.mjs +224 -0
  20. package/src/isolation-runner.lib.mjs +2 -1
  21. package/src/lenv-reader.lib.mjs +2 -1
  22. package/src/lib.mjs +2 -1
  23. package/src/lino.lib.mjs +2 -1
  24. package/src/local-ci-checks.lib.mjs +2 -1
  25. package/src/log-upload.lib.mjs +2 -1
  26. package/src/memory-check.mjs +2 -1
  27. package/src/models/index.mjs +2 -1
  28. package/src/npm-global-prefix.lib.mjs +160 -0
  29. package/src/opencode.lib.mjs +2 -1
  30. package/src/playwright-mcp.lib.mjs +2 -1
  31. package/src/protect-branch.mjs +2 -1
  32. package/src/queue-config.lib.mjs +2 -1
  33. package/src/qwen.lib.mjs +2 -1
  34. package/src/review.mjs +2 -1
  35. package/src/reviewers-hive.mjs +2 -1
  36. package/src/solve.auto-continue.lib.mjs +2 -1
  37. package/src/solve.auto-ensure.lib.mjs +2 -1
  38. package/src/solve.auto-merge-helpers.lib.mjs +2 -1
  39. package/src/solve.auto-merge.lib.mjs +2 -1
  40. package/src/solve.bootstrap.lib.mjs +2 -1
  41. package/src/solve.escalate.lib.mjs +2 -1
  42. package/src/solve.execution.lib.mjs +2 -1
  43. package/src/solve.fork-detection.lib.mjs +2 -1
  44. package/src/solve.fork-sync.lib.mjs +2 -1
  45. package/src/solve.keep-working.lib.mjs +2 -1
  46. package/src/solve.mjs +2 -2
  47. package/src/solve.repository.lib.mjs +2 -1
  48. package/src/solve.restart-shared.lib.mjs +2 -1
  49. package/src/solve.results.lib.mjs +2 -1
  50. package/src/solve.validation.lib.mjs +2 -1
  51. package/src/solve.watch.lib.mjs +2 -1
  52. package/src/telegram-bot.mjs +2 -1
  53. package/src/telegram-safe-reply.lib.mjs +112 -10
  54. package/src/telegram-solve-queue.helpers.lib.mjs +117 -14
  55. package/src/telegram-solve-queue.lib.mjs +38 -44
  56. package/src/token-sanitization.lib.mjs +2 -1
  57. package/src/tool-comments.lib.mjs +2 -1
  58. package/src/use-m-bootstrap.lib.mjs +23 -0
  59. package/src/useless-tools.lib.mjs +2 -1
  60. package/src/youtrack/youtrack.lib.mjs +2 -1
@@ -10,6 +10,8 @@
10
10
  * - system.task_started: Agent subtask started (Issue #1450)
11
11
  * - system.task_progress: Agent subtask progress update (Issue #1450)
12
12
  * - system.task_notification: Agent subtask completed/failed (Issue #1450)
13
+ * - system.thinking_tokens: Accumulated thinking progress (Issue #1900)
14
+ * - system.status / system.compact_boundary / system.task_updated: Status lifecycle events (Issue #1900)
13
15
  * - assistant (text): AI text responses
14
16
  * - assistant (tool_use): Tool invocations
15
17
  * - user (tool_result): Tool execution results
@@ -32,7 +34,9 @@
32
34
  * @experimental
33
35
  */
34
36
 
35
- import { CONFIG, createCollapsible, createRawJsonSection, createRedactedRawJsonSection, escapeMarkdown, execFileAsync, formatCost, formatDuration, getToolIcon, redactImageData, safeJsonStringify, sanitizeUnicode, truncateMiddle } from './interactive-mode.shared.lib.mjs';
37
+ import { CONFIG, createCollapsible, createRawJsonSection, createRedactedRawJsonSection, escapeMarkdown, execFileAsync, formatCost, formatDuration, getToolIcon, safeJsonStringify, sanitizeUnicode, truncateMiddle } from './interactive-mode.shared.lib.mjs';
38
+ import { createCodexEventHandlers } from './interactive-codex-events.lib.mjs';
39
+ import { createSystemLifecycleHandlers } from './interactive-system-events.lib.mjs';
36
40
  // Issue #1843: turn base64 image tool-results into inline PR-comment images.
37
41
  import { createImageRenderer, extractImagePayload, isImageNode } from './interactive-image-render.lib.mjs';
38
42
  // Issue #1625: track interactive-mode comment IDs so they're excluded from
@@ -108,6 +112,9 @@ export const createInteractiveHandler = options => {
108
112
  // Track active agent tasks for progress update deduplication
109
113
  // Map of task_id -> { commentId, toolUseId, description, commentIdPromise, resolveCommentId }
110
114
  pendingTasks: new Map(),
115
+ // Track consecutive thinking-token events so high-frequency model progress
116
+ // edits one live comment instead of posting hundreds of PR comments.
117
+ activeThinking: null,
111
118
  // Issue #1472: Diagnostic counters for tracking comment posting success/failure
112
119
  eventsProcessed: 0,
113
120
  commentsAttempted: 0,
@@ -190,10 +197,11 @@ export const createInteractiveHandler = options => {
190
197
  * @param {string} body - Comment body
191
198
  * @param {string} [toolId] - Optional tool ID for tracking pending tool calls
192
199
  * @param {string} [taskId] - Optional task ID for tracking pending agent tasks
200
+ * @param {Function} [onPosted] - Optional callback invoked with the posted comment ID
193
201
  * @returns {Promise<string|null>} Comment ID if successful, null if queued or failed
194
202
  * @private
195
203
  */
196
- const postComment = async (body, toolId = null, taskId = null) => {
204
+ const postComment = async (body, toolId = null, taskId = null, onPosted = null) => {
197
205
  if (!prNumber || !owner || !repo) {
198
206
  if (verbose) {
199
207
  await log('⚠️ Interactive mode: Cannot post comment - missing PR info', { verbose: true });
@@ -210,7 +218,7 @@ export const createInteractiveHandler = options => {
210
218
 
211
219
  if (timeSinceLastComment < CONFIG.MIN_COMMENT_INTERVAL) {
212
220
  // Queue the comment for later with toolId/taskId for tracking
213
- state.commentQueue.push({ body: safeBody, toolId, taskId });
221
+ state.commentQueue.push({ body: safeBody, toolId, taskId, onPosted });
214
222
  if (verbose) {
215
223
  await log(`📝 Interactive mode: Comment queued (${state.commentQueue.length} in queue)${toolId ? ` [tool: ${toolId}]` : ''}${taskId ? ` [task: ${taskId}]` : ''}`, { verbose: true });
216
224
  }
@@ -247,6 +255,16 @@ export const createInteractiveHandler = options => {
247
255
  // AI-authored-comment check. Tracking is a no-op when commentId is null.
248
256
  trackToolCommentId(commentId);
249
257
 
258
+ if (commentId && typeof onPosted === 'function') {
259
+ try {
260
+ await onPosted(commentId);
261
+ } catch (error) {
262
+ if (verbose) {
263
+ await log(`⚠️ Interactive mode: post-comment callback failed for ${commentId}: ${error.message}`, { verbose: true });
264
+ }
265
+ }
266
+ }
267
+
250
268
  if (verbose) {
251
269
  await log(`✅ Interactive mode: Comment posted${commentId ? ` (ID: ${commentId})` : ''} (body: ${safeBody.length} chars)`, { verbose: true });
252
270
  }
@@ -325,9 +343,9 @@ export const createInteractiveHandler = options => {
325
343
 
326
344
  const queueItem = state.commentQueue.shift();
327
345
  if (queueItem) {
328
- const { body, toolId, taskId } = queueItem;
346
+ const { body, toolId, taskId, onPosted } = queueItem;
329
347
  // Post the comment (don't pass toolId/taskId to avoid re-queueing)
330
- const commentId = await postComment(body);
348
+ const commentId = await postComment(body, null, null, onPosted);
331
349
 
332
350
  // If this was a tool use comment, update the pending call with the comment ID
333
351
  if (toolId && commentId) {
@@ -1092,6 +1110,20 @@ ${createRawJsonSection(data)}`;
1092
1110
  }
1093
1111
  };
1094
1112
 
1113
+ const { handleThinkingTokens, finalizeThinkingGroup, handleSystemStatus, handleCompactBoundary, handleTaskUpdated } = createSystemLifecycleHandlers({
1114
+ state,
1115
+ owner,
1116
+ repo,
1117
+ prNumber,
1118
+ log,
1119
+ verbose,
1120
+ postComment,
1121
+ editComment,
1122
+ processQueue,
1123
+ handleTaskProgress,
1124
+ handleTaskNotification,
1125
+ });
1126
+
1095
1127
  /**
1096
1128
  * Handle rate_limit_event (silently logged, no comment created)
1097
1129
  * @param {Object} data - Event data
@@ -1104,153 +1136,12 @@ ${createRawJsonSection(data)}`;
1104
1136
  }
1105
1137
  };
1106
1138
 
1107
- const handleCodexThreadStarted = async data => {
1108
- if (state.sessionId) return;
1109
-
1110
- state.sessionId = data.thread_id || data.session_id || null;
1111
- state.startTime = Date.now();
1112
-
1113
- const comment = `## 🚀 ${INTERACTIVE_SESSION_STARTED_MARKER}
1114
-
1115
- | Property | Value |
1116
- |----------|-------|
1117
- | **Session ID** | \`${state.sessionId || 'unknown'}\` |
1118
- | **Model** | \`${data.model || 'unknown'}\` |
1119
- | **Tool** | \`codex\` |
1120
-
1121
- ---
1122
-
1123
- ${createRawJsonSection(data)}`;
1124
-
1125
- await postComment(comment);
1126
- };
1127
-
1128
- const handleCodexAgentMessage = async data => {
1129
- const text = data.item?.text;
1130
- if (typeof text !== 'string' || !text.trim()) return;
1131
- await handleAssistantText(data, text);
1132
- };
1133
-
1134
- const handleCodexTodoList = async data => {
1135
- const items = Array.isArray(data.item?.items) ? data.item.items : [];
1136
- const todosPreview = items.length > 0 ? items.map(todo => `- [${todo?.completed ? 'x' : ' '}] ${todo?.text || ''}`).join('\n') : '_No tasks_';
1137
- const completedCount = items.filter(todo => todo?.completed).length;
1138
-
1139
- const comment = `## 📋 Codex todo list
1140
-
1141
- ${createCollapsible(`📋 Todos (${completedCount}/${items.length} items)`, todosPreview, true)}
1142
-
1143
- ---
1144
-
1145
- ${createRawJsonSection(data)}`;
1146
-
1147
- await postComment(comment);
1148
- };
1149
-
1150
- const handleCodexCommandExecution = async data => {
1151
- const item = data.item || {};
1152
- const command = item.command || '';
1153
- const output = item.aggregated_output || '';
1154
- const status = item.status || (data.type === 'item.completed' ? 'completed' : data.type === 'item.updated' ? 'updated' : 'started');
1155
- const body = `## 💻 Codex command execution
1156
-
1157
- **Status:** \`${status}\`
1158
- ${command ? '\n' + createCollapsible('📋 Executed command', '```bash\n' + escapeMarkdown(command) + '\n```', true) : ''}
1159
- ${output ? '\n\n' + createCollapsible('📤 Output', '```\n' + escapeMarkdown(truncateMiddle(output, { maxLines: 60, keepStart: 25, keepEnd: 25 })) + '\n```', true) : ''}
1160
-
1161
- ---
1162
-
1163
- ${createRawJsonSection(data)}`;
1164
- await postComment(body);
1165
- };
1166
-
1167
- const handleCodexMcpToolCall = async data => {
1168
- const item = data.item || {};
1169
- const summary = [`**Server:** \`${item.server || 'unknown'}\``, `**Tool:** \`${item.tool || 'unknown'}\``, `**Status:** \`${item.status || 'unknown'}\``].join('\n');
1170
- const details = item.arguments != null ? createCollapsible('📥 Arguments', '```json\n' + safeJsonStringify(item.arguments, 2) + '\n```', true) : '';
1171
- // Issue #1843: render MCP-result images inline; base64 is redacted from JSON below.
1172
- const imagesSection = await imageRenderer.section([item.result], `${item.tool || 'mcp'} image`);
1173
- const imagesBlock = imagesSection ? '\n\n' + imagesSection : '';
1174
- const resultSection = item.result != null ? '\n\n' + createCollapsible('📤 Result', '```json\n' + safeJsonStringify(redactImageData(item.result), 2) + '\n```', false) : '';
1175
- const errorSection = item.error != null ? '\n\n' + createCollapsible('❌ Error', '```json\n' + safeJsonStringify(item.error, 2) + '\n```', true) : '';
1176
-
1177
- await postComment(`## 🔌 Codex MCP tool call
1178
-
1179
- ${summary}
1180
- ${details}${resultSection}${imagesBlock}${errorSection}
1181
-
1182
- ---
1183
-
1184
- ${createRedactedRawJsonSection(data)}`);
1185
- };
1186
-
1187
- const handleCodexWebSearch = async data => {
1188
- const item = data.item || {};
1189
- await postComment(`## 🌐 Codex web search
1190
-
1191
- **Query:** ${escapeMarkdown(item.query || 'unknown')}
1192
- ${item.action ? `\n**Action:** \`${item.action}\`` : ''}
1193
-
1194
- ---
1195
-
1196
- ${createRawJsonSection(data)}`);
1197
- };
1198
-
1199
- const handleCodexFileChange = async data => {
1200
- const item = data.item || {};
1201
- const changes = Array.isArray(item.changes) ? item.changes.map(change => `- \`${change?.kind || 'change'}\` ${change?.path || ''}`).join('\n') : '_No changes listed_';
1202
- await postComment(`## 📝 Codex file changes
1203
-
1204
- **Status:** \`${item.status || 'unknown'}\`
1205
- ${createCollapsible('📄 Files', changes, true)}
1206
-
1207
- ---
1208
-
1209
- ${createRawJsonSection(data)}`);
1210
- };
1211
-
1212
- const handleCodexCollabToolCall = async data => {
1213
- const item = data.item || {};
1214
- const prompt = item.prompt || item.description || `${item.tool || 'collab_tool_call'} via codex`;
1215
- await postComment(`## 🤝 Codex collab/sub-agent call
1216
-
1217
- **Tool:** \`${item.tool || 'unknown'}\`
1218
- **Status:** \`${item.status || 'unknown'}\`
1219
- ${createCollapsible('📝 Prompt', escapeMarkdown(truncateMiddle(prompt, { maxLines: 30, keepStart: 12, keepEnd: 12 })), true)}
1220
-
1221
- ---
1222
-
1223
- ${createRawJsonSection(data)}`);
1224
- };
1225
-
1226
- const handleCodexTurnCompleted = async data => {
1227
- const usage = data.usage || {};
1228
- let usageSection = '| Type | Count |\n|------|-------|\n';
1229
- usageSection += `| Input | ${(usage.input_tokens || 0).toLocaleString()} |\n`;
1230
- usageSection += `| Cache Read | ${(usage.cached_input_tokens || 0).toLocaleString()} |\n`;
1231
- usageSection += `| Output | ${(usage.output_tokens || 0).toLocaleString()} |\n`;
1232
-
1233
- await postComment(`## ✅ Codex turn completed
1234
-
1235
- ### 📊 Token Usage
1236
-
1237
- ${usageSection}
1238
-
1239
- ---
1240
-
1241
- ${createRawJsonSection(data)}`);
1242
- };
1243
-
1244
- const handleCodexError = async data => {
1245
- const message = data.message || data.error?.message || 'Unknown Codex error';
1246
- await postComment(`## ❌ Codex error
1247
-
1248
- ${createCollapsible('View error', escapeMarkdown(message), true)}
1249
-
1250
- ---
1251
-
1252
- ${createRawJsonSection(data)}`);
1253
- };
1139
+ const { handleCodexThreadStarted, handleCodexAgentMessage, handleCodexTodoList, handleCodexCommandExecution, handleCodexMcpToolCall, handleCodexWebSearch, handleCodexFileChange, handleCodexCollabToolCall, handleCodexTurnCompleted, handleCodexError } = createCodexEventHandlers({
1140
+ state,
1141
+ postComment,
1142
+ handleAssistantText,
1143
+ imageRenderer,
1144
+ });
1254
1145
 
1255
1146
  /**
1256
1147
  * Handle unrecognized event types
@@ -1285,6 +1176,11 @@ ${createRawJsonSection(data)}`;
1285
1176
  }
1286
1177
  state.eventsProcessed++;
1287
1178
 
1179
+ const isThinkingTokenEvent = data.type === 'system' && data.subtype === 'thinking_tokens';
1180
+ if (!isThinkingTokenEvent) {
1181
+ await finalizeThinkingGroup();
1182
+ }
1183
+
1288
1184
  // Handle events without type as unrecognized
1289
1185
  if (!data.type) {
1290
1186
  await handleUnrecognized(data);
@@ -1301,6 +1197,14 @@ ${createRawJsonSection(data)}`;
1301
1197
  await handleTaskProgress(data);
1302
1198
  } else if (data.subtype === 'task_notification') {
1303
1199
  await handleTaskNotification(data);
1200
+ } else if (data.subtype === 'thinking_tokens') {
1201
+ await handleThinkingTokens(data);
1202
+ } else if (data.subtype === 'status') {
1203
+ await handleSystemStatus(data);
1204
+ } else if (data.subtype === 'compact_boundary') {
1205
+ await handleCompactBoundary(data);
1206
+ } else if (data.subtype === 'task_updated') {
1207
+ await handleTaskUpdated(data);
1304
1208
  } else {
1305
1209
  // Unknown system subtype
1306
1210
  await handleUnrecognized(data);
@@ -1392,6 +1296,7 @@ ${createRawJsonSection(data)}`;
1392
1296
  * @returns {Promise<void>}
1393
1297
  */
1394
1298
  const flush = async () => {
1299
+ await finalizeThinkingGroup();
1395
1300
  await processQueue();
1396
1301
  };
1397
1302
 
@@ -1417,6 +1322,11 @@ ${createRawJsonSection(data)}`;
1417
1322
  handleTaskStarted,
1418
1323
  handleTaskProgress,
1419
1324
  handleTaskNotification,
1325
+ handleThinkingTokens,
1326
+ finalizeThinkingGroup,
1327
+ handleSystemStatus,
1328
+ handleCompactBoundary,
1329
+ handleTaskUpdated,
1420
1330
  handleRateLimitEvent,
1421
1331
  handleUnrecognized,
1422
1332
  handleCodexThreadStarted,
@@ -7,6 +7,7 @@ export { sanitizeUnicode };
7
7
 
8
8
  export const CONFIG = {
9
9
  MIN_COMMENT_INTERVAL: 5000,
10
+ MIN_THINKING_COMMENT_UPDATE_INTERVAL: 60000,
10
11
  MAX_LINES_BEFORE_TRUNCATION: 50,
11
12
  LINES_TO_KEEP_START: 20,
12
13
  LINES_TO_KEEP_END: 20,
@@ -0,0 +1,224 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { CONFIG, createRawJsonSection, formatDuration, safeJsonStringify } from './interactive-mode.shared.lib.mjs';
4
+
5
+ const isFiniteNumber = value => typeof value === 'number' && Number.isFinite(value);
6
+
7
+ const formatNumber = value => (isFiniteNumber(value) ? value.toLocaleString() : 'unknown');
8
+
9
+ const formatSignedNumber = value => {
10
+ if (!isFiniteNumber(value)) return 'unknown';
11
+ return `${value > 0 ? '+' : ''}${value.toLocaleString()}`;
12
+ };
13
+
14
+ const pluralize = (count, singular, plural = `${singular}s`) => `${count} ${count === 1 ? singular : plural}`;
15
+
16
+ const formatThinkingDuration = ms => {
17
+ if (!isFiniteNumber(ms) || ms < 1000) return 'a moment';
18
+
19
+ const totalSeconds = Math.max(1, Math.round(ms / 1000));
20
+ if (totalSeconds < 60) return pluralize(totalSeconds, 'second');
21
+
22
+ const totalMinutes = Math.max(1, Math.round(totalSeconds / 60));
23
+ if (totalMinutes < 60) return pluralize(totalMinutes, 'minute');
24
+
25
+ const hours = Math.floor(totalMinutes / 60);
26
+ const minutes = totalMinutes % 60;
27
+ return [pluralize(hours, 'hour'), minutes > 0 ? pluralize(minutes, 'minute') : ''].filter(Boolean).join(' ');
28
+ };
29
+
30
+ const getThinkingStats = events => {
31
+ const latest = events[events.length - 1] || {};
32
+ const estimatedTokens = isFiniteNumber(latest.estimated_tokens) ? latest.estimated_tokens : null;
33
+ const totalDelta = events.reduce((sum, event) => (isFiniteNumber(event.estimated_tokens_delta) ? sum + event.estimated_tokens_delta : sum), 0);
34
+ const latestDelta = isFiniteNumber(latest.estimated_tokens_delta) ? latest.estimated_tokens_delta : null;
35
+ return { estimatedTokens, totalDelta, latestDelta };
36
+ };
37
+
38
+ const buildThinkingComment = (thinking, { final = false } = {}) => {
39
+ const events = thinking.events;
40
+ const stats = getThinkingStats(events);
41
+ const elapsedMs = Math.max(0, thinking.lastEventTime - thinking.firstEventTime);
42
+ const header = final ? `## 🧠 Thought for ${formatThinkingDuration(elapsedMs)}.` : '## 🧠 Thinking...';
43
+ const summary = [`${formatNumber(stats.estimatedTokens)} estimated thinking tokens`, pluralize(events.length, 'thinking-token event'), `${formatSignedNumber(stats.totalDelta)} total delta`].join(' | ');
44
+ const latestDelta = stats.latestDelta == null ? 'unknown' : formatSignedNumber(stats.latestDelta);
45
+
46
+ return `${header}
47
+
48
+ ${summary}
49
+
50
+ | Metric | Value |
51
+ |--------|-------|
52
+ | **Estimated tokens** | ${formatNumber(stats.estimatedTokens)} |
53
+ | **Latest delta** | ${latestDelta} |
54
+ | **Events grouped** | ${events.length.toLocaleString()} |
55
+ | **Elapsed** | ${formatDuration(elapsedMs)} |
56
+
57
+ ---
58
+
59
+ ${createRawJsonSection(events)}`;
60
+ };
61
+
62
+ const waitForCommentId = async commentIdPromise => {
63
+ let timeoutId;
64
+ const timeoutPromise = new Promise(resolve => {
65
+ timeoutId = setTimeout(() => resolve(null), 15000);
66
+ });
67
+ const commentId = await Promise.race([commentIdPromise, timeoutPromise]);
68
+ clearTimeout(timeoutId);
69
+ return commentId;
70
+ };
71
+
72
+ export const createSystemLifecycleHandlers = ({ state, owner, repo, prNumber, log, verbose, postComment, editComment, processQueue, handleTaskProgress, handleTaskNotification }) => {
73
+ const resolveThinkingCommentId = async thinking => {
74
+ if (thinking.commentId) return thinking.commentId;
75
+ if (!prNumber || !owner || !repo) return null;
76
+
77
+ if (state.commentQueue.length > 0) {
78
+ const wasProcessing = state.isProcessing;
79
+ state.isProcessing = false;
80
+ await processQueue();
81
+ state.isProcessing = wasProcessing;
82
+ }
83
+
84
+ if (thinking.commentId) return thinking.commentId;
85
+ if (!thinking.commentIdPromise) return null;
86
+ return waitForCommentId(thinking.commentIdPromise);
87
+ };
88
+
89
+ const rememberThinkingComment = thinking => commentId => {
90
+ thinking.commentId = commentId;
91
+ if (thinking.resolveCommentId) thinking.resolveCommentId(commentId);
92
+ };
93
+
94
+ const handleThinkingTokens = async data => {
95
+ const now = Date.now();
96
+ let thinking = state.activeThinking;
97
+
98
+ if (!thinking) {
99
+ let resolveCommentId;
100
+ const commentIdPromise = new Promise(resolve => {
101
+ resolveCommentId = resolve;
102
+ });
103
+ thinking = {
104
+ commentId: null,
105
+ commentIdPromise,
106
+ resolveCommentId,
107
+ events: [],
108
+ firstEventTime: now,
109
+ lastEventTime: now,
110
+ lastEditTime: now,
111
+ };
112
+ state.activeThinking = thinking;
113
+ }
114
+
115
+ thinking.events.push(data);
116
+ thinking.lastEventTime = now;
117
+
118
+ if (thinking.events.length === 1) {
119
+ const rememberComment = rememberThinkingComment(thinking);
120
+ const commentId = await postComment(buildThinkingComment(thinking), null, null, rememberComment);
121
+ if (commentId) rememberComment(commentId);
122
+ if (verbose) {
123
+ await log(`🧠 Interactive mode: Thinking started (${formatNumber(data.estimated_tokens)} estimated tokens)`, { verbose: true });
124
+ }
125
+ return;
126
+ }
127
+
128
+ if (now - thinking.lastEditTime >= CONFIG.MIN_THINKING_COMMENT_UPDATE_INTERVAL) {
129
+ const commentId = await resolveThinkingCommentId(thinking);
130
+ if (commentId) {
131
+ await editComment(commentId, buildThinkingComment(thinking));
132
+ thinking.lastEditTime = now;
133
+ }
134
+ }
135
+ };
136
+
137
+ const finalizeThinkingGroup = async () => {
138
+ const thinking = state.activeThinking;
139
+ if (!thinking) return;
140
+
141
+ state.activeThinking = null;
142
+ const commentId = await resolveThinkingCommentId(thinking);
143
+ if (commentId) await editComment(commentId, buildThinkingComment(thinking, { final: true }));
144
+
145
+ if (verbose) {
146
+ await log(`🧠 Interactive mode: Thinking finished after ${formatThinkingDuration(thinking.lastEventTime - thinking.firstEventTime)} (${thinking.events.length} events)`, {
147
+ verbose: true,
148
+ });
149
+ }
150
+ };
151
+
152
+ const handleSystemStatus = async data => {
153
+ if (verbose) await log(`ℹ️ Interactive mode: System status ${data.status || 'unknown'}`, { verbose: true });
154
+ };
155
+
156
+ const handleCompactBoundary = async data => {
157
+ const metadata = data.compact_metadata || {};
158
+ const trigger = metadata.trigger || 'unknown';
159
+ const preTokens = isFiniteNumber(metadata.pre_tokens) ? metadata.pre_tokens : null;
160
+ const postTokens = isFiniteNumber(metadata.post_tokens) ? metadata.post_tokens : null;
161
+ const reduction = preTokens != null && postTokens != null ? preTokens - postTokens : null;
162
+ const durationText = isFiniteNumber(metadata.duration_ms) ? formatDuration(metadata.duration_ms) : 'unknown';
163
+
164
+ await postComment(`## 🧭 Context compacted
165
+
166
+ | Metric | Value |
167
+ |--------|-------|
168
+ | **Trigger** | \`${trigger}\` |
169
+ | **Before** | ${formatNumber(preTokens)} tokens |
170
+ | **After** | ${formatNumber(postTokens)} tokens |
171
+ | **Reduction** | ${formatNumber(reduction)} tokens |
172
+ | **Duration** | ${durationText} |
173
+
174
+ ---
175
+
176
+ ${createRawJsonSection(data)}`);
177
+ };
178
+
179
+ const handleTaskUpdated = async data => {
180
+ const taskId = data.task_id;
181
+ const patch = data.patch && typeof data.patch === 'object' ? data.patch : {};
182
+ const pendingTask = state.pendingTasks.get(taskId);
183
+
184
+ if (!pendingTask) {
185
+ if (verbose) {
186
+ await log(`🤖 Interactive mode: Task update for unknown task ${taskId || 'unknown'}: ${safeJsonStringify(patch)}`, { verbose: true });
187
+ }
188
+ return;
189
+ }
190
+
191
+ const status = patch.status || data.status;
192
+ if (status) {
193
+ await handleTaskNotification({
194
+ ...data,
195
+ status,
196
+ summary: data.summary || data.description || patch.summary || `Task ${status}`,
197
+ usage: data.usage || patch.usage || {},
198
+ });
199
+ return;
200
+ }
201
+
202
+ if (patch.description || data.description || patch.last_tool_name || data.last_tool_name || patch.usage || data.usage) {
203
+ await handleTaskProgress({
204
+ ...data,
205
+ subtype: 'task_progress',
206
+ description: patch.description || data.description || pendingTask.lastProgressDescription || pendingTask.description,
207
+ usage: data.usage || patch.usage || {},
208
+ last_tool_name: patch.last_tool_name || data.last_tool_name || '',
209
+ });
210
+ return;
211
+ }
212
+
213
+ pendingTask.allEvents.push(data);
214
+ if (verbose) await log(`🤖 Interactive mode: Task update recorded for ${taskId}: ${safeJsonStringify(patch)}`, { verbose: true });
215
+ };
216
+
217
+ return {
218
+ handleThinkingTokens,
219
+ finalizeThinkingGroup,
220
+ handleSystemStatus,
221
+ handleCompactBoundary,
222
+ handleTaskUpdated,
223
+ };
224
+ };
@@ -1,3 +1,4 @@
1
+ import { ensureUseM } from './use-m-bootstrap.lib.mjs';
1
2
  /**
2
3
  * Isolation Runner for Telegram bot
3
4
  *
@@ -19,7 +20,7 @@ import os from 'node:os';
19
20
  import path from 'node:path';
20
21
 
21
22
  if (typeof use === 'undefined') {
22
- globalThis.use = (await eval(await (await fetch('https://unpkg.com/use-m/use.js')).text())).use;
23
+ await ensureUseM();
23
24
  }
24
25
 
25
26
  const { $ } = await use('command-stream');
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import { ensureUseM } from './use-m-bootstrap.lib.mjs';
2
3
 
3
4
  /**
4
5
  * lenv-reader.lib.mjs - LINO-based environment configuration reader
@@ -30,7 +31,7 @@
30
31
  */
31
32
 
32
33
  if (typeof use === 'undefined') {
33
- globalThis.use = (await eval(await (await fetch('https://unpkg.com/use-m/use.js')).text())).use;
34
+ await ensureUseM();
34
35
  }
35
36
 
36
37
  const linoModule = await use('links-notation');
package/src/lib.mjs CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import { ensureUseM } from './use-m-bootstrap.lib.mjs';
2
3
 
3
4
  // Shared library functions for hive-mind project
4
5
 
@@ -32,7 +33,7 @@ try {
32
33
  // Check if use is already defined (when imported from solve.mjs)
33
34
  // If not, fetch it (when running standalone)
34
35
  if (typeof globalThis.use === 'undefined') {
35
- globalThis.use = (await eval(await (await fetch('https://unpkg.com/use-m/use.js')).text())).use;
36
+ await ensureUseM();
36
37
  }
37
38
 
38
39
  const fs = (await use('fs')).promises;
package/src/lino.lib.mjs CHANGED
@@ -1,5 +1,6 @@
1
+ import { ensureUseM } from './use-m-bootstrap.lib.mjs';
1
2
  if (typeof use === 'undefined') {
2
- globalThis.use = (await eval(await (await fetch('https://unpkg.com/use-m/use.js')).text())).use;
3
+ await ensureUseM();
3
4
  }
4
5
 
5
6
  // Issue #1710: hosted CI npm-install flake — retry once on a corrupt install.
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import { ensureUseM } from './use-m-bootstrap.lib.mjs';
2
3
 
3
4
  /**
4
5
  * Local CI Checks Module
@@ -6,7 +7,7 @@
6
7
  */
7
8
 
8
9
  if (typeof globalThis.use === 'undefined') {
9
- globalThis.use = (await eval(await (await fetch('https://unpkg.com/use-m/use.js')).text())).use;
10
+ await ensureUseM();
10
11
  }
11
12
 
12
13
  const { $ } = await use('command-stream');
@@ -1,11 +1,12 @@
1
1
  #!/usr/bin/env node
2
+ import { ensureUseM } from './use-m-bootstrap.lib.mjs';
2
3
 
3
4
  // Log upload module for hive-mind
4
5
  // Uses gh-upload-log for uploading log files to GitHub
5
6
 
6
7
  // Use use-m to dynamically import modules for cross-runtime compatibility
7
8
  if (typeof globalThis.use === 'undefined') {
8
- globalThis.use = (await eval(await (await fetch('https://unpkg.com/use-m/use.js')).text())).use;
9
+ await ensureUseM();
9
10
  }
10
11
  const use = globalThis.use;
11
12
 
@@ -1,9 +1,10 @@
1
1
  #!/usr/bin/env node
2
+ import { ensureUseM } from './use-m-bootstrap.lib.mjs';
2
3
 
3
4
  // Check if use is already defined (when imported from solve.mjs)
4
5
  // If not, fetch it (when running standalone)
5
6
  if (typeof globalThis.use === 'undefined') {
6
- globalThis.use = (await eval(await (await fetch('https://unpkg.com/use-m/use.js')).text())).use;
7
+ await ensureUseM();
7
8
  }
8
9
  const use = globalThis.use;
9
10
  const { getLinoYargsFactory, hideBin, parseCliArgumentsWithLino } = await import('./cli-arguments.lib.mjs');
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import { ensureUseM } from '../use-m-bootstrap.lib.mjs';
2
3
 
3
4
  /**
4
5
  * Unified models module for hive-mind
@@ -18,7 +19,7 @@ import { promisify } from 'node:util';
18
19
  // Check if use is already defined (when imported from solve.mjs)
19
20
  // If not, fetch it (when running standalone)
20
21
  if (typeof globalThis.use === 'undefined') {
21
- globalThis.use = (await eval(await (await fetch('https://unpkg.com/use-m/use.js')).text())).use;
22
+ await ensureUseM();
22
23
  }
23
24
 
24
25
  import { log } from '../lib.mjs';