@visorcraft/idlehands 1.3.3 → 1.3.5
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/README.md +28 -1
- package/dist/agent/formatting.js +1 -5
- package/dist/agent/formatting.js.map +1 -1
- package/dist/agent.js +130 -22
- package/dist/agent.js.map +1 -1
- package/dist/anton/controller.js +20 -1
- package/dist/anton/controller.js.map +1 -1
- package/dist/anton/reporter.js +2 -20
- package/dist/anton/reporter.js.map +1 -1
- package/dist/bot/auto-continue.js +24 -0
- package/dist/bot/auto-continue.js.map +1 -0
- package/dist/bot/commands.js +50 -0
- package/dist/bot/commands.js.map +1 -1
- package/dist/bot/discord-commands.js +833 -0
- package/dist/bot/discord-commands.js.map +1 -0
- package/dist/bot/discord-routing.js +1 -8
- package/dist/bot/discord-routing.js.map +1 -1
- package/dist/bot/discord.js +36 -789
- package/dist/bot/discord.js.map +1 -1
- package/dist/bot/session-manager.js +52 -0
- package/dist/bot/session-manager.js.map +1 -1
- package/dist/bot/telegram-commands.js +201 -0
- package/dist/bot/telegram-commands.js.map +1 -0
- package/dist/bot/telegram.js +32 -310
- package/dist/bot/telegram.js.map +1 -1
- package/dist/bot/ux/events.js +142 -0
- package/dist/bot/ux/events.js.map +1 -0
- package/dist/cli/commands/project.js +52 -0
- package/dist/cli/commands/project.js.map +1 -1
- package/dist/config.js +16 -0
- package/dist/config.js.map +1 -1
- package/dist/context.js +1 -3
- package/dist/context.js.map +1 -1
- package/dist/progress/ir.js +0 -3
- package/dist/progress/ir.js.map +1 -1
- package/dist/progress/tool-summary.js +1 -4
- package/dist/progress/tool-summary.js.map +1 -1
- package/dist/progress/turn-progress.js +1 -5
- package/dist/progress/turn-progress.js.map +1 -1
- package/dist/runtime/executor.js +1 -3
- package/dist/runtime/executor.js.map +1 -1
- package/dist/runtime/health.js +2 -1
- package/dist/runtime/health.js.map +1 -1
- package/dist/shared/async.js +5 -0
- package/dist/shared/async.js.map +1 -0
- package/dist/shared/config-utils.js +8 -0
- package/dist/shared/config-utils.js.map +1 -0
- package/dist/shared/format.js +19 -0
- package/dist/shared/format.js.map +1 -0
- package/dist/shared/math.js +5 -0
- package/dist/shared/math.js.map +1 -0
- package/dist/shared/strings.js +8 -0
- package/dist/shared/strings.js.map +1 -0
- package/dist/tools/patch.js +82 -0
- package/dist/tools/patch.js.map +1 -0
- package/dist/tools/path-safety.js +89 -0
- package/dist/tools/path-safety.js.map +1 -0
- package/dist/tools/undo.js +141 -0
- package/dist/tools/undo.js.map +1 -0
- package/dist/tools.js +11 -289
- package/dist/tools.js.map +1 -1
- package/dist/tui/controller.js +24 -1
- package/dist/tui/controller.js.map +1 -1
- package/dist/tui/event-bridge.js +1 -3
- package/dist/tui/event-bridge.js.map +1 -1
- package/dist/tui/render.js +1 -5
- package/dist/tui/render.js.map +1 -1
- package/dist/vault.js +1 -5
- package/dist/vault.js.map +1 -1
- package/package.json +1 -1
package/dist/bot/discord.js
CHANGED
|
@@ -1,18 +1,15 @@
|
|
|
1
|
-
import fs from 'node:fs/promises';
|
|
2
1
|
import path from 'node:path';
|
|
3
2
|
import { Client, Events, GatewayIntentBits, Partials, REST, Routes, SlashCommandBuilder, } from 'discord.js';
|
|
4
3
|
import { createSession } from '../agent.js';
|
|
5
|
-
import { runAnton } from '../anton/controller.js';
|
|
6
|
-
import { parseTaskFile } from '../anton/parser.js';
|
|
7
|
-
import { formatRunSummary, formatProgressBar, formatTaskStart, formatTaskEnd, formatTaskSkip, formatToolLoopEvent, formatCompactionEvent, formatVerificationDetail, } from '../anton/reporter.js';
|
|
8
|
-
import { firstToken } from '../cli/command-utils.js';
|
|
9
4
|
import { chainAgentHooks } from '../progress/agent-hooks.js';
|
|
10
5
|
import { projectDir, PKG_VERSION } from '../utils.js';
|
|
11
6
|
import { WATCHDOG_RECOMMENDED_TUNING_TEXT, formatWatchdogCancelMessage, resolveWatchdogSettings, shouldRecommendWatchdogTuning, } from '../watchdog.js';
|
|
12
7
|
import { DiscordConfirmProvider } from './confirm-discord.js';
|
|
13
|
-
import { detectRepoCandidates, expandHome,
|
|
14
|
-
import { parseAllowedUsers, normalizeApprovalMode,
|
|
8
|
+
import { detectRepoCandidates, expandHome, normalizeAllowedDirs, } from './dir-guard.js';
|
|
9
|
+
import { parseAllowedUsers, normalizeApprovalMode, safeContent, detectEscalation, checkKeywordEscalation, resolveAgentForMessage, sessionKeyForMessage, } from './discord-routing.js';
|
|
15
10
|
import { DiscordStreamingMessage } from './discord-streaming.js';
|
|
11
|
+
import { isToolLoopBreak, formatAutoContinueNotice, AUTO_CONTINUE_PROMPT } from './auto-continue.js';
|
|
12
|
+
import { handleTextCommand } from './discord-commands.js';
|
|
16
13
|
export async function startDiscordBot(config, botConfig) {
|
|
17
14
|
const token = process.env.IDLEHANDS_DISCORD_TOKEN || botConfig.token;
|
|
18
15
|
if (!token) {
|
|
@@ -428,6 +425,11 @@ When you escalate, your request will be re-run on a more capable model.`;
|
|
|
428
425
|
try {
|
|
429
426
|
let askComplete = false;
|
|
430
427
|
let isRetryAfterCompaction = false;
|
|
428
|
+
let isToolLoopRetry = false;
|
|
429
|
+
let toolLoopRetryCount = 0;
|
|
430
|
+
const autoContinueCfg = managed.config.tool_loop_auto_continue;
|
|
431
|
+
const autoContinueEnabled = autoContinueCfg?.enabled !== false;
|
|
432
|
+
const autoContinueMaxRetries = autoContinueCfg?.max_retries ?? 5;
|
|
431
433
|
while (!askComplete) {
|
|
432
434
|
// Create a fresh AbortController for each attempt (watchdog compaction aborts the previous one)
|
|
433
435
|
const attemptController = new AbortController();
|
|
@@ -435,7 +437,10 @@ When you escalate, your request will be re-run on a more capable model.`;
|
|
|
435
437
|
turn.controller = attemptController;
|
|
436
438
|
let askText = isRetryAfterCompaction
|
|
437
439
|
? 'Continue working on the task from where you left off. Context was compacted to free memory — do NOT restart from the beginning.'
|
|
438
|
-
:
|
|
440
|
+
: isToolLoopRetry
|
|
441
|
+
? AUTO_CONTINUE_PROMPT
|
|
442
|
+
: msg.content;
|
|
443
|
+
isToolLoopRetry = false;
|
|
439
444
|
if (managed.antonActive) {
|
|
440
445
|
askText = `${askText}\n\n[System Runtime Context: Anton task runner is CURRENTLY ACTIVE and running autonomously in the background for this project.]`;
|
|
441
446
|
}
|
|
@@ -496,6 +501,17 @@ When you escalate, your request will be re-run on a more capable model.`;
|
|
|
496
501
|
isRetryAfterCompaction = true;
|
|
497
502
|
continue;
|
|
498
503
|
}
|
|
504
|
+
// Auto-continue on tool-loop breaks
|
|
505
|
+
if (!isAbort && isToolLoopBreak(e) && autoContinueEnabled && toolLoopRetryCount < autoContinueMaxRetries) {
|
|
506
|
+
toolLoopRetryCount++;
|
|
507
|
+
const notice = formatAutoContinueNotice(raw, toolLoopRetryCount, autoContinueMaxRetries);
|
|
508
|
+
console.error(`[bot:discord] ${managed.userId} tool-loop auto-continue (retry ${toolLoopRetryCount}/${autoContinueMaxRetries})`);
|
|
509
|
+
if (isTurnActive(managed, turnId)) {
|
|
510
|
+
await streamer.finalizeError(notice);
|
|
511
|
+
}
|
|
512
|
+
isToolLoopRetry = true;
|
|
513
|
+
continue;
|
|
514
|
+
}
|
|
499
515
|
// If aborted by watchdog after max compaction attempts, it's a real cancel
|
|
500
516
|
if (!isTurnActive(managed, turnId))
|
|
501
517
|
return;
|
|
@@ -950,601 +966,19 @@ When you escalate, your request will be re-run on a more capable model.`;
|
|
|
950
966
|
await sendUserVisible(msg, '⚠️ Too many active sessions. Please retry later.').catch(() => { });
|
|
951
967
|
return;
|
|
952
968
|
}
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
...(agentLine ? [agentLine] : []),
|
|
966
|
-
`Model: \`${managed.session.model}\``,
|
|
967
|
-
`Endpoint: \`${managed.config.endpoint || '?'}\``,
|
|
968
|
-
`Default dir: \`${managed.config.dir || defaultDir}\``,
|
|
969
|
-
'',
|
|
970
|
-
'Send me a coding task, or use /help for commands.',
|
|
971
|
-
];
|
|
972
|
-
await sendUserVisible(msg, lines.join('\n')).catch(() => { });
|
|
973
|
-
return;
|
|
974
|
-
}
|
|
975
|
-
if (content === '/help') {
|
|
976
|
-
const lines = [
|
|
977
|
-
'Commands:',
|
|
978
|
-
'/start — Welcome + config summary',
|
|
979
|
-
'/help — This message',
|
|
980
|
-
'/version — Show version',
|
|
981
|
-
'/new — Start a new session',
|
|
982
|
-
'/cancel — Abort current generation',
|
|
983
|
-
'/status — Session stats',
|
|
984
|
-
'/watchdog [status] — Show watchdog settings/status',
|
|
985
|
-
'/agent — Show current agent',
|
|
986
|
-
'/agents — List all configured agents',
|
|
987
|
-
'/escalate [model] — Use larger model for next message',
|
|
988
|
-
'/deescalate — Return to base model',
|
|
989
|
-
'/dir [path] — Get/set working directory',
|
|
990
|
-
'/model — Show current model',
|
|
991
|
-
'/approval [mode] — Get/set approval mode',
|
|
992
|
-
'/mode [code|sys] — Get/set mode',
|
|
993
|
-
'/subagents [on|off] — Toggle sub-agents',
|
|
994
|
-
'/compact — Trigger context compaction',
|
|
995
|
-
'/changes — Show files modified this session',
|
|
996
|
-
'/undo — Undo last edit',
|
|
997
|
-
'/vault <query> — Search vault entries',
|
|
998
|
-
'/anton <file> — Start autonomous task runner',
|
|
999
|
-
'/anton status | /anton stop | /anton last',
|
|
1000
|
-
];
|
|
1001
|
-
await sendUserVisible(msg, lines.join('\n')).catch(() => { });
|
|
1002
|
-
return;
|
|
1003
|
-
}
|
|
1004
|
-
if (content === '/model') {
|
|
1005
|
-
await sendUserVisible(msg, `Model: \`${managed.session.model}\`\nHarness: \`${managed.session.harness}\``).catch(() => { });
|
|
1006
|
-
return;
|
|
1007
|
-
}
|
|
1008
|
-
if (content === '/version') {
|
|
1009
|
-
await sendUserVisible(msg, `Idle Hands v${PKG_VERSION}`).catch(() => { });
|
|
1010
|
-
return;
|
|
1011
|
-
}
|
|
1012
|
-
if (content === '/compact') {
|
|
1013
|
-
managed.session.reset();
|
|
1014
|
-
await sendUserVisible(msg, '🗜 Session context compacted (reset to system prompt).').catch(() => { });
|
|
1015
|
-
return;
|
|
1016
|
-
}
|
|
1017
|
-
if (content === '/dir' || content.startsWith('/dir ')) {
|
|
1018
|
-
const arg = content.slice('/dir'.length).trim();
|
|
1019
|
-
if (!arg) {
|
|
1020
|
-
const lines = [
|
|
1021
|
-
`Working directory: \`${managed.config.dir || defaultDir}\``,
|
|
1022
|
-
`Directory pinned: ${managed.dirPinned ? 'yes' : 'no'}`,
|
|
1023
|
-
];
|
|
1024
|
-
if (!managed.dirPinned && managed.repoCandidates.length > 1) {
|
|
1025
|
-
lines.push('Action required: run `/dir <repo-root>` before file edits.');
|
|
1026
|
-
lines.push(`Detected repos: ${managed.repoCandidates
|
|
1027
|
-
.slice(0, 5)
|
|
1028
|
-
.map((p) => `\`${p}\``)
|
|
1029
|
-
.join(', ')}`);
|
|
1030
|
-
}
|
|
1031
|
-
await sendUserVisible(msg, lines.join('\n')).catch(() => { });
|
|
1032
|
-
return;
|
|
1033
|
-
}
|
|
1034
|
-
const resolvedDir = path.resolve(expandHome(arg));
|
|
1035
|
-
if (!isPathAllowed(resolvedDir, managed.allowedDirs)) {
|
|
1036
|
-
await sendUserVisible(msg, `❌ Directory not allowed. Allowed roots: ${managed.allowedDirs.map((d) => `\`${d}\``).join(', ')}`).catch(() => { });
|
|
1037
|
-
return;
|
|
1038
|
-
}
|
|
1039
|
-
const repoCandidates = await detectRepoCandidates(resolvedDir, managed.allowedDirs).catch(() => managed.repoCandidates);
|
|
1040
|
-
const cfg = {
|
|
1041
|
-
...managed.config,
|
|
1042
|
-
dir: resolvedDir,
|
|
1043
|
-
allowed_write_roots: managed.allowedDirs,
|
|
1044
|
-
dir_pinned: true,
|
|
1045
|
-
repo_candidates: repoCandidates,
|
|
1046
|
-
};
|
|
1047
|
-
await recreateSession(managed, cfg);
|
|
1048
|
-
managed.dirPinned = true;
|
|
1049
|
-
managed.repoCandidates = repoCandidates;
|
|
1050
|
-
await sendUserVisible(msg, `✅ Working directory pinned to \`${resolvedDir}\``).catch(() => { });
|
|
1051
|
-
return;
|
|
1052
|
-
}
|
|
1053
|
-
if (content === '/approval' || content.startsWith('/approval ')) {
|
|
1054
|
-
const arg = content.slice('/approval'.length).trim().toLowerCase();
|
|
1055
|
-
const modes = ['plan', 'default', 'auto-edit', 'yolo'];
|
|
1056
|
-
if (!arg) {
|
|
1057
|
-
await sendUserVisible(msg, `Approval mode: \`${managed.config.approval_mode || approvalMode}\`\nOptions: ${modes.join(', ')}`).catch(() => { });
|
|
1058
|
-
return;
|
|
1059
|
-
}
|
|
1060
|
-
if (!modes.includes(arg)) {
|
|
1061
|
-
await sendUserVisible(msg, `Invalid mode. Options: ${modes.join(', ')}`).catch(() => { });
|
|
1062
|
-
return;
|
|
1063
|
-
}
|
|
1064
|
-
managed.config.approval_mode = arg;
|
|
1065
|
-
managed.config.no_confirm = arg === 'yolo';
|
|
1066
|
-
await sendUserVisible(msg, `✅ Approval mode set to \`${arg}\``).catch(() => { });
|
|
1067
|
-
return;
|
|
1068
|
-
}
|
|
1069
|
-
if (content === '/mode' || content.startsWith('/mode ')) {
|
|
1070
|
-
const arg = content.slice('/mode'.length).trim().toLowerCase();
|
|
1071
|
-
if (!arg) {
|
|
1072
|
-
await sendUserVisible(msg, `Mode: \`${managed.config.mode || 'code'}\``).catch(() => { });
|
|
1073
|
-
return;
|
|
1074
|
-
}
|
|
1075
|
-
if (arg !== 'code' && arg !== 'sys') {
|
|
1076
|
-
await sendUserVisible(msg, 'Invalid mode. Options: code, sys').catch(() => { });
|
|
1077
|
-
return;
|
|
1078
|
-
}
|
|
1079
|
-
managed.config.mode = arg;
|
|
1080
|
-
if (arg === 'sys' && managed.config.approval_mode === 'auto-edit') {
|
|
1081
|
-
managed.config.approval_mode = 'default';
|
|
1082
|
-
}
|
|
1083
|
-
await sendUserVisible(msg, `✅ Mode set to \`${arg}\``).catch(() => { });
|
|
1084
|
-
return;
|
|
1085
|
-
}
|
|
1086
|
-
if (content === '/subagents' || content.startsWith('/subagents ')) {
|
|
1087
|
-
const arg = content.slice('/subagents'.length).trim().toLowerCase();
|
|
1088
|
-
const current = managed.config.sub_agents?.enabled !== false;
|
|
1089
|
-
if (!arg) {
|
|
1090
|
-
await sendUserVisible(msg, `Sub-agents: \`${current ? 'on' : 'off'}\`\nUsage: /subagents on | off`).catch(() => { });
|
|
1091
|
-
return;
|
|
1092
|
-
}
|
|
1093
|
-
if (arg !== 'on' && arg !== 'off') {
|
|
1094
|
-
await sendUserVisible(msg, 'Invalid value. Usage: /subagents on | off').catch(() => { });
|
|
1095
|
-
return;
|
|
1096
|
-
}
|
|
1097
|
-
const enabled = arg === 'on';
|
|
1098
|
-
managed.config.sub_agents = { ...(managed.config.sub_agents ?? {}), enabled };
|
|
1099
|
-
await sendUserVisible(msg, `✅ Sub-agents \`${enabled ? 'on' : 'off'}\`${!enabled ? ' — spawn_task disabled for this session' : ''}`).catch(() => { });
|
|
1100
|
-
return;
|
|
1101
|
-
}
|
|
1102
|
-
if (content === '/changes') {
|
|
1103
|
-
const replay = managed.session.replay;
|
|
1104
|
-
if (!replay) {
|
|
1105
|
-
await sendUserVisible(msg, 'Replay is disabled. No change tracking available.').catch(() => { });
|
|
1106
|
-
return;
|
|
1107
|
-
}
|
|
1108
|
-
try {
|
|
1109
|
-
const checkpoints = await replay.list(50);
|
|
1110
|
-
if (!checkpoints.length) {
|
|
1111
|
-
await sendUserVisible(msg, 'No file changes this session.').catch(() => { });
|
|
1112
|
-
return;
|
|
1113
|
-
}
|
|
1114
|
-
const byFile = new Map();
|
|
1115
|
-
for (const cp of checkpoints)
|
|
1116
|
-
byFile.set(cp.filePath, (byFile.get(cp.filePath) ?? 0) + 1);
|
|
1117
|
-
const lines = [`Session changes (${byFile.size} files):`];
|
|
1118
|
-
for (const [fp, count] of byFile)
|
|
1119
|
-
lines.push(`✎ \`${fp}\` (${count} edit${count > 1 ? 's' : ''})`);
|
|
1120
|
-
await sendUserVisible(msg, lines.join('\n')).catch(() => { });
|
|
1121
|
-
}
|
|
1122
|
-
catch (e) {
|
|
1123
|
-
await sendUserVisible(msg, `Error listing changes: ${e?.message ?? String(e)}`).catch(() => { });
|
|
1124
|
-
}
|
|
1125
|
-
return;
|
|
1126
|
-
}
|
|
1127
|
-
if (content === '/undo') {
|
|
1128
|
-
const lastPath = managed.session.lastEditedPath;
|
|
1129
|
-
if (!lastPath) {
|
|
1130
|
-
await sendUserVisible(msg, 'No recent edits to undo.').catch(() => { });
|
|
1131
|
-
return;
|
|
1132
|
-
}
|
|
1133
|
-
try {
|
|
1134
|
-
const { undo_path } = await import('../tools.js');
|
|
1135
|
-
const result = await undo_path({ cwd: managed.config.dir || defaultDir, noConfirm: true, dryRun: false }, { path: lastPath });
|
|
1136
|
-
await sendUserVisible(msg, `✅ ${result}`).catch(() => { });
|
|
1137
|
-
}
|
|
1138
|
-
catch (e) {
|
|
1139
|
-
await sendUserVisible(msg, `❌ Undo failed: ${e?.message ?? String(e)}`).catch(() => { });
|
|
1140
|
-
}
|
|
1141
|
-
return;
|
|
1142
|
-
}
|
|
1143
|
-
if (content === '/vault' || content.startsWith('/vault ')) {
|
|
1144
|
-
const query = content.slice('/vault'.length).trim();
|
|
1145
|
-
if (!query) {
|
|
1146
|
-
await sendUserVisible(msg, 'Usage: /vault <search query>').catch(() => { });
|
|
1147
|
-
return;
|
|
1148
|
-
}
|
|
1149
|
-
const vault = managed.session.vault;
|
|
1150
|
-
if (!vault) {
|
|
1151
|
-
await sendUserVisible(msg, 'Vault is disabled.').catch(() => { });
|
|
1152
|
-
return;
|
|
1153
|
-
}
|
|
1154
|
-
try {
|
|
1155
|
-
const results = await vault.search(query, 5);
|
|
1156
|
-
if (!results.length) {
|
|
1157
|
-
await sendUserVisible(msg, `No vault results for "${query}"`).catch(() => { });
|
|
1158
|
-
return;
|
|
1159
|
-
}
|
|
1160
|
-
const lines = [`Vault results for "${query}":`];
|
|
1161
|
-
for (const r of results) {
|
|
1162
|
-
const title = r.kind === 'note' ? `note:${r.key}` : `tool:${r.tool || r.key || '?'}`;
|
|
1163
|
-
const body = (r.value ?? r.snippet ?? r.content ?? '').replace(/\s+/g, ' ').slice(0, 120);
|
|
1164
|
-
lines.push(`• ${title}: ${body}`);
|
|
1165
|
-
}
|
|
1166
|
-
await sendUserVisible(msg, lines.join('\n')).catch(() => { });
|
|
1167
|
-
}
|
|
1168
|
-
catch (e) {
|
|
1169
|
-
await sendUserVisible(msg, `Error searching vault: ${e?.message ?? String(e)}`).catch(() => { });
|
|
1170
|
-
}
|
|
1171
|
-
return;
|
|
1172
|
-
}
|
|
1173
|
-
if (content === '/status') {
|
|
1174
|
-
const used = managed.session.currentContextTokens;
|
|
1175
|
-
const pct = managed.session.contextWindow > 0
|
|
1176
|
-
? Math.min(100, (used / managed.session.contextWindow) * 100).toFixed(1)
|
|
1177
|
-
: '?';
|
|
1178
|
-
const agentLine = managed.agentPersona
|
|
1179
|
-
? `Agent: ${managed.agentPersona.display_name || managed.agentId}`
|
|
1180
|
-
: null;
|
|
1181
|
-
await sendUserVisible(msg, [
|
|
1182
|
-
...(agentLine ? [agentLine] : []),
|
|
1183
|
-
`Mode: ${managed.config.mode ?? 'code'}`,
|
|
1184
|
-
`Approval: ${managed.config.approval_mode}`,
|
|
1185
|
-
`Model: ${managed.session.model}`,
|
|
1186
|
-
`Harness: ${managed.session.harness}`,
|
|
1187
|
-
`Dir: ${managed.config.dir ?? defaultDir}`,
|
|
1188
|
-
`Dir pinned: ${managed.dirPinned ? 'yes' : 'no'}`,
|
|
1189
|
-
`Context: ~${used}/${managed.session.contextWindow} (${pct}%)`,
|
|
1190
|
-
`State: ${managed.state}`,
|
|
1191
|
-
`Queue: ${managed.pendingQueue.length}/${maxQueue}`,
|
|
1192
|
-
].join('\n')).catch(() => { });
|
|
1193
|
-
return;
|
|
1194
|
-
}
|
|
1195
|
-
if (content === '/watchdog' || content === '/watchdog status') {
|
|
1196
|
-
await sendUserVisible(msg, watchdogStatusText(managed)).catch(() => { });
|
|
1197
|
-
return;
|
|
1198
|
-
}
|
|
1199
|
-
if (content.startsWith('/watchdog ')) {
|
|
1200
|
-
await sendUserVisible(msg, 'Usage: /watchdog or /watchdog status').catch(() => { });
|
|
1201
|
-
return;
|
|
1202
|
-
}
|
|
1203
|
-
// /agent - show current agent info
|
|
1204
|
-
if (content === '/agent') {
|
|
1205
|
-
if (!managed.agentPersona) {
|
|
1206
|
-
await sendUserVisible(msg, 'No agent configured. Using global config.').catch(() => { });
|
|
1207
|
-
return;
|
|
1208
|
-
}
|
|
1209
|
-
const p = managed.agentPersona;
|
|
1210
|
-
const lines = [
|
|
1211
|
-
`**Agent: ${p.display_name || managed.agentId}** (\`${managed.agentId}\`)`,
|
|
1212
|
-
...(p.model ? [`Model: \`${p.model}\``] : []),
|
|
1213
|
-
...(p.endpoint ? [`Endpoint: \`${p.endpoint}\``] : []),
|
|
1214
|
-
...(p.approval_mode ? [`Approval: \`${p.approval_mode}\``] : []),
|
|
1215
|
-
...(p.default_dir ? [`Default dir: \`${p.default_dir}\``] : []),
|
|
1216
|
-
...(p.allowed_dirs?.length
|
|
1217
|
-
? [`Allowed dirs: ${p.allowed_dirs.map((d) => `\`${d}\``).join(', ')}`]
|
|
1218
|
-
: []),
|
|
1219
|
-
];
|
|
1220
|
-
await sendUserVisible(msg, lines.join('\n')).catch(() => { });
|
|
1221
|
-
return;
|
|
1222
|
-
}
|
|
1223
|
-
// /agents - list all configured agents
|
|
1224
|
-
if (content === '/agents') {
|
|
1225
|
-
const agents = botConfig.agents;
|
|
1226
|
-
if (!agents || Object.keys(agents).length === 0) {
|
|
1227
|
-
await sendUserVisible(msg, 'No agents configured. Using global config.').catch(() => { });
|
|
1228
|
-
return;
|
|
1229
|
-
}
|
|
1230
|
-
const lines = ['**Configured Agents:**'];
|
|
1231
|
-
for (const [id, p] of Object.entries(agents)) {
|
|
1232
|
-
const current = id === managed.agentId ? ' ← current' : '';
|
|
1233
|
-
const model = p.model ? ` (${p.model})` : '';
|
|
1234
|
-
lines.push(`• **${p.display_name || id}** (\`${id}\`)${model}${current}`);
|
|
1235
|
-
}
|
|
1236
|
-
// Show routing rules
|
|
1237
|
-
const routing = botConfig.routing;
|
|
1238
|
-
if (routing) {
|
|
1239
|
-
lines.push('', '**Routing:**');
|
|
1240
|
-
if (routing.default)
|
|
1241
|
-
lines.push(`Default: \`${routing.default}\``);
|
|
1242
|
-
if (routing.users && Object.keys(routing.users).length > 0) {
|
|
1243
|
-
lines.push(`Users: ${Object.entries(routing.users)
|
|
1244
|
-
.map(([u, a]) => `${u}→${a}`)
|
|
1245
|
-
.join(', ')}`);
|
|
1246
|
-
}
|
|
1247
|
-
if (routing.channels && Object.keys(routing.channels).length > 0) {
|
|
1248
|
-
lines.push(`Channels: ${Object.entries(routing.channels)
|
|
1249
|
-
.map(([c, a]) => `${c}→${a}`)
|
|
1250
|
-
.join(', ')}`);
|
|
1251
|
-
}
|
|
1252
|
-
if (routing.guilds && Object.keys(routing.guilds).length > 0) {
|
|
1253
|
-
lines.push(`Guilds: ${Object.entries(routing.guilds)
|
|
1254
|
-
.map(([g, a]) => `${g}→${a}`)
|
|
1255
|
-
.join(', ')}`);
|
|
1256
|
-
}
|
|
1257
|
-
}
|
|
1258
|
-
await sendUserVisible(msg, lines.join('\n')).catch(() => { });
|
|
1259
|
-
return;
|
|
1260
|
-
}
|
|
1261
|
-
// /escalate - explicitly escalate to a larger model for next message
|
|
1262
|
-
if (content === '/escalate' || content.startsWith('/escalate ')) {
|
|
1263
|
-
const escalation = managed.agentPersona?.escalation;
|
|
1264
|
-
if (!escalation || !escalation.models?.length) {
|
|
1265
|
-
await sendUserVisible(msg, '❌ No escalation models configured for this agent.').catch(() => { });
|
|
1266
|
-
return;
|
|
1267
|
-
}
|
|
1268
|
-
const arg = content.slice('/escalate'.length).trim();
|
|
1269
|
-
// No arg: show available models and current state
|
|
1270
|
-
if (!arg) {
|
|
1271
|
-
const currentModel = managed.config.model || config.model || 'default';
|
|
1272
|
-
const lines = [
|
|
1273
|
-
`**Current model:** \`${currentModel}\``,
|
|
1274
|
-
`**Escalation models:** ${escalation.models.map((m) => `\`${m}\``).join(', ')}`,
|
|
1275
|
-
'',
|
|
1276
|
-
'Usage: `/escalate <model>` or `/escalate next`',
|
|
1277
|
-
'Then send your message - it will use the escalated model.',
|
|
1278
|
-
];
|
|
1279
|
-
if (managed.pendingEscalation) {
|
|
1280
|
-
lines.push('', `⚡ **Pending escalation:** \`${managed.pendingEscalation}\` (next message will use this)`);
|
|
1281
|
-
}
|
|
1282
|
-
await sendUserVisible(msg, lines.join('\n')).catch(() => { });
|
|
1283
|
-
return;
|
|
1284
|
-
}
|
|
1285
|
-
// Handle 'next' - escalate to next model in chain
|
|
1286
|
-
let targetModel;
|
|
1287
|
-
if (arg.toLowerCase() === 'next') {
|
|
1288
|
-
const nextIndex = Math.min(managed.currentModelIndex, escalation.models.length - 1);
|
|
1289
|
-
targetModel = escalation.models[nextIndex];
|
|
1290
|
-
}
|
|
1291
|
-
else {
|
|
1292
|
-
// Specific model requested
|
|
1293
|
-
if (!escalation.models.includes(arg)) {
|
|
1294
|
-
await sendUserVisible(msg, `❌ Model \`${arg}\` not in escalation chain. Available: ${escalation.models.map((m) => `\`${m}\``).join(', ')}`).catch(() => { });
|
|
1295
|
-
return;
|
|
1296
|
-
}
|
|
1297
|
-
targetModel = arg;
|
|
1298
|
-
}
|
|
1299
|
-
managed.pendingEscalation = targetModel;
|
|
1300
|
-
await sendUserVisible(msg, `⚡ Next message will use \`${targetModel}\`. Send your request now.`).catch(() => { });
|
|
1301
|
-
return;
|
|
1302
|
-
}
|
|
1303
|
-
// /deescalate - return to base model
|
|
1304
|
-
if (content === '/deescalate') {
|
|
1305
|
-
if (managed.currentModelIndex === 0 && !managed.pendingEscalation) {
|
|
1306
|
-
await sendUserVisible(msg, 'Already using base model.').catch(() => { });
|
|
1307
|
-
return;
|
|
1308
|
-
}
|
|
1309
|
-
const baseModel = managed.agentPersona?.model || config.model || 'default';
|
|
1310
|
-
managed.pendingEscalation = null;
|
|
1311
|
-
managed.currentModelIndex = 0;
|
|
1312
|
-
// Recreate session with base model
|
|
1313
|
-
const cfg = {
|
|
1314
|
-
...managed.config,
|
|
1315
|
-
model: baseModel,
|
|
1316
|
-
};
|
|
1317
|
-
await recreateSession(managed, cfg);
|
|
1318
|
-
await sendUserVisible(msg, `✅ Returned to base model: \`${baseModel}\``).catch(() => { });
|
|
1319
|
-
return;
|
|
1320
|
-
}
|
|
1321
|
-
// /git_status - show git status for working directory
|
|
1322
|
-
if (content === '/git_status') {
|
|
1323
|
-
const cwd = managed.config.dir || defaultDir;
|
|
1324
|
-
if (!cwd) {
|
|
1325
|
-
await sendUserVisible(msg, 'No working directory set. Use `/dir` to set one.').catch(() => { });
|
|
1326
|
-
return;
|
|
1327
|
-
}
|
|
1328
|
-
try {
|
|
1329
|
-
const { spawnSync } = await import('node:child_process');
|
|
1330
|
-
// Run git status -s
|
|
1331
|
-
const statusResult = spawnSync('git', ['status', '-s'], {
|
|
1332
|
-
cwd,
|
|
1333
|
-
encoding: 'utf8',
|
|
1334
|
-
timeout: 5000,
|
|
1335
|
-
});
|
|
1336
|
-
if (statusResult.status !== 0) {
|
|
1337
|
-
const err = String(statusResult.stderr || statusResult.error || 'Unknown error');
|
|
1338
|
-
if (err.includes('not a git repository') || err.includes('not in a git')) {
|
|
1339
|
-
await sendUserVisible(msg, '❌ Not a git repository.').catch(() => { });
|
|
1340
|
-
}
|
|
1341
|
-
else {
|
|
1342
|
-
await sendUserVisible(msg, `❌ git status failed: ${err.slice(0, 200)}`).catch(() => { });
|
|
1343
|
-
}
|
|
1344
|
-
return;
|
|
1345
|
-
}
|
|
1346
|
-
const statusOut = String(statusResult.stdout || '').trim();
|
|
1347
|
-
// Get branch info
|
|
1348
|
-
const branchResult = spawnSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {
|
|
1349
|
-
cwd,
|
|
1350
|
-
encoding: 'utf8',
|
|
1351
|
-
timeout: 2000,
|
|
1352
|
-
});
|
|
1353
|
-
const branch = branchResult.status === 0 ? String(branchResult.stdout || '').trim() : 'unknown';
|
|
1354
|
-
if (!statusOut) {
|
|
1355
|
-
await sendUserVisible(msg, `📁 \`${cwd}\`\n🌿 Branch: \`${branch}\`\n\n✅ Working tree clean`).catch(() => { });
|
|
1356
|
-
return;
|
|
1357
|
-
}
|
|
1358
|
-
const lines = statusOut.split('\n').slice(0, 30);
|
|
1359
|
-
const truncated = statusOut.split('\n').length > 30;
|
|
1360
|
-
const formatted = lines
|
|
1361
|
-
.map((line) => `\`${line.slice(0, 2)}\` ${line.slice(3)}`)
|
|
1362
|
-
.join('\n');
|
|
1363
|
-
await sendUserVisible(msg, `📁 \`${cwd}\`\n🌿 Branch: \`${branch}\`\n\n\`\`\`\n${formatted}${truncated ? '\n...' : ''}\`\`\``).catch(() => { });
|
|
1364
|
-
}
|
|
1365
|
-
catch (e) {
|
|
1366
|
-
await sendUserVisible(msg, `❌ git status failed: ${e?.message ?? String(e)}`).catch(() => { });
|
|
1367
|
-
}
|
|
1368
|
-
return;
|
|
1369
|
-
}
|
|
1370
|
-
if (content === '/hosts') {
|
|
1371
|
-
try {
|
|
1372
|
-
const { loadRuntimes, redactConfig } = await import('../runtime/store.js');
|
|
1373
|
-
const config = await loadRuntimes();
|
|
1374
|
-
const redacted = redactConfig(config);
|
|
1375
|
-
if (!redacted.hosts.length) {
|
|
1376
|
-
await sendUserVisible(msg, 'No hosts configured. Use `idlehands hosts add` in CLI.').catch(() => { });
|
|
1377
|
-
return;
|
|
1378
|
-
}
|
|
1379
|
-
const lines = redacted.hosts.map((h) => `${h.enabled ? '🟢' : '🔴'} ${h.display_name} (\`${h.id}\`)\n Transport: ${h.transport}`);
|
|
1380
|
-
const chunks = splitDiscord(lines.join('\n\n'));
|
|
1381
|
-
for (const [i, chunk] of chunks.entries()) {
|
|
1382
|
-
if (i === 0)
|
|
1383
|
-
await sendUserVisible(msg, chunk).catch(() => { });
|
|
1384
|
-
else
|
|
1385
|
-
await msg.channel.send(chunk).catch(() => { });
|
|
1386
|
-
}
|
|
1387
|
-
}
|
|
1388
|
-
catch (e) {
|
|
1389
|
-
await sendUserVisible(msg, `❌ Failed to load hosts: ${e?.message ?? String(e)}`).catch(() => { });
|
|
1390
|
-
}
|
|
1391
|
-
return;
|
|
1392
|
-
}
|
|
1393
|
-
if (content === '/backends') {
|
|
1394
|
-
try {
|
|
1395
|
-
const { loadRuntimes, redactConfig } = await import('../runtime/store.js');
|
|
1396
|
-
const config = await loadRuntimes();
|
|
1397
|
-
const redacted = redactConfig(config);
|
|
1398
|
-
if (!redacted.backends.length) {
|
|
1399
|
-
await sendUserVisible(msg, 'No backends configured. Use `idlehands backends add` in CLI.').catch(() => { });
|
|
1400
|
-
return;
|
|
1401
|
-
}
|
|
1402
|
-
const lines = redacted.backends.map((b) => `${b.enabled ? '🟢' : '🔴'} ${b.display_name} (\`${b.id}\`)\n Type: ${b.type}`);
|
|
1403
|
-
const chunks = splitDiscord(lines.join('\n\n'));
|
|
1404
|
-
for (const [i, chunk] of chunks.entries()) {
|
|
1405
|
-
if (i === 0)
|
|
1406
|
-
await sendUserVisible(msg, chunk).catch(() => { });
|
|
1407
|
-
else
|
|
1408
|
-
await msg.channel.send(chunk).catch(() => { });
|
|
1409
|
-
}
|
|
1410
|
-
}
|
|
1411
|
-
catch (e) {
|
|
1412
|
-
await sendUserVisible(msg, `❌ Failed to load backends: ${e?.message ?? String(e)}`).catch(() => { });
|
|
1413
|
-
}
|
|
1414
|
-
return;
|
|
1415
|
-
}
|
|
1416
|
-
if (content === '/models' || content === '/rtmodels') {
|
|
1417
|
-
try {
|
|
1418
|
-
const { loadRuntimes } = await import('../runtime/store.js');
|
|
1419
|
-
const config = await loadRuntimes();
|
|
1420
|
-
if (!config.models.length) {
|
|
1421
|
-
await sendUserVisible(msg, 'No runtime models configured.').catch(() => { });
|
|
1422
|
-
return;
|
|
1423
|
-
}
|
|
1424
|
-
const enabledModels = config.models.filter((m) => m.enabled);
|
|
1425
|
-
if (!enabledModels.length) {
|
|
1426
|
-
await sendUserVisible(msg, 'No enabled runtime models. Use `idlehands models enable <id>` in CLI.').catch(() => { });
|
|
1427
|
-
return;
|
|
1428
|
-
}
|
|
1429
|
-
// Create buttons for model selection (Discord max 5 buttons per row, 5 rows)
|
|
1430
|
-
const { ActionRowBuilder, ButtonBuilder, ButtonStyle } = await import('discord.js');
|
|
1431
|
-
const rows = [];
|
|
1432
|
-
let currentRow = new ActionRowBuilder();
|
|
1433
|
-
for (const m of enabledModels) {
|
|
1434
|
-
const btn = new ButtonBuilder()
|
|
1435
|
-
.setCustomId(`model_switch:${m.id}`)
|
|
1436
|
-
.setLabel(m.display_name.slice(0, 80))
|
|
1437
|
-
.setStyle(ButtonStyle.Primary);
|
|
1438
|
-
currentRow.addComponents(btn);
|
|
1439
|
-
if (currentRow.components.length >= 5) {
|
|
1440
|
-
rows.push(currentRow);
|
|
1441
|
-
currentRow = new ActionRowBuilder();
|
|
1442
|
-
}
|
|
1443
|
-
}
|
|
1444
|
-
if (currentRow.components.length > 0) {
|
|
1445
|
-
rows.push(currentRow);
|
|
1446
|
-
}
|
|
1447
|
-
await msg.channel.send({
|
|
1448
|
-
content: '📋 **Select a model to switch to:**',
|
|
1449
|
-
components: rows.slice(0, 5), // Discord max 5 rows
|
|
1450
|
-
}).catch(() => { });
|
|
1451
|
-
}
|
|
1452
|
-
catch (e) {
|
|
1453
|
-
await sendUserVisible(msg, `❌ Failed to load runtime models: ${e?.message ?? String(e)}`).catch(() => { });
|
|
1454
|
-
}
|
|
1455
|
-
return;
|
|
1456
|
-
}
|
|
1457
|
-
if (content === '/rtstatus') {
|
|
1458
|
-
try {
|
|
1459
|
-
const { loadActiveRuntime } = await import('../runtime/executor.js');
|
|
1460
|
-
const active = await loadActiveRuntime();
|
|
1461
|
-
if (!active) {
|
|
1462
|
-
await sendUserVisible(msg, 'No active runtime.').catch(() => { });
|
|
1463
|
-
return;
|
|
1464
|
-
}
|
|
1465
|
-
const lines = [
|
|
1466
|
-
'Active Runtime',
|
|
1467
|
-
`Model: \`${active.modelId}\``,
|
|
1468
|
-
`Backend: \`${active.backendId ?? 'none'}\``,
|
|
1469
|
-
`Hosts: ${active.hostIds.map((id) => `\`${id}\``).join(', ') || 'none'}`,
|
|
1470
|
-
`Healthy: ${active.healthy ? '✅ yes' : '❌ no'}`,
|
|
1471
|
-
`Endpoint: \`${active.endpoint ?? 'unknown'}\``,
|
|
1472
|
-
`Started: \`${active.startedAt}\``,
|
|
1473
|
-
];
|
|
1474
|
-
const chunks = splitDiscord(lines.join('\n'));
|
|
1475
|
-
for (const [i, chunk] of chunks.entries()) {
|
|
1476
|
-
if (i === 0)
|
|
1477
|
-
await sendUserVisible(msg, chunk).catch(() => { });
|
|
1478
|
-
else
|
|
1479
|
-
await msg.channel.send(chunk).catch(() => { });
|
|
1480
|
-
}
|
|
1481
|
-
}
|
|
1482
|
-
catch (e) {
|
|
1483
|
-
await sendUserVisible(msg, `❌ Failed to read runtime status: ${e?.message ?? String(e)}`).catch(() => { });
|
|
1484
|
-
}
|
|
1485
|
-
return;
|
|
1486
|
-
}
|
|
1487
|
-
if (content === '/switch' || content.startsWith('/switch ')) {
|
|
1488
|
-
try {
|
|
1489
|
-
const modelId = content.slice('/switch'.length).trim();
|
|
1490
|
-
if (!modelId) {
|
|
1491
|
-
await sendUserVisible(msg, 'Usage: /switch <model-id>').catch(() => { });
|
|
1492
|
-
return;
|
|
1493
|
-
}
|
|
1494
|
-
const { plan } = await import('../runtime/planner.js');
|
|
1495
|
-
const { execute, loadActiveRuntime } = await import('../runtime/executor.js');
|
|
1496
|
-
const { loadRuntimes } = await import('../runtime/store.js');
|
|
1497
|
-
const rtConfig = await loadRuntimes();
|
|
1498
|
-
const active = await loadActiveRuntime();
|
|
1499
|
-
const result = plan({ modelId, mode: 'live' }, rtConfig, active);
|
|
1500
|
-
if (!result.ok) {
|
|
1501
|
-
await sendUserVisible(msg, `❌ Plan failed: ${result.reason}`).catch(() => { });
|
|
1502
|
-
return;
|
|
1503
|
-
}
|
|
1504
|
-
if (result.reuse) {
|
|
1505
|
-
await sendUserVisible(msg, '✅ Runtime already active and healthy.').catch(() => { });
|
|
1506
|
-
return;
|
|
1507
|
-
}
|
|
1508
|
-
const statusMsg = await sendUserVisible(msg, `⏳ Switching to \`${result.model.display_name}\`...`).catch(() => null);
|
|
1509
|
-
const execResult = await execute(result, {
|
|
1510
|
-
onStep: async (step, status) => {
|
|
1511
|
-
if (status === 'done' && statusMsg) {
|
|
1512
|
-
await statusMsg.edit(`⏳ ${step.description}... ✓`).catch(() => { });
|
|
1513
|
-
}
|
|
1514
|
-
},
|
|
1515
|
-
confirm: async (prompt) => {
|
|
1516
|
-
await sendUserVisible(msg, `⚠️ ${prompt}\nAuto-approving for bot context.`).catch(() => { });
|
|
1517
|
-
return true;
|
|
1518
|
-
},
|
|
1519
|
-
});
|
|
1520
|
-
if (execResult.ok) {
|
|
1521
|
-
if (statusMsg) {
|
|
1522
|
-
await statusMsg.edit(`✅ Switched to \`${result.model.display_name}\``).catch(() => { });
|
|
1523
|
-
}
|
|
1524
|
-
else {
|
|
1525
|
-
await sendUserVisible(msg, `✅ Switched to \`${result.model.display_name}\``).catch(() => { });
|
|
1526
|
-
}
|
|
1527
|
-
}
|
|
1528
|
-
else {
|
|
1529
|
-
const err = `❌ Switch failed: ${execResult.error || 'unknown error'}`;
|
|
1530
|
-
if (statusMsg) {
|
|
1531
|
-
await statusMsg.edit(err).catch(() => { });
|
|
1532
|
-
}
|
|
1533
|
-
else {
|
|
1534
|
-
await sendUserVisible(msg, err).catch(() => { });
|
|
1535
|
-
}
|
|
1536
|
-
}
|
|
1537
|
-
}
|
|
1538
|
-
catch (e) {
|
|
1539
|
-
await sendUserVisible(msg, `❌ Switch failed: ${e?.message ?? String(e)}`).catch(() => { });
|
|
1540
|
-
}
|
|
1541
|
-
return;
|
|
1542
|
-
}
|
|
1543
|
-
// /anton command
|
|
1544
|
-
if (content === '/anton' || content.startsWith('/anton ')) {
|
|
1545
|
-
await handleDiscordAnton(managed, msg, content);
|
|
969
|
+
const cmdCtx = {
|
|
970
|
+
sendUserVisible,
|
|
971
|
+
cancelActive,
|
|
972
|
+
recreateSession,
|
|
973
|
+
watchdogStatusText,
|
|
974
|
+
defaultDir,
|
|
975
|
+
config,
|
|
976
|
+
botConfig,
|
|
977
|
+
approvalMode,
|
|
978
|
+
maxQueue,
|
|
979
|
+
};
|
|
980
|
+
if (await handleTextCommand(managed, msg, content, cmdCtx))
|
|
1546
981
|
return;
|
|
1547
|
-
}
|
|
1548
982
|
if (managed.inFlight) {
|
|
1549
983
|
if (managed.pendingQueue.length >= maxQueue) {
|
|
1550
984
|
await sendUserVisible(msg, `⏳ Queue full (${managed.pendingQueue.length}/${maxQueue}). Use /cancel.`).catch(() => { });
|
|
@@ -1564,193 +998,6 @@ When you escalate, your request will be re-run on a more capable model.`;
|
|
|
1564
998
|
await sendUserVisible(msg, `⚠️ Bot error: ${errMsg.length > 300 ? errMsg.slice(0, 297) + '...' : errMsg}`).catch(() => { });
|
|
1565
999
|
});
|
|
1566
1000
|
});
|
|
1567
|
-
const DISCORD_RATE_LIMIT_MS = 15_000;
|
|
1568
|
-
async function handleDiscordAnton(managed, msg, content) {
|
|
1569
|
-
const args = content.replace(/^\/anton\s*/, '').trim();
|
|
1570
|
-
const sub = firstToken(args);
|
|
1571
|
-
if (!sub || sub === 'status') {
|
|
1572
|
-
if (!managed.antonActive) {
|
|
1573
|
-
await sendUserVisible(msg, 'No Anton run in progress.').catch(() => { });
|
|
1574
|
-
}
|
|
1575
|
-
else if (managed.antonAbortSignal?.aborted) {
|
|
1576
|
-
await sendUserVisible(msg, '🛑 Anton is stopping. Please wait for the current attempt to unwind.').catch(() => { });
|
|
1577
|
-
}
|
|
1578
|
-
else if (managed.antonProgress) {
|
|
1579
|
-
const line1 = formatProgressBar(managed.antonProgress);
|
|
1580
|
-
if (managed.antonProgress.currentTask) {
|
|
1581
|
-
await sendUserVisible(msg, `${line1}\n\n**Working on:** *${managed.antonProgress.currentTask}* (Attempt ${managed.antonProgress.currentAttempt})`).catch(() => { });
|
|
1582
|
-
}
|
|
1583
|
-
else {
|
|
1584
|
-
await sendUserVisible(msg, line1).catch(() => { });
|
|
1585
|
-
}
|
|
1586
|
-
}
|
|
1587
|
-
else {
|
|
1588
|
-
await sendUserVisible(msg, '🤖 Anton is running (no progress data yet).').catch(() => { });
|
|
1589
|
-
}
|
|
1590
|
-
return;
|
|
1591
|
-
}
|
|
1592
|
-
if (sub === 'stop') {
|
|
1593
|
-
if (!managed.antonActive || !managed.antonAbortSignal) {
|
|
1594
|
-
await sendUserVisible(msg, 'No Anton run in progress.').catch(() => { });
|
|
1595
|
-
return;
|
|
1596
|
-
}
|
|
1597
|
-
managed.lastActivity = Date.now();
|
|
1598
|
-
managed.antonAbortSignal.aborted = true;
|
|
1599
|
-
await sendUserVisible(msg, '🛑 Anton stop requested.').catch(() => { });
|
|
1600
|
-
return;
|
|
1601
|
-
}
|
|
1602
|
-
if (sub === 'last') {
|
|
1603
|
-
if (!managed.antonLastResult) {
|
|
1604
|
-
await sendUserVisible(msg, 'No previous Anton run.').catch(() => { });
|
|
1605
|
-
return;
|
|
1606
|
-
}
|
|
1607
|
-
await sendUserVisible(msg, formatRunSummary(managed.antonLastResult)).catch(() => { });
|
|
1608
|
-
return;
|
|
1609
|
-
}
|
|
1610
|
-
const filePart = sub === 'run' ? args.replace(/^\S+\s*/, '').trim() : args;
|
|
1611
|
-
if (!filePart) {
|
|
1612
|
-
await sendUserVisible(msg, '/anton <file> — start | /anton status | /anton stop | /anton last').catch(() => { });
|
|
1613
|
-
return;
|
|
1614
|
-
}
|
|
1615
|
-
if (managed.antonActive) {
|
|
1616
|
-
const staleMs = Date.now() - managed.lastActivity;
|
|
1617
|
-
if (staleMs > 120_000) {
|
|
1618
|
-
managed.antonActive = false;
|
|
1619
|
-
managed.antonAbortSignal = null;
|
|
1620
|
-
managed.antonProgress = null;
|
|
1621
|
-
await sendUserVisible(msg, '♻️ Recovered stale Anton run state. Starting a fresh run...').catch(() => { });
|
|
1622
|
-
}
|
|
1623
|
-
else {
|
|
1624
|
-
const runningMsg = managed.antonAbortSignal?.aborted
|
|
1625
|
-
? '🛑 Anton is still stopping. Please wait a moment, then try again.'
|
|
1626
|
-
: '⚠️ Anton is already running. Use /anton stop first.';
|
|
1627
|
-
await sendUserVisible(msg, runningMsg).catch(() => { });
|
|
1628
|
-
return;
|
|
1629
|
-
}
|
|
1630
|
-
}
|
|
1631
|
-
const cwd = managed.config.dir || process.cwd();
|
|
1632
|
-
const filePath = path.resolve(cwd, filePart);
|
|
1633
|
-
try {
|
|
1634
|
-
await fs.stat(filePath);
|
|
1635
|
-
}
|
|
1636
|
-
catch {
|
|
1637
|
-
await sendUserVisible(msg, `File not found: ${filePath}`).catch(() => { });
|
|
1638
|
-
return;
|
|
1639
|
-
}
|
|
1640
|
-
const defaults = managed.config.anton || {};
|
|
1641
|
-
const runConfig = {
|
|
1642
|
-
taskFile: filePath,
|
|
1643
|
-
projectDir: defaults.project_dir || cwd,
|
|
1644
|
-
maxRetriesPerTask: defaults.max_retries ?? 3,
|
|
1645
|
-
maxIterations: defaults.max_iterations ?? 200,
|
|
1646
|
-
taskMaxIterations: defaults.task_max_iterations ?? 50,
|
|
1647
|
-
taskTimeoutSec: defaults.task_timeout_sec ?? 600,
|
|
1648
|
-
totalTimeoutSec: defaults.total_timeout_sec ?? 7200,
|
|
1649
|
-
maxTotalTokens: defaults.max_total_tokens ?? Infinity,
|
|
1650
|
-
maxPromptTokensPerAttempt: defaults.max_prompt_tokens_per_attempt ?? 128_000,
|
|
1651
|
-
autoCommit: defaults.auto_commit ?? true,
|
|
1652
|
-
branch: false,
|
|
1653
|
-
allowDirty: false,
|
|
1654
|
-
aggressiveCleanOnFail: false,
|
|
1655
|
-
verifyAi: defaults.verify_ai ?? true,
|
|
1656
|
-
verifyModel: undefined,
|
|
1657
|
-
decompose: defaults.decompose ?? true,
|
|
1658
|
-
maxDecomposeDepth: defaults.max_decompose_depth ?? 2,
|
|
1659
|
-
maxTotalTasks: defaults.max_total_tasks ?? 500,
|
|
1660
|
-
buildCommand: defaults.build_command ?? undefined,
|
|
1661
|
-
testCommand: defaults.test_command ?? undefined,
|
|
1662
|
-
lintCommand: defaults.lint_command ?? undefined,
|
|
1663
|
-
skipOnFail: defaults.skip_on_fail ?? false,
|
|
1664
|
-
skipOnBlocked: defaults.skip_on_blocked ?? true,
|
|
1665
|
-
rollbackOnFail: defaults.rollback_on_fail ?? false,
|
|
1666
|
-
maxIdenticalFailures: defaults.max_identical_failures ?? 5,
|
|
1667
|
-
approvalMode: (defaults.approval_mode ?? 'yolo'),
|
|
1668
|
-
verbose: false,
|
|
1669
|
-
dryRun: false,
|
|
1670
|
-
};
|
|
1671
|
-
const abortSignal = { aborted: false };
|
|
1672
|
-
managed.antonActive = true;
|
|
1673
|
-
managed.antonAbortSignal = abortSignal;
|
|
1674
|
-
managed.antonProgress = null;
|
|
1675
|
-
let lastProgressAt = 0;
|
|
1676
|
-
const channel = msg.channel;
|
|
1677
|
-
const progress = {
|
|
1678
|
-
onTaskStart(task, attempt, prog) {
|
|
1679
|
-
managed.antonProgress = prog;
|
|
1680
|
-
managed.lastActivity = Date.now();
|
|
1681
|
-
const now = Date.now();
|
|
1682
|
-
if (now - lastProgressAt >= DISCORD_RATE_LIMIT_MS) {
|
|
1683
|
-
lastProgressAt = now;
|
|
1684
|
-
channel.send(formatTaskStart(task, attempt, prog)).catch(() => { });
|
|
1685
|
-
}
|
|
1686
|
-
},
|
|
1687
|
-
onTaskEnd(task, result, prog) {
|
|
1688
|
-
managed.antonProgress = prog;
|
|
1689
|
-
managed.lastActivity = Date.now();
|
|
1690
|
-
const now = Date.now();
|
|
1691
|
-
if (now - lastProgressAt >= DISCORD_RATE_LIMIT_MS) {
|
|
1692
|
-
lastProgressAt = now;
|
|
1693
|
-
channel.send(formatTaskEnd(task, result, prog)).catch(() => { });
|
|
1694
|
-
}
|
|
1695
|
-
},
|
|
1696
|
-
onTaskSkip(task, reason) {
|
|
1697
|
-
managed.lastActivity = Date.now();
|
|
1698
|
-
channel.send(formatTaskSkip(task, reason)).catch(() => { });
|
|
1699
|
-
},
|
|
1700
|
-
onRunComplete(result) {
|
|
1701
|
-
managed.lastActivity = Date.now();
|
|
1702
|
-
managed.antonLastResult = result;
|
|
1703
|
-
managed.antonActive = false;
|
|
1704
|
-
managed.antonAbortSignal = null;
|
|
1705
|
-
managed.antonProgress = null;
|
|
1706
|
-
channel.send(formatRunSummary(result)).catch(() => { });
|
|
1707
|
-
},
|
|
1708
|
-
onHeartbeat() {
|
|
1709
|
-
managed.lastActivity = Date.now();
|
|
1710
|
-
},
|
|
1711
|
-
onToolLoop(taskText, event) {
|
|
1712
|
-
managed.lastActivity = Date.now();
|
|
1713
|
-
if (defaults.progress_events !== false) {
|
|
1714
|
-
channel.send(formatToolLoopEvent(taskText, event)).catch(() => { });
|
|
1715
|
-
}
|
|
1716
|
-
},
|
|
1717
|
-
onCompaction(taskText, event) {
|
|
1718
|
-
managed.lastActivity = Date.now();
|
|
1719
|
-
// Only send for significant compactions to avoid noise
|
|
1720
|
-
if (defaults.progress_events !== false && event.droppedMessages >= 5) {
|
|
1721
|
-
channel.send(formatCompactionEvent(taskText, event)).catch(() => { });
|
|
1722
|
-
}
|
|
1723
|
-
},
|
|
1724
|
-
onVerification(taskText, verification) {
|
|
1725
|
-
managed.lastActivity = Date.now();
|
|
1726
|
-
// Only send for failures — successes are already reported in onTaskEnd
|
|
1727
|
-
if (defaults.progress_events !== false && !verification.passed) {
|
|
1728
|
-
channel.send(formatVerificationDetail(taskText, verification)).catch(() => { });
|
|
1729
|
-
}
|
|
1730
|
-
},
|
|
1731
|
-
};
|
|
1732
|
-
let pendingCount = 0;
|
|
1733
|
-
try {
|
|
1734
|
-
const tf = await parseTaskFile(filePath);
|
|
1735
|
-
pendingCount = tf.pending.length;
|
|
1736
|
-
}
|
|
1737
|
-
catch { }
|
|
1738
|
-
await sendUserVisible(msg, `🤖 Anton started on ${filePart} (${pendingCount} tasks pending)`).catch(() => { });
|
|
1739
|
-
runAnton({
|
|
1740
|
-
config: runConfig,
|
|
1741
|
-
idlehandsConfig: managed.config,
|
|
1742
|
-
progress,
|
|
1743
|
-
abortSignal,
|
|
1744
|
-
vault: managed.session.vault,
|
|
1745
|
-
lens: managed.session.lens,
|
|
1746
|
-
}).catch((err) => {
|
|
1747
|
-
managed.lastActivity = Date.now();
|
|
1748
|
-
managed.antonActive = false;
|
|
1749
|
-
managed.antonAbortSignal = null;
|
|
1750
|
-
managed.antonProgress = null;
|
|
1751
|
-
channel.send(`Anton error: ${err.message}`).catch(() => { });
|
|
1752
|
-
});
|
|
1753
|
-
}
|
|
1754
1001
|
const shutdown = async () => {
|
|
1755
1002
|
clearInterval(cleanupTimer);
|
|
1756
1003
|
for (const key of sessions.keys())
|