beast-agent 0.25.6 → 0.25.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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "beast-agent",
3
3
  "productName": "Beast Agent",
4
- "version": "0.25.6",
4
+ "version": "0.25.8",
5
5
  "description": "Ultra-fast local agent shell for Windows.",
6
6
  "author": "algokodcom (AlgoKod)",
7
7
  "license": "MIT",
package/src/agent/bots.js CHANGED
@@ -327,6 +327,14 @@ function update(id, patch) {
327
327
  changes.push('dış tarayıcı komutu güncellendi');
328
328
  b.extCommand = String(patch.extCommand).slice(0, 200);
329
329
  }
330
+ if (typeof patch.model === 'string') {
331
+ /* bot bazlı model override — boş string = global seçim */
332
+ const next = patch.model.trim().slice(0, 160);
333
+ if (next !== (b.model || '')) {
334
+ changes.push('model güncellendi');
335
+ b.model = next;
336
+ }
337
+ }
330
338
  if (Array.isArray(patch.plugins)) {
331
339
  const next = [...new Set(patch.plugins.map((p) => String(p).slice(0, 40)).filter((p) => PLUGIN_LIST.includes(p)))];
332
340
  if (JSON.stringify(next) !== JSON.stringify(b.plugins || [])) {
@@ -0,0 +1,234 @@
1
+ 'use strict';
2
+
3
+ /* DISCORD KÖPRÜSÜ — Telegram köprüsünün Discord hali.
4
+ Bağımlılık: yalnızca `ws` (paket zaten kurulu — baileys ile geliyor).
5
+ - Gateway WebSocket: Identify → READY → MESSAGE_CREATE dispatch
6
+ - REST (api/v10): mesaj gönderme (2000 karakter sınırında bölerek)
7
+ - Sunucu (guild) mesajlarında yalnız @mention'a cevap verilir (spam koruması);
8
+ DM'de her izinli kullanıcıya cevap verilir.
9
+ - Bağlantı koparsa backoff ile yeniden bağlanır (yeniden Identify;
10
+ çevrimdışıyken gelen mesajlar backfill edilmez — Telegram long-poll'un aksine).
11
+ ÖNEMLİ: Discord Developer Portal'da bot için "MESSAGE CONTENT INTENT"
12
+ açılmalıdır — yoksa mesaj içerikleri boş gelir. */
13
+
14
+ const WebSocket = require('ws');
15
+ const https = require('https');
16
+
17
+ const API_BASE = 'https://discord.com/api/v10';
18
+ const GATEWAY_URL = 'wss://gateway.discord.gg/?v=10&encoding=json';
19
+ const SEND_CHUNK = 1900; // Discord mesaj sınırı 2000 — güvenli pay
20
+ /* GUILDS(1) | GUILD_MESSAGES(512) | DIRECT_MESSAGES(4096) | MESSAGE_CONTENT(32768) */
21
+ const INTENTS = 1 | 512 | 4096 | 32768;
22
+
23
+ class DiscordBridge {
24
+ constructor({ token, emit, onIncoming }) {
25
+ this.token = String(token || '').trim();
26
+ this.emit = emit || (() => {});
27
+ this.onIncoming = onIncoming || null;
28
+ this.connected = false;
29
+ this.stopping = false;
30
+ this.status = 'disconnected';
31
+ this.user = null; // { id, username, ... }
32
+ this._ws = null;
33
+ this._hbTimer = null;
34
+ this._seq = null;
35
+ this._backoff = 2000;
36
+ }
37
+
38
+ _setStatus(status, user) {
39
+ this.status = status;
40
+ this.connected = status === 'connected';
41
+ this.emit({ type: 'status', status, user: user || null });
42
+ }
43
+
44
+ /* REST çağrısı — JSON (token: "Bot <token>") */
45
+ api(method, path, body) {
46
+ return new Promise((resolve, reject) => {
47
+ const payload = body ? JSON.stringify(body) : null;
48
+ const req = https.request(
49
+ `${API_BASE}${path}`,
50
+ {
51
+ method,
52
+ headers: {
53
+ 'Content-Type': 'application/json',
54
+ Authorization: 'Bot ' + this.token,
55
+ ...(payload ? { 'Content-Length': Buffer.byteLength(payload) } : {}),
56
+ },
57
+ timeout: 15000,
58
+ },
59
+ (res) => {
60
+ let data = '';
61
+ res.setEncoding('utf8');
62
+ res.on('data', (c) => (data += c));
63
+ res.on('end', () => {
64
+ try {
65
+ const j = data ? JSON.parse(data) : null;
66
+ if (res.statusCode >= 200 && res.statusCode < 300) resolve(j);
67
+ else reject(new Error(`discord ${path}: HTTP ${res.statusCode} ${(j && j.message) || ''}`.trim()));
68
+ } catch {
69
+ reject(new Error(`discord ${path}: bozuk yanıt`));
70
+ }
71
+ });
72
+ }
73
+ );
74
+ req.on('timeout', () => req.destroy(new Error('zaman aşımı')));
75
+ req.on('error', reject);
76
+ if (payload) req.write(payload);
77
+ req.end();
78
+ });
79
+ }
80
+
81
+ async start() {
82
+ if (!this.token) {
83
+ this._setStatus('error');
84
+ return false;
85
+ }
86
+ this.stopping = false;
87
+ this._setStatus('connecting');
88
+ try {
89
+ const me = await this.api('GET', '/users/@me');
90
+ this.user = me; // { id, username, ... }
91
+ this._setStatus('connected', '@' + (me.username || 'bot'));
92
+ } catch (e) {
93
+ this._setStatus('error');
94
+ throw e;
95
+ }
96
+ this._connect();
97
+ return true;
98
+ }
99
+
100
+ _connect() {
101
+ if (this.stopping) return;
102
+ clearInterval(this._hbTimer);
103
+ this._hbTimer = null;
104
+ let identified = false;
105
+ const ws = new WebSocket(GATEWAY_URL);
106
+ this._ws = ws;
107
+
108
+ ws.on('message', (raw) => {
109
+ let p = null;
110
+ try { p = JSON.parse(String(raw)); } catch { return; }
111
+ const op = p.op;
112
+ const d = p.d;
113
+ const t = p.t;
114
+ if (typeof p.s === 'number') this._seq = p.s;
115
+
116
+ if (op === 10) {
117
+ /* Hello: heartbeat aralığı gelıyor → hafif erken at (güvenli pay) */
118
+ const iv = (d && d.heartbeat_interval) || 41250;
119
+ clearInterval(this._hbTimer);
120
+ this._hbTimer = setInterval(() => {
121
+ try {
122
+ if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ op: 1, d: this._seq }));
123
+ } catch {}
124
+ }, Math.max(5000, Math.floor(iv * 0.8)));
125
+ if (!identified) {
126
+ identified = true;
127
+ try {
128
+ ws.send(
129
+ JSON.stringify({
130
+ op: 2,
131
+ d: {
132
+ token: this.token,
133
+ intents: INTENTS,
134
+ properties: { os: 'windows', browser: 'beast-agent', device: 'beast-agent' },
135
+ },
136
+ })
137
+ );
138
+ } catch {}
139
+ }
140
+ return;
141
+ }
142
+ if (op === 1) {
143
+ /* sunucu heartbeat istedi — hemen at */
144
+ try {
145
+ if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ op: 1, d: this._seq }));
146
+ } catch {}
147
+ return;
148
+ }
149
+ if (op === 9 || op === 7) {
150
+ /* invalid session / reconnect isteği — kapat, close handler yeniden bağlanır */
151
+ try { ws.close(); } catch {}
152
+ return;
153
+ }
154
+ if (op === 0 && t === 'READY') {
155
+ this._backoff = 2000;
156
+ this._setStatus('connected', '@' + ((d && d.user && d.user.username) || 'bot'));
157
+ return;
158
+ }
159
+ if (op === 0 && t === 'MESSAGE_CREATE' && this.onIncoming) {
160
+ try {
161
+ const m = d || {};
162
+ if (!m.author || m.author.bot || m.webhook_id) return;
163
+ let text = String(m.content || '').trim();
164
+ if (!text) return;
165
+ const botId = this.user && this.user.id;
166
+ const mentioned = !!(botId && text.includes('<@' + botId + '>'));
167
+ if (botId) text = text.split('<@' + botId + '>').join(' ').replace(/\s+/g, ' ').trim();
168
+ const payload = {
169
+ text: text.slice(0, 6000),
170
+ senderId: String((m.author && m.author.id) || ''),
171
+ username: String((m.author && m.author.username) || ''),
172
+ senderName: String((m.author && (m.author.global_name || m.author.username)) || ''),
173
+ isGroup: !!m.guild_id,
174
+ mentioned,
175
+ channelId: String(m.channel_id || ''),
176
+ };
177
+ if (!payload.channelId) return;
178
+ /* Sunucu mesajlarında yalnız @mention (spam koruması); DM'de hepsi */
179
+ if (payload.isGroup && !mentioned) return;
180
+ this.onIncoming(payload.channelId, payload);
181
+ } catch {}
182
+ return;
183
+ }
184
+ });
185
+
186
+ ws.on('close', () => {
187
+ clearInterval(this._hbTimer);
188
+ this._hbTimer = null;
189
+ if (this.stopping) return;
190
+ this._setStatus('connecting');
191
+ const wait = this._backoff;
192
+ this._backoff = Math.min(30000, this._backoff * 2);
193
+ setTimeout(() => this._connect(), wait);
194
+ });
195
+
196
+ ws.on('error', () => {
197
+ /* close tetiklenir — yeniden bağlanma orada */
198
+ });
199
+ }
200
+
201
+ async stop() {
202
+ this.stopping = true;
203
+ clearInterval(this._hbTimer);
204
+ this._hbTimer = null;
205
+ try {
206
+ if (this._ws) this._ws.close();
207
+ } catch {}
208
+ this._ws = null;
209
+ this.connected = false;
210
+ this.status = 'disconnected';
211
+ }
212
+
213
+ snapshot() {
214
+ return {
215
+ status: this.status,
216
+ user: this.user ? '@' + (this.user.username || 'bot') : null,
217
+ connected: this.connected,
218
+ };
219
+ }
220
+
221
+ /* Metin gönder — 2000 karakter sınırı için parçalara böl */
222
+ async send(channelId, text) {
223
+ const t = String(text || '');
224
+ if (!t.trim() || !channelId) return false;
225
+ const chunks = [];
226
+ for (let i = 0; i < t.length; i += SEND_CHUNK) chunks.push(t.slice(i, i + SEND_CHUNK));
227
+ for (const part of chunks) {
228
+ await this.api('POST', `/channels/${channelId}/messages`, { content: part });
229
+ }
230
+ return true;
231
+ }
232
+ }
233
+
234
+ module.exports = { DiscordBridge };
@@ -35,9 +35,10 @@ const SUP_IDLE_MIN = 2; // ...bu kadar süredir hiç aktivite yoksa TAKILDI
35
35
  const SUP_LONG_MIN = 6; // bu kadar süredir koşuyorsa ara kontrol nödü
36
36
  const SUP_NUDGE_COOLDOWN_MIN = 4; // aynı iş için iki uyarı arası min süre
37
37
  const BG_FIX_MAX = 2; // paralel ajanın otomatik öz-kurtarma hakkı (bitince CEO devreye girer)
38
- /* Zarif bitirme: BU sürede bitmezse ajan artık yeni araştırma yapmaz,
39
- elindekiyle SON RAPORU yazar ve düzgün biter "görev bitmeden patlama" yok */
40
- const BG_WRAP_MIN = 4;
38
+ /* SÜRE SINIRI YOK: bg ajanlar dakika bazında kesilmez. Sonsuz döngü koruması
39
+ üç katmanla sağlanır: 1) aktivite yoksa öz-kurtarma + CEO (stuck denetimi),
40
+ 2) MAX_TURNS sert tur tavanı, 3) tur limitine yaklaşınca zarif "raporu yaz"
41
+ uyarısı (_run içinde). Dakika bazlı wrap-up KALDIRILDI (BG_WRAP_MIN). */
41
42
  /* #5 hız: eşzamanlı arka plan LLM turu sınırı — rate-limit yemeden maksimum paralellik */
42
43
  const BG_MAX_CONCURRENT_DEFAULT = 4;
43
44
  /* #17 kalıcı başarısızlıkta owner'a anlık uyarı */
@@ -203,6 +204,7 @@ class Engine {
203
204
  this.lockdown = !!opts.lockdown; // varsayılan kısıt (oturum bazlı override edilmezse)
204
205
  this.sessionPerm = new Map(); // sessionId -> ['web'] | ['web','read'] | ['chat'] (kişi/bot bazlı izin)
205
206
  this.sessionTools = new Map(); // sessionId -> Set(araç adları) — bot skill kısıtı
207
+ this.sessionModel = new Map(); // sessionId -> chain entry — bot bazlı model override (setSessionModel)
206
208
  this.resolveBot = opts.resolveBot || null; // botId -> bot bilgisi (main enjekte eder)
207
209
  /* bot oturumu hafıza köprüsü: botun kendi SOUL/USER/MEMORY dosyaları */
208
210
  this.botMemory = opts.botMemory || null;
@@ -334,6 +336,16 @@ class Engine {
334
336
  else this.sessionTools.delete(id);
335
337
  }
336
338
 
339
+ /* BOT MODEL OVERRIDE: her bot farklı model kullanabilir (sel boşsa global seçim).
340
+ sel biçimi: 'providerId::model' — zincirde yoksa sessizce global'e düşer. */
341
+ setSessionModel(sessionId, sel) {
342
+ const id = String(sessionId || '');
343
+ if (!id) return;
344
+ const resolved = sel ? this._resolve(String(sel)) : null;
345
+ if (resolved) this.sessionModel.set(id, resolved);
346
+ else this.sessionModel.delete(id);
347
+ }
348
+
337
349
  /* Oturumun bağlı olduğu MÜŞTERİ botu (admin/seasız → null → global hafıza) */
338
350
  _sessionBotCtx(session) {
339
351
  if (!session || !session.botId || typeof this.resolveBot !== 'function') return null;
@@ -1286,7 +1298,8 @@ class Engine {
1286
1298
  'Kullanıcı kalıcı bir arka plan takibi isterse (fiyat eşiği, pil seviyesi, sayfa değişikliği) watcher_add ile izleyici kur; kurduktan sonra watcher_list ile doğrula ve kullanıcıya koşulu + kontrol sıklığını kısaca bildir.',
1287
1299
  'Anlık olay takipleri için (yeni mail, fiyat eşiği, dosya değişimi, webhook) event_subscribe kullan — cron/polling gerekmez; listeyi event_list ile göster, vazgeçirirse event_unsubscribe.',
1288
1300
  'Kullanıcı "artık hep böyle yap / bunu unutma" tarzı kalıcı talimat verirse kural olarak kaydet: sohbette /rule <metin> kullanmasını söyle VEYA kullanıcı isterse event_subscribe ile olaya bağlan (mail/fiyat/dosya/webhook).',
1289
- 'Kullanıcının mesajında 2+ ayrı iş/hedef varsa (örn "X yap ve sonra Y\u2019i kontrol et") KODLAMAYA/İŞE BAŞLAMADAN önce todo_write ile plan çıkar ve sırayla yürüt; her adımı tamamlarken güncelle.',
1301
+ 'Kullanıcının mesajında 2+ ayrı iş/hedef varsa (örn "X yap ve sonra Y\u2019i kontrol et") KODLAMAYA/İŞE BAŞLAMADAN önce todo_write ile plan çıkar ve sırayla yürüt; her adımı tamamlarken güncelle. LİSTE DİSİPLİNİ: her adım bittiği AN status:"done" yap; son cevabını vermeden önce tüm maddeler done olmalı — yapılmayacaksa listeden düş. Listeyi yarım bırakma.',
1302
+ 'HIZ KURALI: Bağımsız işleri AYNI turda birden çok tool_calls ile PARALEL ver. Küçük işleri tek tek çağırma — her ayrı araç turu 5-15 sn LLM gecikmesidir: 3+ küçük komutu TEK run_command\u2019te `;` ile zincirle (örn `git status; node -v; dir`), döngülü/çoklu işleri TEK python_run betiğinde topla, çok dosyalık değişikliği TEK script ile yap. Her küçük işlem için ayrı araç çağrısı açmak yavaşlığın 1 numaralı sebebidir.',
1290
1303
  this.ceoMode
1291
1304
  ? 'Bağımsız alt-işleri run_background ile PARALEL ajana devret; işi KENDİN YÜRÜTME — emri ver, takip et, raporla.'
1292
1305
  : 'Bağımsız alt-işleri delegate_task ile devret; kendi başına halledebileceğin işleri devretme.',
@@ -1910,7 +1923,7 @@ class Engine {
1910
1923
  job.lastActivityAt = nowIso();
1911
1924
  }
1912
1925
 
1913
- /* Saf sınıflandırma (test edilebilir): null | 'stuck' | 'wrapup' | 'long' */
1926
+ /* Saf sınıflandırma (test edilebilir): null | 'stuck' | 'long' */
1914
1927
  static superviseReason(job, nowMs) {
1915
1928
  if (!job || job.status !== 'running') return null;
1916
1929
  const started = Date.parse(job.startedAt || '') || nowMs;
@@ -1922,7 +1935,6 @@ class Engine {
1922
1935
  : Infinity;
1923
1936
  if (nudgedMin < SUP_NUDGE_COOLDOWN_MIN) return null; // yeni uyardık — boğmayalım
1924
1937
  if (runMin >= SUP_STUCK_START_MIN && idleMin >= SUP_IDLE_MIN) return 'stuck';
1925
- if (!job.wrapAt && runMin >= BG_WRAP_MIN) return 'wrapup';
1926
1938
  if (runMin >= SUP_LONG_MIN) return 'long';
1927
1939
  return null;
1928
1940
  }
@@ -1939,11 +1951,6 @@ class Engine {
1939
1951
  this._bgSelfHeal(job, now);
1940
1952
  continue;
1941
1953
  }
1942
- /* zarif bitirme: süre dolunca yeni araştırma yasak → SON RAPORU yazsın */
1943
- if (reason === 'wrapup') {
1944
- this._bgWrapUp(job, now);
1945
- continue;
1946
- }
1947
1954
  job.lastNudgeAt = nowIso();
1948
1955
  job.checks = (job.checks || 0) + 1;
1949
1956
  const det = this.bgDetail(job.id);
@@ -1982,24 +1989,6 @@ class Engine {
1982
1989
  this._bgKick(job, text);
1983
1990
  }
1984
1991
 
1985
- /* Zarif bitirme: süre dolunca ajanı kesip SON RAPORU yazmasını iste.
1986
- Uzatma yok, öldürme yok — mevcut bulgularla düzgün kapanış. */
1987
- _bgWrapUp(job, now) {
1988
- job.wrapAt = nowIso();
1989
- job.lastNudgeAt = nowIso();
1990
- job.checks = (job.checks || 0) + 1;
1991
- const runMin = Math.max(1, Math.round((now - Date.parse(job.startedAt)) / 60000));
1992
- const det = this.bgDetail(job.id);
1993
- const tail = det.ok && det.messages.length ? det.messages.slice(-3).join('\n') : '';
1994
- const text =
1995
- `[SÜRE UYARISI] "${job.title}" görevi ${runMin} dk'dır sürüyor — süre doldu.\n` +
1996
- `YENİ arama/fetch/sayfa açma YAPMA. Şu ana kadar bulduklarınla SON RAPORU şimdi yaz:\n` +
1997
- `- 3-5 madde: net sonuç + yapılamayanlar açıkça "bulunamadı" diye.\n` +
1998
- `Raporu yazınca görev tamamlanır — fazladan tur harcama.` +
1999
- (tail ? `\n\nSon çıktın:\n${tail}` : '');
2000
- this._bgKick(job, text);
2001
- }
2002
-
2003
1992
  /* Arka plan oturumuna müdahale: asılı turu kes (ctrl.abort), sonra
2004
1993
  kurtarma mesajını bas. abort → eski _run sonlanır → send yeni tur açar.
2005
1994
  revive bayrağı: /stop veya silme sonrası bekleyen müdahaleler patlamasın. */
@@ -2225,6 +2214,11 @@ class Engine {
2225
2214
  role = 'terminal';
2226
2215
  }
2227
2216
  let sel = this.modelFor(role);
2217
+ /* bot model override: rol modeli (vision/terminal) yoksa botun kendi seçimi kazanır */
2218
+ if (!role) {
2219
+ const ssel = this.sessionModel.get(String(session.id));
2220
+ if (ssel) sel = ssel;
2221
+ }
2228
2222
  if (role && this.roleModels[role]) {
2229
2223
  emitSafe(this, session.id, { type: 'status', status: `rol: ${role} → ${sel.providerName} · ${sel.model}` });
2230
2224
  }
@@ -2288,9 +2282,26 @@ class Engine {
2288
2282
  const sid = session.id;
2289
2283
  const emit = (ev) => this.emit({ ...ev, sessionId: sid });
2290
2284
  try {
2291
- if (!this.sel) throw new Error('Model yapılandırılmadı — %APPDATA%\\beast\\config.yaml ve .env kontrol et');
2285
+ if (!this.sel && !this.sessionModel.get(String(session.id))) throw new Error('Model yapılandırılmadı — %APPDATA%\\beast\\config.yaml ve .env kontrol et');
2292
2286
 
2287
+ let nudged = false; // görev listesi disiplini: run başına en fazla 1 hatırlatma
2288
+ let wrapNudged = false; // tur limitine yaklaşınca zarif kapanış (bg ajanlar)
2293
2289
  for (let turn = 0; turn < MAX_TURNS; turn++) {
2290
+ /* SÜRE SINIRI YOK — ama sonsuz tur da yok: bg ajan son 3 tura gelirken
2291
+ "raporu yaz ve bitir" uyarısı alır; MAX_TURNS sert tavan olarak kalır. */
2292
+ if (session.bgJob && turn === MAX_TURNS - 3 && !wrapNudged) {
2293
+ wrapNudged = true;
2294
+ const wmsg = {
2295
+ role: 'user',
2296
+ content:
2297
+ '[TUR LİMİTİ YAKLAŞIYOR] Kalın son turlar: yeni araştırma/iş AÇMA — şu ana kadarki bulgularınla SON RAPORU yaz ve bitir. Yapılamayanları açıkça "bulunamadı" diye belirt.',
2298
+ };
2299
+ session.messages.push(wmsg);
2300
+ try {
2301
+ this._append(session, wmsg);
2302
+ } catch {}
2303
+ emit({ type: 'message', message: wmsg });
2304
+ }
2294
2305
  this._bgTrim(session); // #21 bg geçmişini olabildiğince ince tut
2295
2306
  emit({ type: 'status', status: 'thinking' });
2296
2307
  const res = await this._chatTurn(session, ctrl.signal, (delta) =>
@@ -2306,6 +2317,27 @@ class Engine {
2306
2317
  emit({ type: 'message', message: assistant });
2307
2318
 
2308
2319
  if (!res.toolCalls || !res.toolCalls.length) {
2320
+ /* GÖREV LİSTESİ DİSİPLİNİ: ajan işi bitti sanıyor ama listede hâlâ
2321
+ bekleyen/aktif madde varsa BİR KEZ hatırlat ve tura devam et —
2322
+ liste ya tamamlanmalı ya kalan maddeler listeden düşürülmeli. */
2323
+ const todos = session.todos || this.todos.get(sid) || [];
2324
+ const pending = todos.filter((t) => t && t.status !== 'done');
2325
+ if (pending.length && !nudged) {
2326
+ nudged = true;
2327
+ const nudge = {
2328
+ role: 'user',
2329
+ content:
2330
+ '[OTOMATİK HATIRLATMA] Görev listende hâlâ tamamlanmamış maddeler var: ' +
2331
+ pending.map((t) => '"' + t.title + '"').join(', ') +
2332
+ '. Görevi yarım bırakma: eksik adımları ŞİMDİ tamamla; gerçekten yapılmayacaksa listeden düş. todo_write ile listeyi güncelle (status:"done") ve işi kapat.',
2333
+ };
2334
+ session.messages.push(nudge);
2335
+ try {
2336
+ this._append(session, nudge);
2337
+ } catch {}
2338
+ emit({ type: 'message', message: nudge });
2339
+ continue;
2340
+ }
2309
2341
  /* cevap tamam → done HEMEN: kullanıcı beklemeden yeni iş yazabilsin.
2310
2342
  Not/memory/skill bakımı artık arka planda (_postRunHousekeeping). */
2311
2343
  this._clearCrash();
@@ -3282,7 +3314,7 @@ const TOOLS = [
3282
3314
  function: {
3283
3315
  name: 'todo_write',
3284
3316
  description:
3285
- 'Replace the visible task checklist for this chat. Use ONLY for multi-step work (3+ steps); do not use for simple questions. Keep titles short; update statuses as you progress; clear the list when done.',
3317
+ 'Replace the visible task checklist for this chat. Use ONLY for multi-step work (3+ steps); do not use for simple questions. Keep titles short; update statuses as you progress; clear the list when done. DISCIPLINE: mark each step done THE MOMENT it is completed; NEVER end your reply while items are still pending/active — the system bounces unfinished lists back.',
3286
3318
  parameters: {
3287
3319
  type: 'object',
3288
3320
  properties: {