@link-assistant/hive-mind 1.6.1 ā 1.6.3
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 +32 -1
- package/README.md +16 -0
- package/package.json +3 -2
- package/src/claude.lib.mjs +16 -160
- package/src/claude.runtime-switch.lib.mjs +175 -0
- package/src/limits.lib.mjs +11 -5
- package/src/telegram-bot.mjs +5 -1
- package/src/telegram-solve-queue.lib.mjs +61 -39
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,36 @@
|
|
|
1
1
|
# @link-assistant/hive-mind
|
|
2
2
|
|
|
3
|
+
## 1.6.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Fix Anthropic cost extraction from JSON stream when session has error_during_execution
|
|
8
|
+
- Added anthropicTotalCostUSD to all failure return paths in executeClaudeCommand
|
|
9
|
+
- Changed cost capture logic to only extract from `subtype === 'success'` results
|
|
10
|
+
- This is explicit and reliable - error_during_execution results have zero cost
|
|
11
|
+
- Added case study documentation for issue #1104
|
|
12
|
+
|
|
13
|
+
Fixes #1104
|
|
14
|
+
|
|
15
|
+
Synchronize line count checks in CI/CD
|
|
16
|
+
- Add ESLint max-lines rule (1500 lines) to match CI workflow check
|
|
17
|
+
- Extract handleClaudeRuntimeSwitch to claude.runtime-switch.lib.mjs
|
|
18
|
+
- Reduce claude.lib.mjs from 1506 to 1354 lines
|
|
19
|
+
- Add case study documentation for issue #1141
|
|
20
|
+
|
|
21
|
+
Fixes #1141
|
|
22
|
+
|
|
23
|
+
## 1.6.2
|
|
24
|
+
|
|
25
|
+
### Patch Changes
|
|
26
|
+
|
|
27
|
+
- 4ccbbd7: Fix CLAUDE_WEEKLY_THRESHOLD not enforcing one-at-a-time mode when external Claude processes are running
|
|
28
|
+
- Fixed oneAtATime mode to also consider externally running Claude processes (detected via pgrep), not just queue-internal processing
|
|
29
|
+
- Standardized all threshold comparisons to use >= (inclusive) instead of mixed > and >= operators
|
|
30
|
+
- Updated documentation comments to accurately reflect inclusive threshold behavior
|
|
31
|
+
- Added README recommendation to capture bot logs using tee for post-incident analysis
|
|
32
|
+
- Added case study documentation for issue #1133
|
|
33
|
+
|
|
3
34
|
## 1.6.1
|
|
4
35
|
|
|
5
36
|
### Patch Changes
|
|
@@ -461,7 +492,7 @@
|
|
|
461
492
|
- `RAM_THRESHOLD: 0.5` - Stop new commands if RAM usage > 50%
|
|
462
493
|
- `CPU_THRESHOLD: 0.5` - Stop new commands if CPU usage > 50%
|
|
463
494
|
- `DISK_THRESHOLD: 0.95` - One-at-a-time mode if disk usage > 95%
|
|
464
|
-
- `
|
|
495
|
+
- `CLAUDE_5_HOUR_SESSION_THRESHOLD: 0.9` - Stop if Claude 5-hour limit > 90%
|
|
465
496
|
- `CLAUDE_WEEKLY_THRESHOLD: 0.99` - One-at-a-time mode if weekly limit > 99%
|
|
466
497
|
- `GITHUB_API_THRESHOLD: 0.8` - Stop if GitHub API > 80% with parallel claude commands
|
|
467
498
|
- 1-minute minimum interval between command starts
|
package/README.md
CHANGED
|
@@ -409,10 +409,26 @@ Want to see the Hive Mind in action? Join our Telegram channel where you can exe
|
|
|
409
409
|
```
|
|
410
410
|
|
|
411
411
|
3. **Start the Bot**
|
|
412
|
+
|
|
412
413
|
```bash
|
|
413
414
|
hive-telegram-bot
|
|
414
415
|
```
|
|
415
416
|
|
|
417
|
+
**Recommended: Capture logs with tee**
|
|
418
|
+
|
|
419
|
+
When running the bot for extended periods, it's recommended to capture logs to a file using `tee`. This ensures you can review logs later even if the terminal buffer overflows:
|
|
420
|
+
|
|
421
|
+
```bash
|
|
422
|
+
hive-telegram-bot 2>&1 | tee -a logs/bot-$(date +%Y%m%d).log
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
Or create a logs directory and start with automatic log rotation:
|
|
426
|
+
|
|
427
|
+
```bash
|
|
428
|
+
mkdir -p logs
|
|
429
|
+
hive-telegram-bot 2>&1 | tee -a "logs/bot-$(date +%Y%m%d-%H%M%S).log"
|
|
430
|
+
```
|
|
431
|
+
|
|
416
432
|
### Bot Commands
|
|
417
433
|
|
|
418
434
|
All commands work in **group chats only** (not in private messages with the bot):
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@link-assistant/hive-mind",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.3",
|
|
4
4
|
"description": "AI-powered issue solver and hive mind for collaborative problem solving",
|
|
5
5
|
"main": "src/hive.mjs",
|
|
6
6
|
"type": "module",
|
|
@@ -13,8 +13,9 @@
|
|
|
13
13
|
"hive-telegram-bot": "./src/telegram-bot.mjs"
|
|
14
14
|
},
|
|
15
15
|
"scripts": {
|
|
16
|
-
"test": "node tests/solve-queue.test.mjs && node tests/test-usage-limit.mjs",
|
|
16
|
+
"test": "node tests/solve-queue.test.mjs && node tests/limits-display.test.mjs && node tests/test-usage-limit.mjs",
|
|
17
17
|
"test:queue": "node tests/solve-queue.test.mjs",
|
|
18
|
+
"test:limits-display": "node tests/limits-display.test.mjs",
|
|
18
19
|
"test:usage-limit": "node tests/test-usage-limit.mjs",
|
|
19
20
|
"lint": "eslint 'src/**/*.{js,mjs,cjs}'",
|
|
20
21
|
"lint:fix": "eslint 'src/**/*.{js,mjs,cjs}' --fix",
|
package/src/claude.lib.mjs
CHANGED
|
@@ -8,7 +8,7 @@ const { $ } = await use('command-stream');
|
|
|
8
8
|
const fs = (await use('fs')).promises;
|
|
9
9
|
const path = (await use('path')).default;
|
|
10
10
|
// Import log from general lib
|
|
11
|
-
import { log
|
|
11
|
+
import { log } from './lib.mjs';
|
|
12
12
|
import { reportError } from './sentry.lib.mjs';
|
|
13
13
|
import { timeouts, retryLimits, claudeCode, getClaudeEnv } from './config.lib.mjs';
|
|
14
14
|
import { detectUsageLimit, formatUsageLimitMessage } from './usage-limit.lib.mjs';
|
|
@@ -16,6 +16,8 @@ import { createInteractiveHandler } from './interactive-mode.lib.mjs';
|
|
|
16
16
|
import { displayBudgetStats } from './claude.budget-stats.lib.mjs';
|
|
17
17
|
// Import Claude command builder for generating resume commands
|
|
18
18
|
import { buildClaudeResumeCommand } from './claude.command-builder.lib.mjs';
|
|
19
|
+
// Import runtime switch module (extracted to maintain file line limits, see issue #1141)
|
|
20
|
+
import { handleClaudeRuntimeSwitch } from './claude.runtime-switch.lib.mjs';
|
|
19
21
|
|
|
20
22
|
// Helper to display resume command at end of session
|
|
21
23
|
const showResumeCommand = async (sessionId, tempDir, claudePath, model, log) => {
|
|
@@ -214,163 +216,9 @@ export const validateClaudeConnection = async (model = 'haiku-3') => {
|
|
|
214
216
|
// Start the validation with retry logic
|
|
215
217
|
return await attemptValidation();
|
|
216
218
|
};
|
|
217
|
-
//
|
|
218
|
-
export
|
|
219
|
-
|
|
220
|
-
await log('\nš§ Switching Claude runtime to bun...');
|
|
221
|
-
try {
|
|
222
|
-
try {
|
|
223
|
-
await $`which bun`;
|
|
224
|
-
await log(' ā
Bun runtime found');
|
|
225
|
-
} catch (bunError) {
|
|
226
|
-
reportError(bunError, {
|
|
227
|
-
context: 'claude.lib.mjs - bun availability check',
|
|
228
|
-
level: 'error',
|
|
229
|
-
});
|
|
230
|
-
await log('ā Bun runtime not found. Please install bun first: https://bun.sh/', { level: 'error' });
|
|
231
|
-
process.exit(1);
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
// Find Claude executable path
|
|
235
|
-
const claudePathResult = await $`which claude`;
|
|
236
|
-
const claudePath = claudePathResult.stdout.toString().trim();
|
|
237
|
-
|
|
238
|
-
if (!claudePath) {
|
|
239
|
-
await log('ā Claude executable not found', { level: 'error' });
|
|
240
|
-
process.exit(1);
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
await log(` Claude path: ${claudePath}`);
|
|
244
|
-
|
|
245
|
-
try {
|
|
246
|
-
await fs.access(claudePath, fs.constants.W_OK);
|
|
247
|
-
} catch (accessError) {
|
|
248
|
-
reportError(accessError, {
|
|
249
|
-
context: 'claude.lib.mjs - Claude executable write permission check (bun)',
|
|
250
|
-
level: 'error',
|
|
251
|
-
});
|
|
252
|
-
await log('ā Cannot write to Claude executable (permission denied)', { level: 'error' });
|
|
253
|
-
await log(' Try running with sudo or changing file permissions', { level: 'error' });
|
|
254
|
-
process.exit(1);
|
|
255
|
-
}
|
|
256
|
-
// Read current shebang
|
|
257
|
-
const firstLine = await $`head -1 "${claudePath}"`;
|
|
258
|
-
const currentShebang = firstLine.stdout.toString().trim();
|
|
259
|
-
await log(` Current shebang: ${currentShebang}`);
|
|
260
|
-
if (currentShebang.includes('bun')) {
|
|
261
|
-
await log(' ā
Claude is already configured to use bun');
|
|
262
|
-
process.exit(0);
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
// Create backup
|
|
266
|
-
const backupPath = `${claudePath}.nodejs-backup`;
|
|
267
|
-
await $`cp "${claudePath}" "${backupPath}"`;
|
|
268
|
-
await log(` š¦ Backup created: ${backupPath}`);
|
|
269
|
-
|
|
270
|
-
// Read file content and replace shebang
|
|
271
|
-
const content = await fs.readFile(claudePath, 'utf8');
|
|
272
|
-
const newContent = content.replace(/^#!.*node.*$/m, '#!/usr/bin/env bun');
|
|
273
|
-
|
|
274
|
-
if (content === newContent) {
|
|
275
|
-
await log('ā ļø No Node.js shebang found to replace', { level: 'warning' });
|
|
276
|
-
await log(` Current shebang: ${currentShebang}`, { level: 'warning' });
|
|
277
|
-
process.exit(0);
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
await fs.writeFile(claudePath, newContent);
|
|
281
|
-
await log(' ā
Claude shebang updated to use bun');
|
|
282
|
-
await log(' š Claude will now run with bun runtime');
|
|
283
|
-
} catch (error) {
|
|
284
|
-
await log(`ā Failed to switch Claude to bun: ${cleanErrorMessage(error)}`, { level: 'error' });
|
|
285
|
-
process.exit(1);
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
// Exit after switching runtime
|
|
289
|
-
process.exit(0);
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
if (argv['force-claude-nodejs-run']) {
|
|
293
|
-
await log('\nš§ Restoring Claude runtime to Node.js...');
|
|
294
|
-
try {
|
|
295
|
-
try {
|
|
296
|
-
await $`which node`;
|
|
297
|
-
await log(' ā
Node.js runtime found');
|
|
298
|
-
} catch (nodeError) {
|
|
299
|
-
reportError(nodeError, {
|
|
300
|
-
context: 'claude.lib.mjs - Node.js availability check',
|
|
301
|
-
level: 'error',
|
|
302
|
-
});
|
|
303
|
-
await log('ā Node.js runtime not found. Please install Node.js first', { level: 'error' });
|
|
304
|
-
process.exit(1);
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
// Find Claude executable path
|
|
308
|
-
const claudePathResult = await $`which claude`;
|
|
309
|
-
const claudePath = claudePathResult.stdout.toString().trim();
|
|
310
|
-
|
|
311
|
-
if (!claudePath) {
|
|
312
|
-
await log('ā Claude executable not found', { level: 'error' });
|
|
313
|
-
process.exit(1);
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
await log(` Claude path: ${claudePath}`);
|
|
317
|
-
|
|
318
|
-
try {
|
|
319
|
-
await fs.access(claudePath, fs.constants.W_OK);
|
|
320
|
-
} catch (accessError) {
|
|
321
|
-
reportError(accessError, {
|
|
322
|
-
context: 'claude.lib.mjs - Claude executable write permission check (nodejs)',
|
|
323
|
-
level: 'error',
|
|
324
|
-
});
|
|
325
|
-
await log('ā Cannot write to Claude executable (permission denied)', { level: 'error' });
|
|
326
|
-
await log(' Try running with sudo or changing file permissions', { level: 'error' });
|
|
327
|
-
process.exit(1);
|
|
328
|
-
}
|
|
329
|
-
// Read current shebang
|
|
330
|
-
const firstLine = await $`head -1 "${claudePath}"`;
|
|
331
|
-
const currentShebang = firstLine.stdout.toString().trim();
|
|
332
|
-
await log(` Current shebang: ${currentShebang}`);
|
|
333
|
-
if (currentShebang.includes('node') && !currentShebang.includes('bun')) {
|
|
334
|
-
await log(' ā
Claude is already configured to use Node.js');
|
|
335
|
-
process.exit(0);
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
const backupPath = `${claudePath}.nodejs-backup`;
|
|
339
|
-
try {
|
|
340
|
-
await fs.access(backupPath);
|
|
341
|
-
// Restore from backup
|
|
342
|
-
await $`cp "${backupPath}" "${claudePath}"`;
|
|
343
|
-
await log(` ā
Restored Claude from backup: ${backupPath}`);
|
|
344
|
-
} catch (backupError) {
|
|
345
|
-
reportError(backupError, {
|
|
346
|
-
context: 'claude_restore_backup',
|
|
347
|
-
level: 'info',
|
|
348
|
-
});
|
|
349
|
-
// No backup available, manually update shebang
|
|
350
|
-
await log(' š No backup found, manually updating shebang...');
|
|
351
|
-
const content = await fs.readFile(claudePath, 'utf8');
|
|
352
|
-
const newContent = content.replace(/^#!.*bun.*$/m, '#!/usr/bin/env node');
|
|
353
|
-
|
|
354
|
-
if (content === newContent) {
|
|
355
|
-
await log('ā ļø No bun shebang found to replace', { level: 'warning' });
|
|
356
|
-
await log(` Current shebang: ${currentShebang}`, { level: 'warning' });
|
|
357
|
-
process.exit(0);
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
await fs.writeFile(claudePath, newContent);
|
|
361
|
-
await log(' ā
Claude shebang updated to use Node.js');
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
await log(' š Claude will now run with Node.js runtime');
|
|
365
|
-
} catch (error) {
|
|
366
|
-
await log(`ā Failed to restore Claude to Node.js: ${cleanErrorMessage(error)}`, { level: 'error' });
|
|
367
|
-
process.exit(1);
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
// Exit after restoring runtime
|
|
371
|
-
process.exit(0);
|
|
372
|
-
}
|
|
373
|
-
};
|
|
219
|
+
// handleClaudeRuntimeSwitch is imported from ./claude.runtime-switch.lib.mjs (see issue #1141)
|
|
220
|
+
// Re-export it for backwards compatibility
|
|
221
|
+
export { handleClaudeRuntimeSwitch };
|
|
374
222
|
/**
|
|
375
223
|
* Check if Playwright MCP is available and connected to Claude
|
|
376
224
|
* @returns {Promise<boolean>} True if Playwright MCP is available, false otherwise
|
|
@@ -1038,9 +886,13 @@ export const executeClaudeCommand = async params => {
|
|
|
1038
886
|
// Handle session result type from Claude CLI (emitted when session completes)
|
|
1039
887
|
// Subtypes: "success", "error_during_execution" (work may have been done), etc.
|
|
1040
888
|
if (data.type === 'result') {
|
|
1041
|
-
|
|
889
|
+
// Issue #1104: Only extract cost from subtype 'success' results
|
|
890
|
+
// This is explicit and reliable - error_during_execution results have zero cost
|
|
891
|
+
if (data.subtype === 'success' && data.total_cost_usd !== undefined && data.total_cost_usd !== null) {
|
|
1042
892
|
anthropicTotalCostUSD = data.total_cost_usd;
|
|
1043
|
-
await log(`š° Anthropic official cost captured: $${anthropicTotalCostUSD.toFixed(6)}`, { verbose: true });
|
|
893
|
+
await log(`š° Anthropic official cost captured from success result: $${anthropicTotalCostUSD.toFixed(6)}`, { verbose: true });
|
|
894
|
+
} else if (data.total_cost_usd !== undefined && data.total_cost_usd !== null) {
|
|
895
|
+
await log(`š° Anthropic cost from ${data.subtype || 'unknown'} result ignored: $${data.total_cost_usd.toFixed(6)}`, { verbose: true });
|
|
1044
896
|
}
|
|
1045
897
|
if (data.is_error === true) {
|
|
1046
898
|
lastMessage = data.result || JSON.stringify(data);
|
|
@@ -1170,6 +1022,7 @@ export const executeClaudeCommand = async params => {
|
|
|
1170
1022
|
limitTimezone: null,
|
|
1171
1023
|
messageCount,
|
|
1172
1024
|
toolUseCount,
|
|
1025
|
+
anthropicTotalCostUSD, // Issue #1104: Include cost even on failure
|
|
1173
1026
|
};
|
|
1174
1027
|
}
|
|
1175
1028
|
}
|
|
@@ -1218,6 +1071,7 @@ export const executeClaudeCommand = async params => {
|
|
|
1218
1071
|
messageCount,
|
|
1219
1072
|
toolUseCount,
|
|
1220
1073
|
is503Error: true,
|
|
1074
|
+
anthropicTotalCostUSD, // Issue #1104: Include cost even on failure
|
|
1221
1075
|
};
|
|
1222
1076
|
}
|
|
1223
1077
|
}
|
|
@@ -1296,6 +1150,7 @@ export const executeClaudeCommand = async params => {
|
|
|
1296
1150
|
messageCount,
|
|
1297
1151
|
toolUseCount,
|
|
1298
1152
|
errorDuringExecution,
|
|
1153
|
+
anthropicTotalCostUSD, // Issue #1104: Include cost even on failure
|
|
1299
1154
|
};
|
|
1300
1155
|
}
|
|
1301
1156
|
// Issue #1088: If error_during_execution occurred but command didn't fail,
|
|
@@ -1414,6 +1269,7 @@ export const executeClaudeCommand = async params => {
|
|
|
1414
1269
|
limitTimezone: null,
|
|
1415
1270
|
messageCount,
|
|
1416
1271
|
toolUseCount,
|
|
1272
|
+
anthropicTotalCostUSD, // Issue #1104: Include cost even on failure
|
|
1417
1273
|
};
|
|
1418
1274
|
}
|
|
1419
1275
|
}; // End of executeWithRetry function
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Claude runtime switching module
|
|
3
|
+
// Extracted from claude.lib.mjs to maintain file line limits
|
|
4
|
+
// See: docs/case-studies/issue-1141
|
|
5
|
+
|
|
6
|
+
// If not, fetch it (when running standalone)
|
|
7
|
+
if (typeof globalThis.use === 'undefined') {
|
|
8
|
+
globalThis.use = (await eval(await (await fetch('https://unpkg.com/use-m/use.js')).text())).use;
|
|
9
|
+
}
|
|
10
|
+
const { $ } = await use('command-stream');
|
|
11
|
+
const fs = (await use('fs')).promises;
|
|
12
|
+
import { log, cleanErrorMessage } from './lib.mjs';
|
|
13
|
+
import { reportError } from './sentry.lib.mjs';
|
|
14
|
+
|
|
15
|
+
// Function to handle Claude runtime switching between Node.js and Bun
|
|
16
|
+
export const handleClaudeRuntimeSwitch = async argv => {
|
|
17
|
+
if (argv['force-claude-bun-run']) {
|
|
18
|
+
await log('\nš§ Switching Claude runtime to bun...');
|
|
19
|
+
try {
|
|
20
|
+
try {
|
|
21
|
+
await $`which bun`;
|
|
22
|
+
await log(' ā
Bun runtime found');
|
|
23
|
+
} catch (bunError) {
|
|
24
|
+
reportError(bunError, {
|
|
25
|
+
context: 'claude.runtime-switch.lib.mjs - bun availability check',
|
|
26
|
+
level: 'error',
|
|
27
|
+
});
|
|
28
|
+
await log('ā Bun runtime not found. Please install bun first: https://bun.sh/', { level: 'error' });
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Find Claude executable path
|
|
33
|
+
const claudePathResult = await $`which claude`;
|
|
34
|
+
const claudePath = claudePathResult.stdout.toString().trim();
|
|
35
|
+
|
|
36
|
+
if (!claudePath) {
|
|
37
|
+
await log('ā Claude executable not found', { level: 'error' });
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
await log(` Claude path: ${claudePath}`);
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
await fs.access(claudePath, fs.constants.W_OK);
|
|
45
|
+
} catch (accessError) {
|
|
46
|
+
reportError(accessError, {
|
|
47
|
+
context: 'claude.runtime-switch.lib.mjs - Claude executable write permission check (bun)',
|
|
48
|
+
level: 'error',
|
|
49
|
+
});
|
|
50
|
+
await log('ā Cannot write to Claude executable (permission denied)', { level: 'error' });
|
|
51
|
+
await log(' Try running with sudo or changing file permissions', { level: 'error' });
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
// Read current shebang
|
|
55
|
+
const firstLine = await $`head -1 "${claudePath}"`;
|
|
56
|
+
const currentShebang = firstLine.stdout.toString().trim();
|
|
57
|
+
await log(` Current shebang: ${currentShebang}`);
|
|
58
|
+
if (currentShebang.includes('bun')) {
|
|
59
|
+
await log(' ā
Claude is already configured to use bun');
|
|
60
|
+
process.exit(0);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Create backup
|
|
64
|
+
const backupPath = `${claudePath}.nodejs-backup`;
|
|
65
|
+
await $`cp "${claudePath}" "${backupPath}"`;
|
|
66
|
+
await log(` š¦ Backup created: ${backupPath}`);
|
|
67
|
+
|
|
68
|
+
// Read file content and replace shebang
|
|
69
|
+
const content = await fs.readFile(claudePath, 'utf8');
|
|
70
|
+
const newContent = content.replace(/^#!.*node.*$/m, '#!/usr/bin/env bun');
|
|
71
|
+
|
|
72
|
+
if (content === newContent) {
|
|
73
|
+
await log('ā ļø No Node.js shebang found to replace', { level: 'warning' });
|
|
74
|
+
await log(` Current shebang: ${currentShebang}`, { level: 'warning' });
|
|
75
|
+
process.exit(0);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
await fs.writeFile(claudePath, newContent);
|
|
79
|
+
await log(' ā
Claude shebang updated to use bun');
|
|
80
|
+
await log(' š Claude will now run with bun runtime');
|
|
81
|
+
} catch (error) {
|
|
82
|
+
await log(`ā Failed to switch Claude to bun: ${cleanErrorMessage(error)}`, { level: 'error' });
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Exit after switching runtime
|
|
87
|
+
process.exit(0);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (argv['force-claude-nodejs-run']) {
|
|
91
|
+
await log('\nš§ Restoring Claude runtime to Node.js...');
|
|
92
|
+
try {
|
|
93
|
+
try {
|
|
94
|
+
await $`which node`;
|
|
95
|
+
await log(' ā
Node.js runtime found');
|
|
96
|
+
} catch (nodeError) {
|
|
97
|
+
reportError(nodeError, {
|
|
98
|
+
context: 'claude.runtime-switch.lib.mjs - Node.js availability check',
|
|
99
|
+
level: 'error',
|
|
100
|
+
});
|
|
101
|
+
await log('ā Node.js runtime not found. Please install Node.js first', { level: 'error' });
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Find Claude executable path
|
|
106
|
+
const claudePathResult = await $`which claude`;
|
|
107
|
+
const claudePath = claudePathResult.stdout.toString().trim();
|
|
108
|
+
|
|
109
|
+
if (!claudePath) {
|
|
110
|
+
await log('ā Claude executable not found', { level: 'error' });
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
await log(` Claude path: ${claudePath}`);
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
await fs.access(claudePath, fs.constants.W_OK);
|
|
118
|
+
} catch (accessError) {
|
|
119
|
+
reportError(accessError, {
|
|
120
|
+
context: 'claude.runtime-switch.lib.mjs - Claude executable write permission check (nodejs)',
|
|
121
|
+
level: 'error',
|
|
122
|
+
});
|
|
123
|
+
await log('ā Cannot write to Claude executable (permission denied)', { level: 'error' });
|
|
124
|
+
await log(' Try running with sudo or changing file permissions', { level: 'error' });
|
|
125
|
+
process.exit(1);
|
|
126
|
+
}
|
|
127
|
+
// Read current shebang
|
|
128
|
+
const firstLine = await $`head -1 "${claudePath}"`;
|
|
129
|
+
const currentShebang = firstLine.stdout.toString().trim();
|
|
130
|
+
await log(` Current shebang: ${currentShebang}`);
|
|
131
|
+
if (currentShebang.includes('node') && !currentShebang.includes('bun')) {
|
|
132
|
+
await log(' ā
Claude is already configured to use Node.js');
|
|
133
|
+
process.exit(0);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const backupPath = `${claudePath}.nodejs-backup`;
|
|
137
|
+
try {
|
|
138
|
+
await fs.access(backupPath);
|
|
139
|
+
// Restore from backup
|
|
140
|
+
await $`cp "${backupPath}" "${claudePath}"`;
|
|
141
|
+
await log(` ā
Restored Claude from backup: ${backupPath}`);
|
|
142
|
+
} catch (backupError) {
|
|
143
|
+
reportError(backupError, {
|
|
144
|
+
context: 'claude_restore_backup',
|
|
145
|
+
level: 'info',
|
|
146
|
+
});
|
|
147
|
+
// No backup available, manually update shebang
|
|
148
|
+
await log(' š No backup found, manually updating shebang...');
|
|
149
|
+
const content = await fs.readFile(claudePath, 'utf8');
|
|
150
|
+
const newContent = content.replace(/^#!.*bun.*$/m, '#!/usr/bin/env node');
|
|
151
|
+
|
|
152
|
+
if (content === newContent) {
|
|
153
|
+
await log('ā ļø No bun shebang found to replace', { level: 'warning' });
|
|
154
|
+
await log(` Current shebang: ${currentShebang}`, { level: 'warning' });
|
|
155
|
+
process.exit(0);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
await fs.writeFile(claudePath, newContent);
|
|
159
|
+
await log(' ā
Claude shebang updated to use Node.js');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
await log(' š Claude will now run with Node.js runtime');
|
|
163
|
+
} catch (error) {
|
|
164
|
+
await log(`ā Failed to restore Claude to Node.js: ${cleanErrorMessage(error)}`, { level: 'error' });
|
|
165
|
+
process.exit(1);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Exit after restoring runtime
|
|
169
|
+
process.exit(0);
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
export default {
|
|
174
|
+
handleClaudeRuntimeSwitch,
|
|
175
|
+
};
|
package/src/limits.lib.mjs
CHANGED
|
@@ -720,8 +720,8 @@ export function formatUsageMessage(usage, diskSpace = null, githubRateLimit = nu
|
|
|
720
720
|
message += '\n';
|
|
721
721
|
}
|
|
722
722
|
|
|
723
|
-
//
|
|
724
|
-
message += '
|
|
723
|
+
// Claude 5 hour session (five_hour)
|
|
724
|
+
message += 'Claude 5 hour session\n';
|
|
725
725
|
if (usage.currentSession.percentage !== null) {
|
|
726
726
|
// Add time passed progress bar first
|
|
727
727
|
const timePassed = calculateTimePassedPercentage(usage.currentSession.resetsAt, 5);
|
|
@@ -731,7 +731,9 @@ export function formatUsageMessage(usage, diskSpace = null, githubRateLimit = nu
|
|
|
731
731
|
}
|
|
732
732
|
|
|
733
733
|
// Add usage progress bar second
|
|
734
|
-
|
|
734
|
+
// Use Math.floor so 100% only appears when usage is exactly 100%
|
|
735
|
+
// See: https://github.com/link-assistant/hive-mind/issues/1133
|
|
736
|
+
const pct = Math.floor(usage.currentSession.percentage);
|
|
735
737
|
const bar = getProgressBar(pct);
|
|
736
738
|
message += `${bar} ${pct}% used\n`;
|
|
737
739
|
|
|
@@ -759,7 +761,9 @@ export function formatUsageMessage(usage, diskSpace = null, githubRateLimit = nu
|
|
|
759
761
|
}
|
|
760
762
|
|
|
761
763
|
// Add usage progress bar second
|
|
762
|
-
|
|
764
|
+
// Use Math.floor so 100% only appears when usage is exactly 100%
|
|
765
|
+
// See: https://github.com/link-assistant/hive-mind/issues/1133
|
|
766
|
+
const pct = Math.floor(usage.allModels.percentage);
|
|
763
767
|
const bar = getProgressBar(pct);
|
|
764
768
|
message += `${bar} ${pct}% used\n`;
|
|
765
769
|
|
|
@@ -787,7 +791,9 @@ export function formatUsageMessage(usage, diskSpace = null, githubRateLimit = nu
|
|
|
787
791
|
}
|
|
788
792
|
|
|
789
793
|
// Add usage progress bar second
|
|
790
|
-
|
|
794
|
+
// Use Math.floor so 100% only appears when usage is exactly 100%
|
|
795
|
+
// See: https://github.com/link-assistant/hive-mind/issues/1133
|
|
796
|
+
const pct = Math.floor(usage.sonnetOnly.percentage);
|
|
791
797
|
const bar = getProgressBar(pct);
|
|
792
798
|
message += `${bar} ${pct}% used\n`;
|
|
793
799
|
|
package/src/telegram-bot.mjs
CHANGED
|
@@ -858,9 +858,13 @@ bot.command('limits', async ctx => {
|
|
|
858
858
|
const solveQueue = getSolveQueue({ verbose: VERBOSE });
|
|
859
859
|
const queueStats = solveQueue.getStats();
|
|
860
860
|
const claudeProcs = await getRunningClaudeProcesses(VERBOSE);
|
|
861
|
+
// Calculate total processing: queue-internal + external claude processes
|
|
862
|
+
// This provides a uniform view of all processing happening
|
|
863
|
+
// See: https://github.com/link-assistant/hive-mind/issues/1133
|
|
864
|
+
const totalProcessing = queueStats.processing + claudeProcs.count;
|
|
861
865
|
const codeBlockEnd = message.lastIndexOf('```');
|
|
862
866
|
if (codeBlockEnd !== -1) {
|
|
863
|
-
const queueStatus = queueStats.queued > 0 ||
|
|
867
|
+
const queueStatus = queueStats.queued > 0 || totalProcessing > 0 ? `Pending: ${queueStats.queued}, Processing: ${totalProcessing}` : 'Empty (no pending commands)';
|
|
864
868
|
message = message.slice(0, codeBlockEnd) + `\nSolve Queue\n${queueStatus}\nClaude processes: ${claudeProcs.count}\n` + message.slice(codeBlockEnd);
|
|
865
869
|
}
|
|
866
870
|
await ctx.telegram.editMessageText(fetchingMessage.chat.id, fetchingMessage.message_id, undefined, message, { parse_mode: 'Markdown' });
|
|
@@ -33,15 +33,17 @@ import { getCachedClaudeLimits, getCachedGitHubLimits, getCachedMemoryInfo, getC
|
|
|
33
33
|
*/
|
|
34
34
|
export const QUEUE_CONFIG = {
|
|
35
35
|
// Resource thresholds (usage ratios: 0.0 - 1.0)
|
|
36
|
-
|
|
36
|
+
// All thresholds use >= comparison (inclusive)
|
|
37
|
+
RAM_THRESHOLD: 0.5, // Stop if RAM usage >= 50%
|
|
37
38
|
// CPU threshold uses 5-minute load average, not instantaneous CPU usage
|
|
38
|
-
CPU_THRESHOLD: 0.5, // Stop if 5-minute load average
|
|
39
|
-
DISK_THRESHOLD: 0.95, // One-at-a-time if disk usage
|
|
39
|
+
CPU_THRESHOLD: 0.5, // Stop if 5-minute load average >= 50% of CPU count
|
|
40
|
+
DISK_THRESHOLD: 0.95, // One-at-a-time if disk usage >= 95%
|
|
40
41
|
|
|
41
42
|
// API limit thresholds (usage ratios: 0.0 - 1.0)
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
// All thresholds use >= comparison (inclusive)
|
|
44
|
+
CLAUDE_5_HOUR_SESSION_THRESHOLD: 0.9, // Stop if 5-hour limit >= 90%
|
|
45
|
+
CLAUDE_WEEKLY_THRESHOLD: 0.99, // One-at-a-time if weekly limit >= 99%
|
|
46
|
+
GITHUB_API_THRESHOLD: 0.8, // Stop if GitHub >= 80% with parallel claude
|
|
45
47
|
|
|
46
48
|
// Timing
|
|
47
49
|
// MIN_START_INTERVAL_MS: Time to allow solve command to start actual claude process
|
|
@@ -135,8 +137,8 @@ function formatWaitingReason(metric, currentValue, threshold) {
|
|
|
135
137
|
return `CPU usage is ${currentPercent}% (threshold: ${thresholdPercent})`;
|
|
136
138
|
case 'disk':
|
|
137
139
|
return `Disk usage is ${currentPercent}% (threshold: ${thresholdPercent})`;
|
|
138
|
-
case '
|
|
139
|
-
return `Claude session limit is ${currentPercent}% (threshold: ${thresholdPercent})`;
|
|
140
|
+
case 'claude_5_hour_session':
|
|
141
|
+
return `Claude 5 hour session limit is ${currentPercent}% (threshold: ${thresholdPercent})`;
|
|
140
142
|
case 'claude_weekly':
|
|
141
143
|
return `Claude weekly limit is ${currentPercent}% (threshold: ${thresholdPercent})`;
|
|
142
144
|
case 'github':
|
|
@@ -394,22 +396,25 @@ export class SolveQueue {
|
|
|
394
396
|
const claudeProcs = await getRunningClaudeProcesses(this.verbose);
|
|
395
397
|
const hasRunningClaude = claudeProcs.count > 0;
|
|
396
398
|
|
|
399
|
+
// Calculate total processing count: queue-internal + external claude processes
|
|
400
|
+
// This is used for CLAUDE_5_HOUR_SESSION_THRESHOLD and CLAUDE_WEEKLY_THRESHOLD
|
|
401
|
+
// to allow exactly one command at a time when threshold is reached
|
|
402
|
+
// See: https://github.com/link-assistant/hive-mind/issues/1133
|
|
403
|
+
const totalProcessing = this.processing.size + claudeProcs.count;
|
|
404
|
+
|
|
397
405
|
// Track claude_running as a metric (but don't add to reasons yet)
|
|
398
406
|
if (hasRunningClaude) {
|
|
399
407
|
this.recordThrottle('claude_running');
|
|
400
408
|
}
|
|
401
409
|
|
|
402
|
-
// Check system resources
|
|
410
|
+
// Check system resources (ultimate restrictions - block unconditionally)
|
|
403
411
|
const resourceCheck = await this.checkSystemResources();
|
|
404
412
|
if (!resourceCheck.ok) {
|
|
405
413
|
reasons.push(...resourceCheck.reasons);
|
|
406
414
|
}
|
|
407
|
-
if (resourceCheck.oneAtATime) {
|
|
408
|
-
oneAtATime = true;
|
|
409
|
-
}
|
|
410
415
|
|
|
411
|
-
// Check API limits (pass hasRunningClaude
|
|
412
|
-
const limitCheck = await this.checkApiLimits(hasRunningClaude);
|
|
416
|
+
// Check API limits (pass hasRunningClaude and totalProcessing for uniform checking)
|
|
417
|
+
const limitCheck = await this.checkApiLimits(hasRunningClaude, totalProcessing);
|
|
413
418
|
if (!limitCheck.ok) {
|
|
414
419
|
reasons.push(...limitCheck.reasons);
|
|
415
420
|
}
|
|
@@ -438,6 +443,7 @@ export class SolveQueue {
|
|
|
438
443
|
reasons,
|
|
439
444
|
oneAtATime,
|
|
440
445
|
claudeProcesses: claudeProcs.count,
|
|
446
|
+
totalProcessing,
|
|
441
447
|
};
|
|
442
448
|
}
|
|
443
449
|
|
|
@@ -448,17 +454,21 @@ export class SolveQueue {
|
|
|
448
454
|
* This provides a more stable metric that isn't affected by brief spikes
|
|
449
455
|
* during claude process startup.
|
|
450
456
|
*
|
|
451
|
-
*
|
|
457
|
+
* Note: System resource thresholds are ultimate restrictions - they block unconditionally
|
|
458
|
+
* when triggered. Unlike Claude API thresholds which allow one command at a time via
|
|
459
|
+
* totalProcessing check, system resources block all new commands immediately.
|
|
460
|
+
* See: https://github.com/link-assistant/hive-mind/issues/1133
|
|
461
|
+
*
|
|
462
|
+
* @returns {Promise<{ok: boolean, reasons: string[]}>}
|
|
452
463
|
*/
|
|
453
464
|
async checkSystemResources() {
|
|
454
465
|
const reasons = [];
|
|
455
|
-
let oneAtATime = false;
|
|
456
466
|
|
|
457
467
|
// Check RAM (using cached value)
|
|
458
468
|
const memResult = await getCachedMemoryInfo(this.verbose);
|
|
459
469
|
if (memResult.success) {
|
|
460
470
|
const usedRatio = memResult.memory.usedPercentage / 100;
|
|
461
|
-
if (usedRatio
|
|
471
|
+
if (usedRatio >= QUEUE_CONFIG.RAM_THRESHOLD) {
|
|
462
472
|
reasons.push(formatWaitingReason('ram', memResult.memory.usedPercentage, QUEUE_CONFIG.RAM_THRESHOLD));
|
|
463
473
|
this.recordThrottle('ram_high');
|
|
464
474
|
}
|
|
@@ -481,42 +491,45 @@ export class SolveQueue {
|
|
|
481
491
|
this.log(`CPU 5m load avg: ${loadAvg5.toFixed(2)}, cpus: ${cpuCount}, usage: ${usagePercent}%`);
|
|
482
492
|
}
|
|
483
493
|
|
|
484
|
-
if (usageRatio
|
|
494
|
+
if (usageRatio >= QUEUE_CONFIG.CPU_THRESHOLD) {
|
|
485
495
|
reasons.push(formatWaitingReason('cpu', usagePercent, QUEUE_CONFIG.CPU_THRESHOLD));
|
|
486
496
|
this.recordThrottle('cpu_high');
|
|
487
497
|
}
|
|
488
498
|
}
|
|
489
499
|
|
|
490
500
|
// Check disk space (using cached value)
|
|
501
|
+
// Note: Disk threshold is an ultimate restriction - it blocks unconditionally when triggered
|
|
502
|
+
// Unlike CLAUDE_5_HOUR_SESSION_THRESHOLD and CLAUDE_WEEKLY_THRESHOLD which use totalProcessing
|
|
503
|
+
// to allow one command at a time, disk threshold blocks all new commands immediately
|
|
504
|
+
// See: https://github.com/link-assistant/hive-mind/issues/1133
|
|
491
505
|
const diskResult = await getCachedDiskInfo(this.verbose);
|
|
492
506
|
if (diskResult.success) {
|
|
493
507
|
// Calculate usage from free percentage
|
|
494
508
|
const usedPercent = 100 - diskResult.diskSpace.freePercentage;
|
|
495
509
|
const usedRatio = usedPercent / 100;
|
|
496
|
-
if (usedRatio
|
|
497
|
-
|
|
510
|
+
if (usedRatio >= QUEUE_CONFIG.DISK_THRESHOLD) {
|
|
511
|
+
reasons.push(formatWaitingReason('disk', usedPercent, QUEUE_CONFIG.DISK_THRESHOLD));
|
|
498
512
|
this.recordThrottle('disk_high');
|
|
499
|
-
if (this.processing.size > 0) {
|
|
500
|
-
reasons.push(formatWaitingReason('disk', usedPercent, QUEUE_CONFIG.DISK_THRESHOLD) + ' (waiting for current command)');
|
|
501
|
-
}
|
|
502
513
|
}
|
|
503
514
|
}
|
|
504
515
|
|
|
505
|
-
return { ok: reasons.length === 0, reasons
|
|
516
|
+
return { ok: reasons.length === 0, reasons };
|
|
506
517
|
}
|
|
507
518
|
|
|
508
519
|
/**
|
|
509
520
|
* Check API limits (Claude, GitHub) using cached values
|
|
510
521
|
*
|
|
511
|
-
*
|
|
512
|
-
* -
|
|
513
|
-
*
|
|
514
|
-
* -
|
|
522
|
+
* Logic per issue #1133:
|
|
523
|
+
* - CLAUDE_5_HOUR_SESSION_THRESHOLD and CLAUDE_WEEKLY_THRESHOLD use one-at-a-time mode:
|
|
524
|
+
* when above threshold, allow exactly one command, block if totalProcessing > 0
|
|
525
|
+
* - GitHub threshold blocks unconditionally when exceeded (ultimate restriction)
|
|
526
|
+
* - totalProcessing = queue-internal count + external claude processes (pgrep)
|
|
515
527
|
*
|
|
516
528
|
* @param {boolean} hasRunningClaude - Whether claude processes are running
|
|
529
|
+
* @param {number} totalProcessing - Total processing count (queue + external claude processes)
|
|
517
530
|
* @returns {Promise<{ok: boolean, reasons: string[], oneAtATime: boolean}>}
|
|
518
531
|
*/
|
|
519
|
-
async checkApiLimits(hasRunningClaude = false) {
|
|
532
|
+
async checkApiLimits(hasRunningClaude = false, totalProcessing = 0) {
|
|
520
533
|
const reasons = [];
|
|
521
534
|
let oneAtATime = false;
|
|
522
535
|
|
|
@@ -527,15 +540,18 @@ export class SolveQueue {
|
|
|
527
540
|
const weeklyPercent = claudeResult.usage.allModels.percentage;
|
|
528
541
|
|
|
529
542
|
// Session limit (5-hour)
|
|
530
|
-
// When above threshold: allow exactly one command, block if
|
|
543
|
+
// When above threshold: allow exactly one command, block if any processing is happening
|
|
544
|
+
// Uses totalProcessing (queue + external claude) for uniform checking
|
|
545
|
+
// See: https://github.com/link-assistant/hive-mind/issues/1133
|
|
531
546
|
if (sessionPercent !== null) {
|
|
532
547
|
const sessionRatio = sessionPercent / 100;
|
|
533
|
-
if (sessionRatio >= QUEUE_CONFIG.
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
548
|
+
if (sessionRatio >= QUEUE_CONFIG.CLAUDE_5_HOUR_SESSION_THRESHOLD) {
|
|
549
|
+
oneAtATime = true;
|
|
550
|
+
this.recordThrottle(sessionRatio >= 1.0 ? 'claude_5_hour_session_100' : 'claude_5_hour_session_high');
|
|
551
|
+
// Use totalProcessing (queue + external claude) for uniform checking
|
|
552
|
+
if (totalProcessing > 0) {
|
|
553
|
+
reasons.push(formatWaitingReason('claude_5_hour_session', sessionPercent, QUEUE_CONFIG.CLAUDE_5_HOUR_SESSION_THRESHOLD) + ' (waiting for current command)');
|
|
537
554
|
}
|
|
538
|
-
this.recordThrottle(sessionRatio >= 1.0 ? 'claude_session_100' : 'claude_session_high');
|
|
539
555
|
}
|
|
540
556
|
}
|
|
541
557
|
|
|
@@ -546,8 +562,9 @@ export class SolveQueue {
|
|
|
546
562
|
if (weeklyRatio >= QUEUE_CONFIG.CLAUDE_WEEKLY_THRESHOLD) {
|
|
547
563
|
oneAtATime = true;
|
|
548
564
|
this.recordThrottle(weeklyRatio >= 1.0 ? 'claude_weekly_100' : 'claude_weekly_high');
|
|
549
|
-
//
|
|
550
|
-
|
|
565
|
+
// Use totalProcessing (queue + external claude) for uniform checking
|
|
566
|
+
// See: https://github.com/link-assistant/hive-mind/issues/1133
|
|
567
|
+
if (totalProcessing > 0) {
|
|
551
568
|
reasons.push(formatWaitingReason('claude_weekly', weeklyPercent, QUEUE_CONFIG.CLAUDE_WEEKLY_THRESHOLD) + ' (waiting for current command)');
|
|
552
569
|
}
|
|
553
570
|
}
|
|
@@ -664,8 +681,13 @@ export class SolveQueue {
|
|
|
664
681
|
}
|
|
665
682
|
|
|
666
683
|
// Check one-at-a-time mode
|
|
667
|
-
|
|
668
|
-
|
|
684
|
+
// When oneAtATime is true (e.g., weekly limit >= 99%), block if any processing is happening
|
|
685
|
+
// totalProcessing = queue-internal (this.processing.size) + external claude processes (pgrep)
|
|
686
|
+
// This ensures uniform checking across all threshold conditions
|
|
687
|
+
// See: https://github.com/link-assistant/hive-mind/issues/1133
|
|
688
|
+
if (check.oneAtATime && check.totalProcessing > 0) {
|
|
689
|
+
const processInfo = check.claudeProcesses > 0 ? ` (${check.claudeProcesses} claude process${check.claudeProcesses > 1 ? 'es' : ''} running)` : '';
|
|
690
|
+
this.log(`One-at-a-time mode: waiting for current command to finish${processInfo}`);
|
|
669
691
|
await this.sleep(QUEUE_CONFIG.CONSUMER_POLL_INTERVAL_MS);
|
|
670
692
|
continue;
|
|
671
693
|
}
|