@visorcraft/idlehands 1.1.6 → 1.1.8

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 (122) hide show
  1. package/README.md +32 -0
  2. package/dist/agent/formatting.js +251 -0
  3. package/dist/agent/formatting.js.map +1 -0
  4. package/dist/agent/review-artifact.js +147 -0
  5. package/dist/agent/review-artifact.js.map +1 -0
  6. package/dist/agent/tool-calls.js +226 -0
  7. package/dist/agent/tool-calls.js.map +1 -0
  8. package/dist/agent.js +314 -695
  9. package/dist/agent.js.map +1 -1
  10. package/dist/anton/controller.js +1 -1
  11. package/dist/anton/controller.js.map +1 -1
  12. package/dist/anton/lock.js +0 -3
  13. package/dist/anton/lock.js.map +1 -1
  14. package/dist/anton/parser.js +0 -1
  15. package/dist/anton/parser.js.map +1 -1
  16. package/dist/anton/reporter.js +1 -1
  17. package/dist/anton/reporter.js.map +1 -1
  18. package/dist/bot/commands.js +3 -2
  19. package/dist/bot/commands.js.map +1 -1
  20. package/dist/bot/confirm-telegram.js +2 -1
  21. package/dist/bot/confirm-telegram.js.map +1 -1
  22. package/dist/bot/discord-routing.js +179 -0
  23. package/dist/bot/discord-routing.js.map +1 -0
  24. package/dist/bot/discord-streaming.js +171 -0
  25. package/dist/bot/discord-streaming.js.map +1 -0
  26. package/dist/bot/discord.js +25 -221
  27. package/dist/bot/discord.js.map +1 -1
  28. package/dist/bot/format.js +2 -25
  29. package/dist/bot/format.js.map +1 -1
  30. package/dist/bot/telegram.js +56 -12
  31. package/dist/bot/telegram.js.map +1 -1
  32. package/dist/cli/args.js +4 -1
  33. package/dist/cli/args.js.map +1 -1
  34. package/dist/cli/build-repl-context.js.map +1 -1
  35. package/dist/cli/command-registry.js +2 -1
  36. package/dist/cli/command-registry.js.map +1 -1
  37. package/dist/cli/command-utils.js +27 -0
  38. package/dist/cli/command-utils.js.map +1 -0
  39. package/dist/cli/commands/anton.js +3 -2
  40. package/dist/cli/commands/anton.js.map +1 -1
  41. package/dist/cli/commands/model.js +8 -7
  42. package/dist/cli/commands/model.js.map +1 -1
  43. package/dist/cli/commands/project.js +5 -4
  44. package/dist/cli/commands/project.js.map +1 -1
  45. package/dist/cli/commands/session.js +118 -8
  46. package/dist/cli/commands/session.js.map +1 -1
  47. package/dist/cli/commands/tools.js +4 -3
  48. package/dist/cli/commands/tools.js.map +1 -1
  49. package/dist/cli/input.js +2 -1
  50. package/dist/cli/input.js.map +1 -1
  51. package/dist/cli/repl-dispatch.js +85 -0
  52. package/dist/cli/repl-dispatch.js.map +1 -0
  53. package/dist/cli/runtime-cmds.js +7 -7
  54. package/dist/cli/runtime-cmds.js.map +1 -1
  55. package/dist/cli/service.js +0 -14
  56. package/dist/cli/service.js.map +1 -1
  57. package/dist/cli/setup.js +25 -5
  58. package/dist/cli/setup.js.map +1 -1
  59. package/dist/cli/watch.js +2 -1
  60. package/dist/cli/watch.js.map +1 -1
  61. package/dist/client.js +51 -4
  62. package/dist/client.js.map +1 -1
  63. package/dist/config.js +79 -0
  64. package/dist/config.js.map +1 -1
  65. package/dist/context.js +101 -10
  66. package/dist/context.js.map +1 -1
  67. package/dist/harnesses.js +1 -1
  68. package/dist/harnesses.js.map +1 -1
  69. package/dist/hooks/index.js +5 -0
  70. package/dist/hooks/index.js.map +1 -0
  71. package/dist/hooks/loader.js +58 -0
  72. package/dist/hooks/loader.js.map +1 -0
  73. package/dist/hooks/manager.js +180 -0
  74. package/dist/hooks/manager.js.map +1 -0
  75. package/dist/hooks/plugins/example-console.js +24 -0
  76. package/dist/hooks/plugins/example-console.js.map +1 -0
  77. package/dist/hooks/scaffold.js +53 -0
  78. package/dist/hooks/scaffold.js.map +1 -0
  79. package/dist/hooks/types.js +8 -0
  80. package/dist/hooks/types.js.map +1 -0
  81. package/dist/index.js +16 -64
  82. package/dist/index.js.map +1 -1
  83. package/dist/progress/agent-hooks.js +37 -0
  84. package/dist/progress/agent-hooks.js.map +1 -0
  85. package/dist/progress/ir.js +7 -0
  86. package/dist/progress/ir.js.map +1 -0
  87. package/dist/progress/progress-message-renderer.js +63 -0
  88. package/dist/progress/progress-message-renderer.js.map +1 -0
  89. package/dist/progress/serialize-discord.js +60 -0
  90. package/dist/progress/serialize-discord.js.map +1 -0
  91. package/dist/progress/serialize-telegram.js +55 -0
  92. package/dist/progress/serialize-telegram.js.map +1 -0
  93. package/dist/progress/serialize-tui.js +39 -0
  94. package/dist/progress/serialize-tui.js.map +1 -0
  95. package/dist/progress/tool-summary.js +58 -0
  96. package/dist/progress/tool-summary.js.map +1 -0
  97. package/dist/progress/tool-tail.js +48 -0
  98. package/dist/progress/tool-tail.js.map +1 -0
  99. package/dist/progress/turn-progress.js +215 -0
  100. package/dist/progress/turn-progress.js.map +1 -0
  101. package/dist/replay.js +2 -5
  102. package/dist/replay.js.map +1 -1
  103. package/dist/safety.js +0 -1
  104. package/dist/safety.js.map +1 -1
  105. package/dist/spinner.js +8 -0
  106. package/dist/spinner.js.map +1 -1
  107. package/dist/tools.js +422 -29
  108. package/dist/tools.js.map +1 -1
  109. package/dist/tui/branch-picker.js.map +1 -1
  110. package/dist/tui/command-handler.js.map +1 -1
  111. package/dist/tui/controller.js +417 -33
  112. package/dist/tui/controller.js.map +1 -1
  113. package/dist/tui/keymap.js +15 -0
  114. package/dist/tui/keymap.js.map +1 -1
  115. package/dist/tui/render.js +115 -3
  116. package/dist/tui/render.js.map +1 -1
  117. package/dist/tui/state.js +82 -1
  118. package/dist/tui/state.js.map +1 -1
  119. package/dist/upgrade.js.map +1 -1
  120. package/dist/utils.js +17 -0
  121. package/dist/utils.js.map +1 -1
  122. package/package.json +1 -1
@@ -0,0 +1,171 @@
1
+ import { splitDiscord, safeContent } from './discord-routing.js';
2
+ import { formatToolCallSummary } from '../progress/tool-summary.js';
3
+ import { TurnProgressController } from '../progress/turn-progress.js';
4
+ import { ToolTailBuffer } from '../progress/tool-tail.js';
5
+ import { ProgressMessageRenderer } from '../progress/progress-message-renderer.js';
6
+ import { renderDiscordMarkdown } from '../progress/serialize-discord.js';
7
+ export class DiscordStreamingMessage {
8
+ placeholder;
9
+ channel;
10
+ opts;
11
+ buffer = '';
12
+ toolLines = [];
13
+ lastToolLine = '';
14
+ lastToolRepeat = 0;
15
+ statusLine = '⏳ Thinking...';
16
+ banner = null;
17
+ tails = new ToolTailBuffer({ maxChars: 4096, maxLines: 4 });
18
+ activeToolId = null;
19
+ timer = null;
20
+ dirty = true;
21
+ finalized = false;
22
+ progress = new TurnProgressController((snap) => {
23
+ this.statusLine = snap.statusLine;
24
+ this.dirty = true;
25
+ }, {
26
+ heartbeatMs: 1000,
27
+ bucketMs: 5000,
28
+ maxToolLines: 8,
29
+ toolCallSummary: (c) => formatToolCallSummary({ name: c.name, args: c.args }),
30
+ });
31
+ renderer = new ProgressMessageRenderer({
32
+ maxToolLines: 6,
33
+ maxTailLines: 4,
34
+ maxAssistantChars: 1200,
35
+ });
36
+ constructor(placeholder, channel, opts) {
37
+ this.placeholder = placeholder;
38
+ this.channel = channel;
39
+ this.opts = opts;
40
+ }
41
+ start() {
42
+ if (this.timer)
43
+ return;
44
+ this.progress.start();
45
+ const every = Math.max(500, Math.floor(this.opts?.editIntervalMs ?? 1500));
46
+ this.timer = setInterval(() => void this.flush(), every);
47
+ }
48
+ stop() {
49
+ if (this.timer) {
50
+ clearInterval(this.timer);
51
+ this.timer = null;
52
+ }
53
+ this.progress.stop();
54
+ }
55
+ setBanner(text) {
56
+ this.banner = text?.trim() ? text.trim() : null;
57
+ this.dirty = true;
58
+ }
59
+ hooks() {
60
+ return {
61
+ onToken: (t) => {
62
+ this.buffer += t;
63
+ this.progress.hooks.onToken?.(t);
64
+ this.dirty = true;
65
+ },
66
+ onToolCall: (call) => {
67
+ this.progress.hooks.onToolCall?.(call);
68
+ this.activeToolId = call.id;
69
+ this.tails.reset(call.id, call.name);
70
+ const line = `◆ ${formatToolCallSummary({ name: call.name, args: call.args })}...`;
71
+ if (this.lastToolLine === line && this.toolLines.length > 0) {
72
+ this.lastToolRepeat += 1;
73
+ this.toolLines[this.toolLines.length - 1] = `${line} (x${this.lastToolRepeat + 1})`;
74
+ }
75
+ else {
76
+ this.lastToolLine = line;
77
+ this.lastToolRepeat = 0;
78
+ this.toolLines.push(line);
79
+ if (this.toolLines.length > 8)
80
+ this.toolLines.splice(0, this.toolLines.length - 8);
81
+ }
82
+ this.dirty = true;
83
+ },
84
+ onToolStream: (ev) => {
85
+ if (!this.activeToolId || ev.id !== this.activeToolId)
86
+ return;
87
+ this.tails.push(ev);
88
+ this.dirty = true;
89
+ },
90
+ onToolResult: (result) => {
91
+ this.progress.hooks.onToolResult?.(result);
92
+ this.lastToolLine = '';
93
+ this.lastToolRepeat = 0;
94
+ if (this.toolLines.length > 0) {
95
+ const icon = result.success ? '✓' : '✗';
96
+ this.toolLines[this.toolLines.length - 1] = `${icon} ${result.name}: ${result.summary}`;
97
+ }
98
+ if (this.activeToolId === result.id) {
99
+ this.tails.clear(result.id);
100
+ this.activeToolId = null;
101
+ }
102
+ this.dirty = true;
103
+ },
104
+ onTurnEnd: (stats) => {
105
+ this.progress.hooks.onTurnEnd?.(stats);
106
+ },
107
+ };
108
+ }
109
+ renderProgressText() {
110
+ const tail = this.activeToolId ? this.tails.get(this.activeToolId) : null;
111
+ const doc = this.renderer.render({
112
+ banner: this.banner,
113
+ statusLine: this.statusLine || '⏳ Thinking...',
114
+ toolLines: this.toolLines,
115
+ toolTail: tail ? { name: tail.name, stream: tail.stream, lines: tail.lines } : null,
116
+ assistantMarkdown: this.buffer.trim() ? safeContent(this.buffer) : null,
117
+ });
118
+ return renderDiscordMarkdown(doc, { maxLen: 1900 });
119
+ }
120
+ async flush() {
121
+ if (this.finalized)
122
+ return;
123
+ if (!this.dirty)
124
+ return;
125
+ this.dirty = false;
126
+ const text = this.renderProgressText();
127
+ try {
128
+ if (this.placeholder) {
129
+ await this.placeholder.edit(text);
130
+ }
131
+ }
132
+ catch {
133
+ // ignore edit failures
134
+ }
135
+ }
136
+ async finalize(finalText) {
137
+ this.finalized = true;
138
+ this.stop();
139
+ const snap = this.progress.snapshot('stop');
140
+ const toolLines = snap.toolLines.slice(-8);
141
+ const combined = safeContent((toolLines.length ? toolLines.join('\n') + '\n\n' : '') + (finalText ?? ''));
142
+ const chunks = splitDiscord(combined);
143
+ if (this.placeholder && chunks.length > 0) {
144
+ await this.placeholder.edit(chunks[0]).catch(() => { });
145
+ }
146
+ else if (chunks.length > 0) {
147
+ await this.channel.send(chunks[0]).catch(() => { });
148
+ }
149
+ for (let i = 1; i < chunks.length && i < 10; i++) {
150
+ await this.channel.send(chunks[i]).catch(() => { });
151
+ }
152
+ if (chunks.length > 10) {
153
+ await this.channel.send('[truncated — response too long]').catch(() => { });
154
+ }
155
+ }
156
+ async finalizeError(errMsg) {
157
+ this.finalized = true;
158
+ this.stop();
159
+ const snap = this.progress.snapshot('stop');
160
+ const toolLines = snap.toolLines.slice(-8);
161
+ const combined = safeContent((toolLines.length ? toolLines.join('\n') + '\n\n' : '') + `❌ ${errMsg}`);
162
+ const chunks = splitDiscord(combined);
163
+ if (this.placeholder && chunks.length > 0) {
164
+ await this.placeholder.edit(chunks[0]).catch(() => { });
165
+ }
166
+ else if (chunks.length > 0) {
167
+ await this.channel.send(chunks[0]).catch(() => { });
168
+ }
169
+ }
170
+ }
171
+ //# sourceMappingURL=discord-streaming.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"discord-streaming.js","sourceRoot":"","sources":["../../src/bot/discord-streaming.ts"],"names":[],"mappings":"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"}
@@ -1,7 +1,8 @@
1
1
  import { Client, Events, GatewayIntentBits, Partials, REST, Routes, SlashCommandBuilder, } from 'discord.js';
2
2
  import { createSession } from '../agent.js';
3
3
  import { DiscordConfirmProvider } from './confirm-discord.js';
4
- import { sanitizeBotOutputText } from './format.js';
4
+ import { parseAllowedUsers, normalizeApprovalMode, splitDiscord, safeContent, detectEscalation, checkKeywordEscalation, resolveAgentForMessage, sessionKeyForMessage, } from './discord-routing.js';
5
+ import { firstToken } from '../cli/command-utils.js';
5
6
  import { projectDir } from '../utils.js';
6
7
  import { WATCHDOG_RECOMMENDED_TUNING_TEXT, formatWatchdogCancelMessage, resolveWatchdogSettings, shouldRecommendWatchdogTuning } from '../watchdog.js';
7
8
  import path from 'node:path';
@@ -9,183 +10,8 @@ import fs from 'node:fs/promises';
9
10
  import { runAnton } from '../anton/controller.js';
10
11
  import { parseTaskFile } from '../anton/parser.js';
11
12
  import { formatRunSummary, formatProgressBar, formatTaskStart, formatTaskEnd, formatTaskSkip } from '../anton/reporter.js';
12
- function parseAllowedUsers(cfg) {
13
- const fromEnv = process.env.IDLEHANDS_DISCORD_ALLOWED_USERS;
14
- if (fromEnv && fromEnv.trim()) {
15
- return new Set(fromEnv
16
- .split(',')
17
- .map((s) => s.trim())
18
- .filter(Boolean));
19
- }
20
- const values = Array.isArray(cfg.allowed_users) ? cfg.allowed_users : [];
21
- return new Set(values.map((v) => String(v).trim()).filter(Boolean));
22
- }
23
- function normalizeApprovalMode(mode, fallback) {
24
- const m = String(mode ?? '').trim().toLowerCase();
25
- if (m === 'plan' || m === 'default' || m === 'auto-edit' || m === 'yolo')
26
- return m;
27
- return fallback;
28
- }
29
- function splitDiscord(text, limit = 1900) {
30
- if (text.length <= limit)
31
- return [text];
32
- const chunks = [];
33
- let i = 0;
34
- while (i < text.length) {
35
- chunks.push(text.slice(i, i + limit));
36
- i += limit;
37
- }
38
- return chunks;
39
- }
40
- function safeContent(text) {
41
- const t = sanitizeBotOutputText(text).trim();
42
- return t.length ? t : '(empty response)';
43
- }
44
- /**
45
- * Check if the model response contains an escalation request.
46
- * Returns { escalate: true, reason: string } if escalation marker found at start of response.
47
- */
48
- function detectEscalation(text) {
49
- const trimmed = text.trim();
50
- const match = trimmed.match(/^\[ESCALATE:\s*([^\]]+)\]/i);
51
- if (match) {
52
- return { escalate: true, reason: match[1].trim() };
53
- }
54
- return { escalate: false };
55
- }
56
- /** Keyword presets for common escalation triggers */
57
- const KEYWORD_PRESETS = {
58
- coding: ['build', 'implement', 'create', 'develop', 'architect', 'refactor', 'debug', 'fix', 'code', 'program', 'write'],
59
- planning: ['plan', 'design', 'roadmap', 'strategy', 'analyze', 'research', 'evaluate', 'compare'],
60
- complex: ['full', 'complete', 'comprehensive', 'multi-step', 'integrate', 'migration', 'overhaul', 'entire', 'whole'],
61
- };
62
- /**
63
- * Check if text matches a set of keywords.
64
- * Returns matched keywords or empty array if none match.
65
- */
66
- function matchKeywords(text, keywords, presets) {
67
- const allKeywords = [...keywords];
68
- // Add preset keywords
69
- if (presets) {
70
- for (const preset of presets) {
71
- const presetWords = KEYWORD_PRESETS[preset];
72
- if (presetWords)
73
- allKeywords.push(...presetWords);
74
- }
75
- }
76
- if (allKeywords.length === 0)
77
- return [];
78
- const lowerText = text.toLowerCase();
79
- const matched = [];
80
- for (const kw of allKeywords) {
81
- if (kw.startsWith('re:')) {
82
- // Regex pattern
83
- try {
84
- const regex = new RegExp(kw.slice(3), 'i');
85
- if (regex.test(text))
86
- matched.push(kw);
87
- }
88
- catch {
89
- // Invalid regex, skip
90
- }
91
- }
92
- else {
93
- // Word boundary match (case-insensitive)
94
- const wordRegex = new RegExp(`\\b${kw.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'i');
95
- if (wordRegex.test(lowerText))
96
- matched.push(kw);
97
- }
98
- }
99
- return matched;
100
- }
101
- /**
102
- * Check if user message matches keyword escalation triggers.
103
- * Returns { escalate: true, tier: number, reason: string } if keywords match.
104
- * Tier indicates which model index to escalate to (highest matching tier wins).
105
- */
106
- function checkKeywordEscalation(text, escalation) {
107
- if (!escalation)
108
- return { escalate: false };
109
- // Tiered keyword escalation
110
- if (escalation.tiers && escalation.tiers.length > 0) {
111
- let highestTier = -1;
112
- let highestReason = '';
113
- // Check each tier, highest matching tier wins
114
- for (let i = 0; i < escalation.tiers.length; i++) {
115
- const tier = escalation.tiers[i];
116
- const matched = matchKeywords(text, tier.keywords || [], tier.keyword_presets);
117
- if (matched.length > 0 && i > highestTier) {
118
- highestTier = i;
119
- highestReason = `tier ${i} keyword match: ${matched.slice(0, 3).join(', ')}${matched.length > 3 ? '...' : ''}`;
120
- }
121
- }
122
- if (highestTier >= 0) {
123
- return { escalate: true, tier: highestTier, reason: highestReason };
124
- }
125
- return { escalate: false };
126
- }
127
- // Legacy flat keywords (treated as tier 0)
128
- const matched = matchKeywords(text, escalation.keywords || [], escalation.keyword_presets);
129
- if (matched.length > 0) {
130
- return {
131
- escalate: true,
132
- tier: 0,
133
- reason: `keyword match: ${matched.slice(0, 3).join(', ')}${matched.length > 3 ? '...' : ''}`
134
- };
135
- }
136
- return { escalate: false };
137
- }
138
- /**
139
- * Resolve which agent persona should handle a message.
140
- * Priority: user > channel > guild > default > first agent > null
141
- */
142
- function resolveAgentForMessage(msg, agents, routing) {
143
- const agentMap = agents ?? {};
144
- const agentIds = Object.keys(agentMap);
145
- // No agents configured — return null persona (use global config)
146
- if (agentIds.length === 0) {
147
- return { agentId: '_default', persona: null };
148
- }
149
- const route = routing ?? {};
150
- let resolvedId;
151
- // Priority 1: User-specific routing
152
- if (route.users && route.users[msg.author.id]) {
153
- resolvedId = route.users[msg.author.id];
154
- }
155
- // Priority 2: Channel-specific routing
156
- else if (route.channels && route.channels[msg.channelId]) {
157
- resolvedId = route.channels[msg.channelId];
158
- }
159
- // Priority 3: Guild-specific routing
160
- else if (msg.guildId && route.guilds && route.guilds[msg.guildId]) {
161
- resolvedId = route.guilds[msg.guildId];
162
- }
163
- // Priority 4: Default agent
164
- else if (route.default) {
165
- resolvedId = route.default;
166
- }
167
- // Priority 5: First defined agent
168
- else {
169
- resolvedId = agentIds[0];
170
- }
171
- // Validate the resolved agent exists
172
- const persona = agentMap[resolvedId];
173
- if (!persona) {
174
- // Fallback to first agent if routing points to non-existent agent
175
- const fallbackId = agentIds[0];
176
- return { agentId: fallbackId, persona: agentMap[fallbackId] ?? null };
177
- }
178
- return { agentId: resolvedId, persona };
179
- }
180
- function sessionKeyForMessage(msg, allowGuilds, agentId) {
181
- // Include agentId in session key so switching agents creates a new session
182
- if (allowGuilds) {
183
- // Per-agent+channel+user session in guilds
184
- return `${agentId}:${msg.channelId}:${msg.author.id}`;
185
- }
186
- // DM-only mode: per-agent+user session
187
- return `${agentId}:${msg.author.id}`;
188
- }
13
+ import { DiscordStreamingMessage } from './discord-streaming.js';
14
+ import { chainAgentHooks } from '../progress/agent-hooks.js';
189
15
  export async function startDiscordBot(config, botConfig) {
190
16
  const token = process.env.IDLEHANDS_DISCORD_TOKEN || botConfig.token;
191
17
  if (!token) {
@@ -489,14 +315,14 @@ When you escalate, your request will be re-run on a more capable model.`;
489
315
  }
490
316
  }
491
317
  const placeholder = await sendUserVisible(msg, '⏳ Thinking...').catch(() => null);
492
- let streamed = '';
493
- const hooks = {
494
- onToken: (t) => {
318
+ const streamer = new DiscordStreamingMessage(placeholder, msg.channel, { editIntervalMs: 1500 });
319
+ streamer.start();
320
+ const baseHooks = {
321
+ onToken: () => {
495
322
  if (!isTurnActive(managed, turnId))
496
323
  return;
497
324
  markProgress(managed, turnId);
498
325
  watchdogGraceUsed = 0;
499
- streamed += t;
500
326
  },
501
327
  onToolCall: () => {
502
328
  if (!isTurnActive(managed, turnId))
@@ -504,6 +330,12 @@ When you escalate, your request will be re-run on a more capable model.`;
504
330
  markProgress(managed, turnId);
505
331
  watchdogGraceUsed = 0;
506
332
  },
333
+ onToolStream: () => {
334
+ if (!isTurnActive(managed, turnId))
335
+ return;
336
+ markProgress(managed, turnId);
337
+ watchdogGraceUsed = 0;
338
+ },
507
339
  onToolResult: () => {
508
340
  if (!isTurnActive(managed, turnId))
509
341
  return;
@@ -530,9 +362,7 @@ When you escalate, your request will be re-run on a more capable model.`;
530
362
  watchdogGraceUsed += 1;
531
363
  managed.lastProgressAt = Date.now();
532
364
  console.error(`[bot:discord] ${managed.userId} watchdog inactivity on turn ${turnId} — applying grace period (${watchdogGraceUsed}/${watchdogIdleGraceTimeouts})`);
533
- if (placeholder) {
534
- void placeholder.edit('⏳ Still working... model is taking longer than usual.').catch(() => { });
535
- }
365
+ streamer.setBanner('⏳ Still working... model is taking longer than usual.');
536
366
  return;
537
367
  }
538
368
  if (managed.watchdogCompactAttempts < maxWatchdogCompacts) {
@@ -568,17 +398,17 @@ When you escalate, your request will be re-run on a more capable model.`;
568
398
  const attemptController = new AbortController();
569
399
  managed.activeAbortController = attemptController;
570
400
  turn.controller = attemptController;
571
- streamed = '';
572
401
  const askText = isRetryAfterCompaction
573
402
  ? 'Continue working on the task from where you left off. Context was compacted to free memory — do NOT restart from the beginning.'
574
403
  : msg.content;
404
+ const hooks = chainAgentHooks({ signal: attemptController.signal }, baseHooks, streamer.hooks());
575
405
  try {
576
- const result = await managed.session.ask(askText, { ...hooks, signal: attemptController.signal });
406
+ const result = await managed.session.ask(askText, hooks);
577
407
  askComplete = true;
578
408
  if (!isTurnActive(managed, turnId))
579
409
  return;
580
410
  markProgress(managed, turnId);
581
- const finalText = safeContent(streamed || result.text);
411
+ const finalText = safeContent(result.text);
582
412
  // Check for auto-escalation request in response
583
413
  const escalation = managed.agentPersona?.escalation;
584
414
  const autoEscalate = escalation?.auto !== false && escalation?.models?.length;
@@ -592,10 +422,7 @@ When you escalate, your request will be re-run on a more capable model.`;
592
422
  // Get endpoint from tier if defined
593
423
  const tierEndpoint = escalation.tiers?.[nextIndex]?.endpoint;
594
424
  console.error(`[bot:discord] ${managed.userId} auto-escalation requested: ${escResult.reason}${tierEndpoint ? ` @ ${tierEndpoint}` : ''}`);
595
- // Update placeholder with escalation notice
596
- if (placeholder) {
597
- await placeholder.edit(`⚡ Escalating to \`${targetModel}\` (${escResult.reason})...`).catch(() => { });
598
- }
425
+ await streamer.finalizeError(`⚡ Escalating to \`${targetModel}\` (${escResult.reason})...`);
599
426
  // Set up escalation for re-run
600
427
  managed.pendingEscalation = targetModel;
601
428
  managed.currentModelIndex = nextIndex + 1;
@@ -615,30 +442,14 @@ When you escalate, your request will be re-run on a more capable model.`;
615
442
  return;
616
443
  }
617
444
  }
618
- const chunks = splitDiscord(finalText);
619
- if (placeholder) {
620
- await placeholder.edit(chunks[0]).catch(() => { });
621
- }
622
- else {
623
- await sendUserVisible(msg, chunks[0]).catch(() => { });
624
- }
625
- for (let i = 1; i < chunks.length && i < 10; i++) {
626
- if (!isTurnActive(managed, turnId))
627
- break;
628
- await msg.channel.send(chunks[i]).catch(() => { });
629
- }
630
- if (chunks.length > 10 && isTurnActive(managed, turnId)) {
631
- await msg.channel.send('[truncated — response too long]').catch(() => { });
632
- }
445
+ await streamer.finalize(finalText);
633
446
  }
634
447
  catch (e) {
635
448
  const raw = String(e?.message ?? e ?? 'unknown error');
636
449
  const isAbort = raw.includes('AbortError') || raw.toLowerCase().includes('aborted');
637
450
  // If aborted by watchdog compaction, wait for compaction to finish then retry
638
451
  if (isAbort && watchdogCompactPending) {
639
- if (placeholder) {
640
- await placeholder.edit(`🔄 Context too large — compacting and retrying (attempt ${managed.watchdogCompactAttempts}/${maxWatchdogCompacts})...`).catch(() => { });
641
- }
452
+ streamer.setBanner(`🔄 Context too large — compacting and retrying (attempt ${managed.watchdogCompactAttempts}/${maxWatchdogCompacts})...`);
642
453
  // Wait for the async compaction to complete
643
454
  while (watchdogCompactPending) {
644
455
  await new Promise((r) => setTimeout(r, 500));
@@ -658,19 +469,11 @@ When you escalate, your request will be re-run on a more capable model.`;
658
469
  abortReason: raw,
659
470
  prefix: '⏹ ',
660
471
  });
661
- if (placeholder)
662
- await placeholder.edit(cancelMsg).catch(() => { });
663
- else
664
- await sendUserVisible(msg, cancelMsg).catch(() => { });
472
+ await streamer.finalizeError(cancelMsg);
665
473
  }
666
474
  else {
667
475
  const errMsg = raw.slice(0, 400);
668
- if (placeholder) {
669
- await placeholder.edit(`❌ ${errMsg}`).catch(() => { });
670
- }
671
- else {
672
- await sendUserVisible(msg, `❌ ${errMsg}`).catch(() => { });
673
- }
476
+ await streamer.finalizeError(errMsg);
674
477
  }
675
478
  askComplete = true;
676
479
  }
@@ -678,6 +481,7 @@ When you escalate, your request will be re-run on a more capable model.`;
678
481
  }
679
482
  finally {
680
483
  clearInterval(watchdog);
484
+ streamer.stop();
681
485
  finishTurn(managed, turnId);
682
486
  // Auto-deescalate back to base model after each request
683
487
  if (managed.currentModelIndex > 0 && managed.agentPersona?.escalation) {
@@ -1539,7 +1343,7 @@ When you escalate, your request will be re-run on a more capable model.`;
1539
1343
  const DISCORD_RATE_LIMIT_MS = 15_000;
1540
1344
  async function handleDiscordAnton(managed, msg, content) {
1541
1345
  const args = content.replace(/^\/anton\s*/, '').trim();
1542
- const sub = args.split(/\s+/)[0]?.toLowerCase() || '';
1346
+ const sub = firstToken(args);
1543
1347
  if (!sub || sub === 'status') {
1544
1348
  if (!managed.antonActive) {
1545
1349
  await sendUserVisible(msg, 'No Anton run in progress.').catch(() => { });