beast-agent 0.15.0

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 (62) hide show
  1. package/.env.example +7 -0
  2. package/LICENSE +21 -0
  3. package/README.md +94 -0
  4. package/assets/app.ico +0 -0
  5. package/assets/tray.png +0 -0
  6. package/bin/beast-agent.js +23 -0
  7. package/config.example.yaml +26 -0
  8. package/eng.traineddata +0 -0
  9. package/package.json +101 -0
  10. package/scripts/swap-electron.js +40 -0
  11. package/src/agent/bus.js +384 -0
  12. package/src/agent/computeruse.js +200 -0
  13. package/src/agent/config.js +284 -0
  14. package/src/agent/engine.js +3118 -0
  15. package/src/agent/kb.js +123 -0
  16. package/src/agent/llm.js +219 -0
  17. package/src/agent/logger.js +90 -0
  18. package/src/agent/memory.js +383 -0
  19. package/src/agent/pdf.js +20 -0
  20. package/src/agent/scripts/__pycache__/websearch.cpython-312.pyc +0 -0
  21. package/src/agent/scripts/news.py +113 -0
  22. package/src/agent/scripts/websearch.py +225 -0
  23. package/src/agent/skills.js +522 -0
  24. package/src/agent/tokens.js +39 -0
  25. package/src/agent/tools.js +911 -0
  26. package/src/agent/usage.js +125 -0
  27. package/src/agent/watchers.js +312 -0
  28. package/src/agent/watext.js +75 -0
  29. package/src/agent/whatsapp.js +494 -0
  30. package/src/cron.js +245 -0
  31. package/src/main.js +3622 -0
  32. package/src/preload.js +122 -0
  33. package/src/renderer/browserPreload.js +73 -0
  34. package/src/renderer/i18n.js +757 -0
  35. package/src/renderer/index.html +227 -0
  36. package/src/renderer/renderer.js +3250 -0
  37. package/src/renderer/style.css +1559 -0
  38. package/tests/approval.test.js +56 -0
  39. package/tests/bg-jobs.test.js +315 -0
  40. package/tests/bus.test.js +46 -0
  41. package/tests/computeruse-context.test.js +69 -0
  42. package/tests/cron.test.js +95 -0
  43. package/tests/engine.test.js +140 -0
  44. package/tests/eval.test.js +91 -0
  45. package/tests/fallout.test.js +188 -0
  46. package/tests/llm.test.js +56 -0
  47. package/tests/memory.test.js +33 -0
  48. package/tests/memoryloop.test.js +26 -0
  49. package/tests/notegen.test.js +63 -0
  50. package/tests/owner.test.js +32 -0
  51. package/tests/python.test.js +75 -0
  52. package/tests/scenarios.json +81 -0
  53. package/tests/sessioncode.test.js +50 -0
  54. package/tests/setup.js +9 -0
  55. package/tests/tokens.test.js +42 -0
  56. package/tests/tools.test.js +84 -0
  57. package/tests/usage.test.js +44 -0
  58. package/tests/v11.test.js +76 -0
  59. package/tests/watchers.test.js +206 -0
  60. package/tests/watext.test.js +53 -0
  61. package/tests/whatsapp.test.js +103 -0
  62. package/tests/wherewasi.test.js +40 -0
@@ -0,0 +1,3118 @@
1
+ 'use strict';
2
+
3
+ /* Beast engine: sessions, frugal context, streaming tool loop.
4
+ v2: token-bazlı bağlam bütçesi (kullanım kalibrasyonlu), paralel tool
5
+ çağrıları, retrieval'lı memory, web araçları, alt-agent ve todo listesi. */
6
+
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+ const { chatStream } = require('./llm');
10
+ const { chatCompletionsUrl } = require('./config');
11
+ const tools = require('./tools');
12
+ const memory = require('./memory');
13
+ const skills = require('./skills');
14
+ const { estTokens, estMsgTokens } = require('./tokens');
15
+
16
+ const MAX_TURNS = 25;
17
+ const SUB_MAX_TURNS = 8;
18
+ const HISTORY_TOKEN_BUDGET = 18000;
19
+ const TOOL_OUT_KEEP = 1200;
20
+ const USER_MAX = 8000;
21
+
22
+ /* Oturum notları (geçici hafıza): eski mesajlar bütçeden düşülmeden önce
23
+ önemli noktalar çıkarılır, sistem promptuna küçük blok olarak girer. */
24
+ const NOTES_TRIGGER = 14; // #24 son nottan bu yana bu kadar mesaj birikince not güncelle
25
+ const NOTES_KEEP_RECENT = 6; // son N mesaj her zaman birebir tutulur
26
+ const NOTES_MAX_UNITS = 12; // notlar varken birebir tutulan azami birim sayısı
27
+
28
+ /* Superyorizyon (#15): CEO paralel ajanları kendi başına bırakmaz.
29
+ Supervisor döngüsü periyodik denetler: takılan/çok uzun süren işte
30
+ ana oturuma [SUPERYORIZON] uyarısı düşer, CEO müdahale eder. */
31
+ const SUP_CHECK_MS = 60000; // denetim aralığı
32
+ const SUP_STUCK_START_MIN = 3; // en az bu kadar süredir koşuyor ve...
33
+ const SUP_IDLE_MIN = 2; // ...bu kadar süredir hiç aktivite yoksa TAKILDI
34
+ const SUP_LONG_MIN = 6; // bu kadar süredir koşuyorsa ara kontrol nödü
35
+ const SUP_NUDGE_COOLDOWN_MIN = 4; // aynı iş için iki uyarı arası min süre
36
+ const BG_FIX_MAX = 2; // paralel ajanın otomatik öz-kurtarma hakkı (bitince CEO devreye girer)
37
+ /* Zarif bitirme: iş BU sürede bitmezse ajan artık yeni araştırma yapmaz,
38
+ elindekiyle SON RAPORU yazar ve düzgün biter — "görev bitmeden patlama" yok */
39
+ const BG_WRAP_MIN = 4;
40
+ /* #5 hız: eşzamanlı arka plan LLM turu sınırı — rate-limit yemeden maksimum paralellik */
41
+ const BG_MAX_CONCURRENT_DEFAULT = 4;
42
+ /* #17 kalıcı başarısızlıkta owner'a anlık uyarı */
43
+ const OWNER_FAIL_EMAIL = 'info@algokod.com';
44
+ /* bu bayrak çalışma zamanında /notify on|off ile değiştirilir (main köprüsü) */
45
+
46
+ /* Düşünme (reasoning) seviyeleri — sağlayıcıların GERÇEK değerleri.
47
+ OpenAI (reasoning.effort) ve OpenRouter (reasoning_effort) 2026 itibarıyla:
48
+ none · minimal · low · medium · high · xhigh · max.
49
+ 0 (Kapalı) parametre göndermez; 1-5 API'ye reasoning_effort olarak gider.
50
+ Model desteklemiyorsa llm.js tek seferlik parametresiz retry yapar. */
51
+ const THINK_LEVELS = [
52
+ { v: 0, label: 'Kapalı', effort: null },
53
+ { v: 1, label: 'Low', effort: 'low' },
54
+ { v: 2, label: 'Medium', effort: 'medium' },
55
+ { v: 3, label: 'High', effort: 'high' },
56
+ { v: 4, label: 'X-High', effort: 'xhigh' },
57
+ { v: 5, label: 'Max', effort: 'max' },
58
+ ];
59
+
60
+ /* Çıktı biçimi kuralı: # ve * karakteri yasak (tüm ajanlar için ortak metin) */
61
+ const FORMAT_RULES =
62
+ 'ÇIKTI BİÇİMİ (ZORUNLU):\n' +
63
+ '- Yanıtlarında asla # karakteri kullanma (markdown başlık yok).\n' +
64
+ '- Asla * karakteri kullanma (kalın/italik yıldız yok); madde işaretleri için - kullan.\n' +
65
+ '- Renkli sembol/ikon kullanma (renkli kalp, daire vb. yok); normal sarı emoji kullanabilirsin.\n' +
66
+ '- Kod bloklarında dil gereği # veya * gerekiyorsa kod içinde serbest.';
67
+
68
+ /* Kişi bazlı granül izin seviyeleri: hangi araçlara erişilebilir */
69
+ const PERM_TOOL_SETS = {
70
+ all: null, // tüm araçlar (filtre yok)
71
+ web: new Set([
72
+ 'web_search', 'http_fetch',
73
+ 'browser_open', 'browser_read', 'browser_snapshot', 'browser_screenshot',
74
+ 'browser_click', 'browser_type', 'browser_press', 'browser_scroll', 'browser_select',
75
+ 'ocr_read',
76
+ ]),
77
+ read: new Set([
78
+ 'web_search', 'http_fetch',
79
+ 'browser_open', 'browser_read', 'browser_snapshot',
80
+ 'list_dir', 'read_file',
81
+ ]),
82
+ chat: new Set([]), // sadece sohbet
83
+ };
84
+ const PERM_LEVELS = ['all', 'web', 'read', 'chat'];
85
+
86
+ /* CEO modu: ana (konuşma) oturumunun KULLANAMAYACAĞI uygulayıcı araçlar.
87
+ Bunların hepsi run_background ile paralel ajana devredilir — CEO sadece
88
+ konuşur, planlar, emir verir ve takip eder. */
89
+ const CEO_EXEC_TOOLS = new Set([
90
+ 'run_command', 'read_file', 'write_file', 'list_dir',
91
+ 'python_run',
92
+ 'web_search', 'http_fetch',
93
+ 'browser_open', 'browser_read', 'browser_screenshot', 'browser_snapshot',
94
+ 'browser_click', 'browser_type', 'browser_press', 'browser_scroll', 'browser_select',
95
+ 'computer_look', 'computer_act',
96
+ 'ocr_read',
97
+ 'delegate_task', // senkron bekler — CEO hep asenkron run_background kullanır
98
+ ]);
99
+
100
+ /* Arka plan oturumunun görmemesi gerekenler: alt ajan daha fazla alt dal
101
+ açmasın ve kendini iptal edemesin — işi bitirip rapor yazsın. */
102
+ const BG_HIDDEN_TOOLS = new Set(['run_background', 'tasks_list', 'task_status', 'task_cancel']);
103
+
104
+ function uid() {
105
+ return Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
106
+ }
107
+ function nowIso() {
108
+ return new Date().toISOString();
109
+ }
110
+ function clamp(n, lo, hi) {
111
+ return Math.max(lo, Math.min(hi, n));
112
+ }
113
+
114
+ /* İnsan-okur 6 haneli oturum kodu — karıştırılan harfler (0,O,1,I,L) yok */
115
+ const CODE_ALPHABET = '23456789ABCDEFGHJKMNPQRSTUVWXYZ';
116
+ function sessionCode() {
117
+ let s = '';
118
+ for (let i = 0; i < 6; i++) {
119
+ s += CODE_ALPHABET[Math.floor(Math.random() * CODE_ALPHABET.length)];
120
+ }
121
+ return s;
122
+ }
123
+
124
+ /* Yansıma cevabından JSON çıkarımı: düz JSON, ```json fence'li veya
125
+ metin arasına gömülü olabilir. Bozuk girişte null döner. */
126
+ function parseReflectionJson(text) {
127
+ const t = String(text || '').replace(/```(?:json)?/gi, '');
128
+ const start = t.indexOf('{');
129
+ if (start < 0) return null;
130
+ /* son } itibarıyla dene, patlarsa kademeli kırp */
131
+ for (let end = t.lastIndexOf('}'); end > start; end = t.lastIndexOf('}', end - 1)) {
132
+ try {
133
+ const obj = JSON.parse(t.slice(start, end + 1));
134
+ if (obj && typeof obj === 'object' && typeof obj.create === 'boolean') return obj;
135
+ } catch {}
136
+ }
137
+ return null;
138
+ }
139
+
140
+ /* ---------- todo yardımcıları ---------- */
141
+
142
+ function sanitizeTodoItems(items) {
143
+ if (!Array.isArray(items)) return [];
144
+ const out = [];
145
+ const seen = new Set();
146
+ for (const it of items.slice(0, 20)) {
147
+ const rawTitle = typeof it === 'string' ? it : it && it.title;
148
+ const title = String(rawTitle ?? '').replace(/\s+/g, ' ').trim().slice(0, 120);
149
+ if (!title || seen.has(title)) continue;
150
+ seen.add(title);
151
+ const status = ['pending', 'active', 'done'].includes(it && it.status) ? it.status : 'pending';
152
+ out.push({ title, status });
153
+ }
154
+ return out;
155
+ }
156
+
157
+ function customChain(list) {
158
+ const out = [];
159
+ for (const p of Array.isArray(list) ? list : []) {
160
+ if (!p || !p.baseUrl || !p.key) continue;
161
+ for (const m of p.models || []) {
162
+ out.push({
163
+ providerId: 'custom:' + p.id,
164
+ providerName: p.name || 'Custom',
165
+ model: m,
166
+ url: chatCompletionsUrl(p.baseUrl),
167
+ key: p.key,
168
+ costIn: Number(p.priceIn ?? p.price_in) || null,
169
+ costOut: Number(p.priceOut ?? p.price_out) || null,
170
+ });
171
+ }
172
+ }
173
+ return out;
174
+ }
175
+
176
+ class Engine {
177
+ constructor(cfg, opts) {
178
+ this.cfg = cfg || {};
179
+ /* emit sarmalayıcı: paralel ajan olaylarını superyorizyon için izle */
180
+ const userEmit = opts.emit || (() => {});
181
+ this.emit = (ev) => {
182
+ try { this._bgTrack(ev); } catch {}
183
+ userEmit(ev);
184
+ };
185
+ this.sessionsDir = opts.sessionsDir;
186
+ fs.mkdirSync(this.sessionsDir, { recursive: true });
187
+ this.workspace = opts.workspace || process.env.USERPROFILE || '.';
188
+ this.cfg.baseChain = (this.cfg.chain || []).slice();
189
+ this.cfg.chain = [...this.cfg.baseChain, ...customChain(opts.customProviders)];
190
+ this.sel = this._resolve(opts.modelOverride) || this.cfg.defaultSelection || null;
191
+ this.roleModels = opts.roleModels || {}; // { vision?, terminal?, coding?, subagent? } // providerId::model string
192
+ this.lockdown = !!opts.lockdown; // varsayılan kısıt (oturum bazlı override edilmezse)
193
+ this.sessionPerm = new Map(); // sessionId -> 'web'|'read'|'chat' (kişi bazlı izin)
194
+ this.deletedModels = new Set(opts.deletedModels || []); // kullanıcı tarafından listeden silinenler
195
+ this.fallout = this._normalizeFallout(opts.fallout); // FALLOUT: yedek provider/model/key zinciri
196
+ this.limits = this._normalizeLimits(opts.limits); // provider bazlı max input limiti + bağlam sıkıştırma
197
+ this.crashFile = opts.crashFile || null; // çökme durumu buraya yazılır (kurtarma için)
198
+ this._customProviders = opts.customProviders || [];
199
+ this.cache = new Map();
200
+ this.ctrls = new Map();
201
+ this.todos = new Map(); // sessionId -> [{title,status}]
202
+ this.tokRatio = 1; // gerçek prompt_tokens ile kalibre edilir
203
+ this._codeIndex = new Map(); // kısa oturum kodu -> session id
204
+ /* yansıma: oturumda 5+ yeni araç çağrısında skill taslağı denenir */
205
+ this.reflection = { enabled: opts.reflection !== false, minTools: 5 };
206
+ this.historyTokenBudget = Number(opts.historyTokenBudget) || HISTORY_TOKEN_BUDGET;
207
+ this.browser = opts.browser || null; // dahili tarayıcı kancaları
208
+ this.fileSend = opts.fileSend || null; // #26 dosya gönderim köprüsü (chat/WA)
209
+ this.notifyOwnerFail = opts.notifyOwnerFail !== false; // #25 hata mail bildirimi (runtime /notify)
210
+ this.reminders = opts.reminders || null; // hatırlatıcı kancası (main enjekte eder)
211
+ this.watchers = opts.watchers || null; // arka plan izleyici köprüsü (main enjekte eder)
212
+ /* onay kapısı: riskli araç öncesi dış onay beklenir ({request} async) */
213
+ this.approvals = opts.approvals || null;
214
+ /* "bir daha sorma" onaylanan araçlar — bu setteki riskli araçlar sorulmaz */
215
+ this.alwaysAllowTools = new Set(Array.isArray(opts.alwaysAllowTools) ? opts.alwaysAllowTools : []);
216
+ /* olay merkezi köprüsü (#4): event_subscribe/list/unsubscribe */
217
+ this.bus = opts.bus || null;
218
+ /* computer use köprüsü (#4 v11): { look(), act(op,args) } — main enjekte eder */
219
+ this.computer = opts.computer || null;
220
+ /* dahili OCR köprüsü: { ocr({image, lang}) } — görsel desteklemeyen modeller için */
221
+ this.ocr = opts.ocr || null;
222
+ this.email = opts.email || null; // e-posta kancaları {list, read, send}
223
+ /* CEO modu: konuşan ana ajan iş YAPMAZ — paralel ajana devreder (varsayılan açık) */
224
+ this.ceoMode = opts.ceoMode !== false;
225
+ /* Düşünme (reasoning) seviyesi: 0=Kapalı .. 5=Max (/think veya üst bardan seçilir) */
226
+ this.thinkLevel = Math.min(5, Math.max(0, Math.round(Number(opts.thinkLevel) || 0)));
227
+ /* paralel arka plan ajanları kaydı: bgSessionId -> job */
228
+ this._bgJobs = new Map();
229
+ this._kickTimers = new Map(); // sid -> bekleyen öz-kurtarma gönderim zamanlayıcısı
230
+ /* #5 eşzamanlılık kuyruğu + #18 fan-out grupları */
231
+ this._bgLimit = Math.max(1, Number(opts.bgLimit || process.env.BEAST_BG_LIMIT) || BG_MAX_CONCURRENT_DEFAULT);
232
+ this._bgPendingStart = new Map(); // sid -> ilk görev metni (slot açılınca gönderilir)
233
+ this._bgGroups = new Map(); // groupId -> {parentId,total,results,dead}
234
+ /* #17 kalıcı ajan geçmişi: restart sonrası işler + sohbetler kaybolmasın */
235
+ this._bgJobsFile = path.join(this.sessionsDir, 'bg-jobs.json');
236
+ this._loadBgJobsPersisted();
237
+ /* superyorizyon döngüsü: takılan/uzun süren ajanı CEO'ya bildirir */
238
+ this._supTimer = setInterval(() => {
239
+ try { this._supervise(); } catch {}
240
+ }, SUP_CHECK_MS);
241
+ if (this._supTimer.unref) this._supTimer.unref();
242
+ skills.seedIfEmpty();
243
+ }
244
+
245
+ /* Zinciri yeniden kur: base + custom, silinenleri ayıkla */
246
+ _rebuildChain() {
247
+ const full = [...(this.cfg.baseChain || []), ...customChain(this._customProviders)];
248
+ const key = (c) => c.providerId + '::' + c.model;
249
+ this.cfg.chain = full.filter((c) => !this.deletedModels.has(key(c)));
250
+ // aktif model silindiyse varsayılana dön
251
+ if (this.sel) {
252
+ const k = this.sel.providerId + '::' + this.sel.model;
253
+ const stillThere = full.some((c) => key(c) === k) && !this.deletedModels.has(k);
254
+ if (!stillThere) {
255
+ const defKey = this.cfg.defaultSelection
256
+ ? this.cfg.defaultSelection.providerId + '::' + this.cfg.defaultSelection.model
257
+ : null;
258
+ this.sel =
259
+ defKey && !this.deletedModels.has(defKey)
260
+ ? this.cfg.defaultSelection
261
+ : this.cfg.chain[0] || null;
262
+ }
263
+ }
264
+ }
265
+
266
+ setCustomProviders(list) {
267
+ this._customProviders = list || [];
268
+ this._rebuildChain();
269
+ }
270
+
271
+ setDeletedModels(list) {
272
+ this.deletedModels = new Set(Array.isArray(list) ? list : []);
273
+ this._rebuildChain();
274
+ }
275
+
276
+ setLockdown(v) {
277
+ this.lockdown = !!v;
278
+ }
279
+
280
+ /* Kişi bazlı granül izin — WhatsApp oturumları için. 'all' kaydı siler. */
281
+ setSessionPerm(sessionId, perm) {
282
+ const id = String(sessionId || '');
283
+ if (!id) return;
284
+ const p = PERM_LEVELS.includes(perm) ? perm : null;
285
+ if (p && p !== 'all') this.sessionPerm.set(id, p);
286
+ else this.sessionPerm.delete(id);
287
+ }
288
+
289
+ sessionPermFor(sessionId) {
290
+ const p = this.sessionPerm.get(String(sessionId || ''));
291
+ if (p) return p;
292
+ return this.lockdown ? 'chat' : 'all';
293
+ }
294
+
295
+ setRoleModels(map) {
296
+ this.roleModels = map || {};
297
+ }
298
+
299
+ setCeoMode(v) {
300
+ this.ceoMode = !!v;
301
+ }
302
+
303
+ /* ---------- FALLOUT: çökme sonrası otomatik kurtarma zinciri ---------- */
304
+
305
+ _normalizeFallout(f) {
306
+ const out = { enabled: !!(f && f.enabled), chain: [] };
307
+ const slots = Array.isArray(f && f.slots) ? f.slots : [];
308
+ const valid = [];
309
+ for (const s of slots) {
310
+ if (!s || typeof s !== 'object') continue;
311
+ const providerId = String(s.providerId || '').trim();
312
+ const model = String(s.model || '').trim();
313
+ const key = String(s.key || '').trim();
314
+ if (!providerId || !model || !key) continue;
315
+ valid.push({
316
+ providerId,
317
+ providerName: String(s.providerName || providerId),
318
+ model,
319
+ key,
320
+ });
321
+ }
322
+ out.chain = valid.slice(0, 10);
323
+ return out;
324
+ }
325
+
326
+ setFallout(cfg) {
327
+ this.fallout = this._normalizeFallout(cfg);
328
+ }
329
+
330
+ /* ---------- provider bazlı girdi limiti + bağlam sıkıştırma ---------- */
331
+
332
+ _normalizeLimits(l) {
333
+ const out = { enabled: false, compress: true, default: 0, perProvider: {} };
334
+ if (!l || typeof l !== 'object') return out;
335
+ out.enabled = !!l.enabled;
336
+ out.compress = l.compress !== false;
337
+ out.default = Math.max(0, Math.round(Number(l.default) || 0));
338
+ const per = l.perProvider && typeof l.perProvider === 'object' ? l.perProvider : {};
339
+ for (const [k, v] of Object.entries(per)) {
340
+ const n = Math.max(0, Math.round(Number(v) || 0));
341
+ if (n > 0 && k) out.perProvider[String(k)] = n;
342
+ }
343
+ return out;
344
+ }
345
+
346
+ setLimits(l) {
347
+ this.limits = this._normalizeLimits(l);
348
+ }
349
+
350
+ /* Seçili provider için girdi token limiti (0 = limitsiz) */
351
+ _inputLimitFor(sel) {
352
+ if (!this.limits || !this.limits.enabled || !sel) return 0;
353
+ const per = this.limits.perProvider[sel.providerName];
354
+ return per || this.limits.default || 0;
355
+ }
356
+
357
+ /* Payload limiti aşıyorsa sıkıştır: system korunur, sondan geriye sığanlar
358
+ alınır, kopuk tool zinciri hizalanır, araya kısa sıkıştırma notu girer.
359
+ withNote=false ise sessiz kırpım yapılır. */
360
+ _compressPayload(payload, maxTokens, withNote) {
361
+ if (!Array.isArray(payload) || payload.length < 2) return payload;
362
+ const sys = payload[0];
363
+ const rest = payload.slice(1);
364
+ const sysCost = Math.ceil(estMsgTokens(sys) * this.tokRatio);
365
+ const budget = maxTokens - sysCost - 48;
366
+ if (budget <= 0) return [sys, rest[rest.length - 1]].filter(Boolean);
367
+ const picked = [];
368
+ let used = 0;
369
+ for (let i = rest.length - 1; i >= 0; i--) {
370
+ const cost = Math.ceil(estMsgTokens(rest[i]) * this.tokRatio);
371
+ if (used + cost > budget) {
372
+ if (picked.length) break; // sığmayan eski mesajlar atılır
373
+ continue; // en yeni mesaj bile sığmıyorsa onu da at (aşağıda min. 1 tutulur)
374
+ }
375
+ picked.unshift(rest[i]);
376
+ used += cost;
377
+ }
378
+ if (!picked.length) picked.push(rest[rest.length - 1]);
379
+ /* kopuk tool yanıtıyla başlama — assistant/tool_calls grubu bozulmasın */
380
+ while (picked.length && picked[0].role === 'tool') picked.shift();
381
+ if (!picked.length) picked.push(rest[rest.length - 1]);
382
+ if (picked.length >= rest.length) return payload; // zaten sığıyordu
383
+ if (withNote) {
384
+ const dropped = rest.length - picked.length;
385
+ picked.unshift({
386
+ role: 'system',
387
+ content: `[bağlam sıkıştırıldı: önceki ${dropped} mesaj, ${maxTokens} token girdi limiti nedeniyle çıkarıldı]`,
388
+ });
389
+ }
390
+ return [sys, ...picked];
391
+ }
392
+
393
+ /* Fallout kayıtlarını mevcut zincirdeki URL'lerle eşleştir.
394
+ Aynı provider+model farklı key ile birden fazla kez gelebilir. */
395
+ _falloutSelections(exclude) {
396
+ if (!this.fallout.enabled) return [];
397
+ const chain = this.cfg.chain || [];
398
+ const exKey = exclude ? exclude.providerId + '::' + exclude.model + '::' + exclude.key : null;
399
+ const out = [];
400
+ for (const e of this.fallout.chain) {
401
+ const base =
402
+ chain.find((c) => c.providerId === e.providerId && c.model === e.model) ||
403
+ chain.find((c) => c.providerId === e.providerId);
404
+ if (!base) continue; // provider artık kayıtlı değil, atla
405
+ const sel = {
406
+ providerId: base.providerId,
407
+ providerName: base.providerName,
408
+ model: e.model,
409
+ url: base.url,
410
+ key: e.key,
411
+ };
412
+ const k = sel.providerId + '::' + sel.model + '::' + sel.key;
413
+ if (k === exKey) continue; // aktif seçimle birebir aynıysa anlamsız
414
+ out.push(sel);
415
+ }
416
+ return out;
417
+ }
418
+
419
+ /* Çökme durumunu diske yaz — hata yakala → durumu kaydet adımı */
420
+ _saveCrash(sessionId, err, phase) {
421
+ if (!this.crashFile) return;
422
+ try {
423
+ fs.writeFileSync(
424
+ this.crashFile,
425
+ JSON.stringify(
426
+ {
427
+ sessionId,
428
+ at: nowIso(),
429
+ phase: phase || 'chat',
430
+ error: String((err && err.message) || err).slice(0, 500),
431
+ model: this.sel ? this.sel.providerId + '::' + this.sel.model : null,
432
+ },
433
+ null,
434
+ 2
435
+ )
436
+ );
437
+ } catch {}
438
+ }
439
+
440
+ _clearCrash() {
441
+ if (!this.crashFile) return;
442
+ try { fs.unlinkSync(this.crashFile); } catch {}
443
+ }
444
+
445
+ /* Bir tur için denenecek seçim listesi: [aktif, ...fallout zinciri] */
446
+ _chatCandidates(sel, wasVision) {
447
+ const list = [];
448
+ const seen = new Set();
449
+ const push = (s) => {
450
+ if (!s || !s.url) return;
451
+ const k = s.providerId + '::' + s.model + '::' + s.key;
452
+ if (!seen.has(k)) { seen.add(k); list.push(s); }
453
+ };
454
+ push(sel);
455
+ if (wasVision) push(this.sel || this.modelFor(null)); // resimsiz modele geri düş
456
+ for (const f of this._falloutSelections(sel)) push(f);
457
+ return list;
458
+ }
459
+
460
+ /* Adayları sırayla dene. Hata olursa durumu kaydet, zincirdeki sonrakine geç. */
461
+ async _streamWithFallbacks(session, payload, activeTools, signal, onDelta, sel, wasVision) {
462
+ const cands = this._chatCandidates(sel, wasVision);
463
+ let lastErr = null;
464
+ for (let i = 0; i < cands.length; i++) {
465
+ const c = cands[i];
466
+ try {
467
+ /* provider bazlı girdi limiti: payload aşıyorsa sıkıştırılıp öyle gönderilir */
468
+ let msgs = payload;
469
+ const lim = this._inputLimitFor(c);
470
+ if (lim > 0) {
471
+ const est = Math.ceil(payload.reduce((a, m) => a + estMsgTokens(m), 0) * this.tokRatio);
472
+ if (est > lim) {
473
+ const before = msgs.length;
474
+ msgs = this._compressPayload(payload, lim, this.limits.compress);
475
+ if (i === 0) {
476
+ emitSafe(this, session.id, {
477
+ type: 'status',
478
+ status: `bağlam sıkıştırıldı: ~${est} token → limit ${lim} (${c.providerName}, ${before}→${msgs.length} mesaj)`,
479
+ });
480
+ }
481
+ }
482
+ }
483
+ const res = await chatStream(
484
+ c,
485
+ { messages: msgs, tools: activeTools, reasoningEffort: this._thinkEffort() },
486
+ {
487
+ signal,
488
+ onDelta,
489
+ onRetry: (attempt) =>
490
+ emitSafe(this, session.id, {
491
+ type: 'status',
492
+ status: `sağlayıcı yanıtsız — tekrar deniyor (${attempt}/${3})`,
493
+ }),
494
+ }
495
+ );
496
+ if (i > 0) {
497
+ emitSafe(this, session.id, {
498
+ type: 'status',
499
+ status: `\u2622 FALLOUT devrede: ${c.providerName} · ${c.model} (#${i + 1})`,
500
+ });
501
+ }
502
+ /* maliyet sayacı için hangi adayın cevapladığı taşınır */
503
+ res.meta = {
504
+ providerId: c.providerId,
505
+ providerName: c.providerName,
506
+ model: c.model,
507
+ costIn: Number(c.costIn) || null,
508
+ costOut: Number(c.costOut) || null,
509
+ };
510
+ return res;
511
+ } catch (e) {
512
+ if (e && (e.name === 'AbortError' || (signal && signal.aborted))) throw e;
513
+ lastErr = e;
514
+ const msgStr = String((e && e.message) || '');
515
+ const imgIssue =
516
+ wasVision &&
517
+ ([400, 404, 422].includes(e.status) ||
518
+ /\bno endpoints\b/i.test(msgStr) ||
519
+ /\bimage input\b/i.test(msgStr));
520
+ const next = cands[i + 1];
521
+ if (!next) break; // zincir bitti — hatayı yukarı fırlat
522
+ if (imgIssue) {
523
+ emitSafe(this, session.id, {
524
+ type: 'status',
525
+ status: `vision rolünde uyuldu (${c.providerName} · ${c.model}) — resimsiz modeller devre dışı, sıradakiyle devam`,
526
+ });
527
+ continue;
528
+ }
529
+ if (!this.fallout.enabled) break;
530
+ // FALLOUT: hata yakalandı → durumu kaydet → sıradaki adaya geç
531
+ this._saveCrash(session.id, e, 'provider');
532
+ emitSafe(this, session.id, {
533
+ type: 'status',
534
+ status: `\u2622 FALLOUT: ${c.providerName} · ${c.model} çöktü (${e.status || 'ağ'}) → ${next.providerName} · ${next.model} geçiliyor`,
535
+ });
536
+ }
537
+ }
538
+ throw lastErr;
539
+ }
540
+
541
+ /* ---------- model selection ---------- */
542
+
543
+ _resolve(input) {
544
+ if (!input) return null;
545
+ const chain = this.cfg.chain || [];
546
+ if (typeof input === 'string') {
547
+ const [p, m] = input.split('::');
548
+ return chain.find((c) => c.providerId === p && c.model === m) || null;
549
+ }
550
+ if (input.providerId && input.model) {
551
+ return chain.find((c) => c.providerId === input.providerId && c.model === input.model) || null;
552
+ }
553
+ return null;
554
+ }
555
+
556
+ _resolveRole(input) {
557
+ if (!input) return null;
558
+ const chain = this.cfg.chain || [];
559
+ if (typeof input === 'string') {
560
+ const [p, m] = input.split('::');
561
+ return chain.find((c) => c.providerId === p && c.model === m) || null;
562
+ }
563
+ if (input.providerId && input.model) {
564
+ return chain.find((c) => c.providerId === input.providerId && c.model === input.model) || null;
565
+ }
566
+ return null;
567
+ }
568
+
569
+ _lastUserHasImage(session) {
570
+ const m = session.messages[session.messages.length - 1];
571
+ if (!m || m.role !== 'user') return false;
572
+ if (!Array.isArray(m.content)) return false;
573
+ return m.content.some((p) => p && p.type === 'image_url');
574
+ }
575
+
576
+ modelFor(role) {
577
+ const map = this.roleModels || {};
578
+ const sel = map[role];
579
+ if (!sel) return this.sel;
580
+ return this._resolveRole(sel) || this.sel;
581
+ }
582
+
583
+ setModelOverride(sel) {
584
+ this.sel = this._resolve(sel) || this.cfg.defaultSelection || null;
585
+ }
586
+
587
+ /* Model refresh: config.yaml'dan gelen taban zinciri tazeler; aktif seçim
588
+ hâlâ mevcutsa korunur, değilse varsayılana döner. */
589
+ refreshBaseChain(cfg) {
590
+ const c = cfg || {};
591
+ this.cfg.baseChain = (c.chain || []).slice();
592
+ if (c.defaultSelection) this.cfg.defaultSelection = c.defaultSelection;
593
+ this._rebuildChain();
594
+ const cur = this.sel ? this.sel.providerId + '::' + this.sel.model : null;
595
+ this.sel = (cur && this._resolve(cur)) || this.cfg.defaultSelection || this.sel || null;
596
+ }
597
+
598
+ setWorkspace(dir) {
599
+ this.workspace = dir;
600
+ }
601
+
602
+ setThinkLevel(v) {
603
+ this.thinkLevel = Math.min(5, Math.max(0, Math.round(Number(v) || 0)));
604
+ return this.thinkLevel;
605
+ }
606
+
607
+ _thinkEffort() {
608
+ const lv = THINK_LEVELS[this.thinkLevel] || THINK_LEVELS[0];
609
+ return lv.effort || null;
610
+ }
611
+
612
+ _thinkLabel() {
613
+ const lv = THINK_LEVELS[this.thinkLevel] || THINK_LEVELS[0];
614
+ return lv.label;
615
+ }
616
+
617
+ publicState() {
618
+ return {
619
+ workspace: this.workspace,
620
+ workspaceName: path.basename(this.workspace) || this.workspace,
621
+ hasModel: !!this.sel,
622
+ activeModel: this.sel
623
+ ? { sel: this.sel.providerId + '::' + this.sel.model, providerName: this.sel.providerName, model: this.sel.model }
624
+ : null,
625
+ models: (this.cfg.chain || []).map((c) => ({
626
+ sel: c.providerId + '::' + c.model,
627
+ providerName: c.providerName,
628
+ model: c.model,
629
+ })),
630
+ roleModels: this.roleModels || {},
631
+ deletedModels: [...this.deletedModels],
632
+ busy: [...this.ctrls.keys()],
633
+ thinkLevel: this.thinkLevel,
634
+ };
635
+ }
636
+
637
+ /* #23 Fallout: providerId -> çözülmüş API key (chain zaten .env/custom'dan
638
+ anahtarları çözmüş durumda — tek doğruluk kaynağı budur) */
639
+ providerKeyMap() {
640
+ const map = {};
641
+ for (const c of this.cfg.chain || []) {
642
+ if (c && c.providerId && c.key && !map[c.providerId]) map[c.providerId] = c.key;
643
+ }
644
+ return map;
645
+ }
646
+
647
+ /* ---------- sessions ---------- */
648
+
649
+ _file(id) {
650
+ if (!/^[A-Za-z0-9]+$/.test(id)) throw new Error('bad session id');
651
+ return path.join(this.sessionsDir, id + '.jsonl');
652
+ }
653
+
654
+ _append(session, msg) {
655
+ fs.appendFileSync(this._file(session.id), JSON.stringify({ t: 'msg', ...msg }) + '\n');
656
+ session.updatedAt = nowIso();
657
+ if (session.code) this._codeIndex.set(session.code, session.id);
658
+ }
659
+
660
+ _load(id) {
661
+ if (this.cache.has(id)) return this.cache.get(id);
662
+ const file = this._file(id);
663
+ const session = { id, messages: [], createdAt: nowIso(), updatedAt: nowIso() };
664
+ try {
665
+ const lines = fs.readFileSync(file, 'utf8').split('\n');
666
+ for (const line of lines) {
667
+ if (!line.trim()) continue;
668
+ try {
669
+ const rec = JSON.parse(line);
670
+ if (rec.t === 'meta') {
671
+ session.createdAt = rec.createdAt || session.createdAt;
672
+ session.code = rec.code || session.code;
673
+ } else if (rec.t === 'meta2') {
674
+ /* #17 paralel ajan oturumu: temiz başlık + işaret */
675
+ session.bgTitle = String(rec.title || '');
676
+ session.bgOf = String(rec.bgOf || '');
677
+ } else if (rec.t === 'todo') {
678
+ session.todos = sanitizeTodoItems(rec.items);
679
+ } else if (rec.t === 'notes') {
680
+ session.notes = String(rec.text || '');
681
+ session.notesAt = Number(rec.at) || 0;
682
+ } else if (rec.t === 'msg') {
683
+ delete rec.t;
684
+ session.messages.push(rec);
685
+ }
686
+ } catch {}
687
+ }
688
+ } catch {}
689
+ if (!session.code) session.code = sessionCode();
690
+ this._codeIndex.set(session.code, id);
691
+ if (session.todos && session.todos.length) this.todos.set(id, session.todos);
692
+ this.cache.set(id, session);
693
+ return session;
694
+ }
695
+
696
+ /* Kısa okunabilir oturum kodu → benzersiz çakışmasız üretim */
697
+ _newCode() {
698
+ for (;;) {
699
+ const c = sessionCode();
700
+ if (!this._codeIndex.has(c)) return c;
701
+ }
702
+ }
703
+
704
+ findByCode(code) {
705
+ const k = String(code || '').trim().toUpperCase();
706
+ if (!k) return null;
707
+ let id = this._codeIndex.get(k) || null;
708
+ if (id && this.cache.has(id)) return this._view(this.cache.get(id));
709
+ /* hızlı yol bulamadıysa dosyaları tara */
710
+ for (const v of this.listSessions()) {
711
+ if (v.code === k) return v;
712
+ }
713
+ return null;
714
+ }
715
+
716
+ _appendNotes(session) {
717
+ fs.appendFileSync(
718
+ this._file(session.id),
719
+ JSON.stringify({ t: 'notes', text: session.notes || '', at: session.notesAt || 0 }) + '\n'
720
+ );
721
+ }
722
+
723
+ /* Oturum dosyası birikince sıkıştırır: meta + son mesajlar + en güncel
724
+ not + todos kalır, eski satırlar düşer. Atomik yazım. */
725
+ _maybeCompact(id) {
726
+ try {
727
+ const file = this._file(String(id));
728
+ const st = fs.statSync(file);
729
+ if (st.size < 512 * 1024) return; // 512 KB altında dokunma
730
+ const s = this._load(String(id));
731
+ const lines = fs.readFileSync(file, 'utf8').split('\n').filter((l) => l.trim());
732
+ const keepFrom = Math.max(0, s.messages.length - NOTES_KEEP_RECENT * 3);
733
+ const lastNotesLine =
734
+ [...lines].reverse().find((l) => {
735
+ try { return JSON.parse(l).t === 'notes'; } catch { return false; }
736
+ }) || '';
737
+ const out = [];
738
+ for (const l of lines) {
739
+ let r;
740
+ try { r = JSON.parse(l); } catch { continue; }
741
+ if (r.t === 'meta') out.push(JSON.stringify({ t: 'meta', id: s.id, code: s.code, createdAt: s.createdAt }));
742
+ else if (r.t === 'todo') out.push(l);
743
+ }
744
+ if (lastNotesLine) out.push(lastNotesLine);
745
+ /* korunan pencere: son N mesaj */
746
+ const msgLines = lines.filter((l) => {
747
+ try { return JSON.parse(l).t === 'msg'; } catch { return false; }
748
+ });
749
+ out.push(...msgLines.slice(keepFrom));
750
+ out.push(JSON.stringify({ t: 'compacted', at: nowIso(), droppedBefore: keepFrom }));
751
+ const tmp = file + '.tmp';
752
+ fs.writeFileSync(tmp, out.join('\n') + '\n');
753
+ fs.renameSync(tmp, file);
754
+ } catch {}
755
+ }
756
+
757
+ /* Not tutan oturumları listele (ayarlar ekranı için) — oturum koduyla eşli */
758
+ listNotes() {
759
+ const out = [];
760
+ for (const v of this.listSessions()) {
761
+ const s = this._load(v.id);
762
+ if (s && s.notes && String(s.notes).trim()) {
763
+ out.push({ id: s.id, code: s.code || '', title: v.title, updatedAt: v.updatedAt, count: v.count, notes: String(s.notes) });
764
+ }
765
+ }
766
+ return out;
767
+ }
768
+
769
+ /* Bir oturumun notlarını sil — dosyadaki notes satırları temizlenir */
770
+ clearNotes(id) {
771
+ const sid = String(id || '');
772
+ let s;
773
+ try { s = this._load(sid); } catch { return false; }
774
+ if (!s || !s.notes) return false;
775
+ delete s.notes;
776
+ delete s.notesAt;
777
+ try {
778
+ const file = this._file(sid);
779
+ const lines = fs.readFileSync(file, 'utf8').split('\n').filter((l) => l.trim());
780
+ const kept = lines.filter((l) => {
781
+ try { return JSON.parse(l).t !== 'notes'; } catch { return true; }
782
+ });
783
+ fs.writeFileSync(file, kept.join('\n') + '\n');
784
+ } catch {}
785
+ return true;
786
+ }
787
+
788
+ _view(s) {
789
+ const firstUser = s.messages.find((m) => m.role === 'user');
790
+ const isBg = !!s.bgTitle;
791
+ const job = isBg ? this._bgJobs.get(String(s.id)) : null;
792
+ return {
793
+ id: s.id,
794
+ code: s.code || '',
795
+ title: (isBg ? s.bgTitle : (firstUser ? String(firstUser.content).slice(0, 48) : '')) || 'Yeni Sohbet',
796
+ createdAt: s.createdAt,
797
+ updatedAt: s.updatedAt,
798
+ count: s.messages.length,
799
+ isBg,
800
+ bgStatus: isBg ? (job ? job.status : null) : null,
801
+ };
802
+ }
803
+
804
+ /* #17 paralel ajan oturumları sol sohbet listesine KARIŞMAZ;
805
+ geçmişleri sağdaki Paralel Ajanlar panelinde izlenir */
806
+ listSessions() {
807
+ let files = [];
808
+ try {
809
+ files = fs.readdirSync(this.sessionsDir).filter((f) => f.endsWith('.jsonl'));
810
+ } catch {}
811
+ const out = [];
812
+ for (const f of files) {
813
+ const id = f.replace(/\.jsonl$/, '');
814
+ const v = this._view(this._load(id));
815
+ if (v.isBg) continue;
816
+ out.push(v);
817
+ }
818
+ out.sort((a, b) => String(b.updatedAt).localeCompare(String(a.updatedAt)));
819
+ return out.slice(0, 100);
820
+ }
821
+
822
+ createSession() {
823
+ const id = uid();
824
+ const code = this._newCode();
825
+ const session = { id, code, messages: [], createdAt: nowIso(), updatedAt: nowIso() };
826
+ fs.writeFileSync(
827
+ this._file(id),
828
+ JSON.stringify({ t: 'meta', id, code, createdAt: session.createdAt }) + '\n'
829
+ );
830
+ this._codeIndex.set(code, id);
831
+ this.cache.set(id, session);
832
+ return this._view(session);
833
+ }
834
+
835
+ /* #25 /clear: oturumun mesaj geçmişini temizler; kod/meta/todolar korunur.
836
+ Notlar da sıfırlanır — taze başlangıç. */
837
+ clearMessages(id) {
838
+ const sid = String(id || '');
839
+ let s;
840
+ try { s = this._load(sid); } catch { return false; }
841
+ if (!s) return false;
842
+ try {
843
+ const file = this._file(sid);
844
+ const lines = fs.readFileSync(file, 'utf8').split('\n').filter((l) => l.trim());
845
+ const kept = lines.filter((l) => {
846
+ try {
847
+ const r = JSON.parse(l);
848
+ return r.t === 'meta' || r.t === 'meta2' || r.t === 'todo';
849
+ } catch { return false; }
850
+ });
851
+ const tmp = file + '.tmp';
852
+ fs.writeFileSync(tmp, kept.join('\n') + '\n');
853
+ fs.renameSync(tmp, file);
854
+ } catch {}
855
+ s.messages = [];
856
+ delete s.notes;
857
+ delete s.notesAt;
858
+ this.todos.set(sid, []);
859
+ this.emit({ type: 'sessions' });
860
+ return true;
861
+ }
862
+
863
+ /* "Nerede kaldım?" (#5): en son aktif oturumun özeti + yarım kalan todolar.
864
+ Saf toplama — gönderim/otomatik-devam main'e aittir. */ lastWhereWasI() {
865
+ let latest = null;
866
+ for (const v of this.listSessions()) {
867
+ if (!latest || String(v.updatedAt) > String(latest.updatedAt)) latest = v;
868
+ }
869
+ if (!latest) return null;
870
+ const s = this._load(latest.id);
871
+ const todos = s.todos || this.todos.get(s.id) || [];
872
+ /* son kullanıcı mesajı + son asistan cevabı */
873
+ let lastUser = '';
874
+ let lastAssistant = '';
875
+ for (let i = s.messages.length - 1; i >= 0; i--) {
876
+ const m = s.messages[i];
877
+ if (!lastUser && m.role === 'user') {
878
+ lastUser = typeof m.content === 'string' ? m.content : '(görsel/ek)';
879
+ }
880
+ if (!lastAssistant && m.role === 'assistant' && m.content && !m.tool_calls) {
881
+ lastAssistant = typeof m.content === 'string' ? m.content : '';
882
+ }
883
+ if (lastUser && lastAssistant) break;
884
+ }
885
+ const when = s.updatedAt ? new Date(s.updatedAt).toLocaleString('tr-TR', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' }) : '?';
886
+ return {
887
+ sessionId: s.id,
888
+ code: s.code || '',
889
+ title: latest.title,
890
+ updatedAt: when,
891
+ msgCount: s.messages.length,
892
+ lastUser: String(lastUser).slice(0, 160),
893
+ lastAssistant: String(lastAssistant).replace(/\s+/g, ' ').slice(0, 200),
894
+ pendingTodos: todos.filter((t) => t.status !== 'done').slice(0, 8),
895
+ doneCount: todos.filter((t) => t.status === 'done').length,
896
+ };
897
+ }
898
+
899
+ openSession(id) {
900
+ const s = this._load(String(id));
901
+ return { ...this._view(s), messages: s.messages, todos: s.todos || this.todos.get(s.id) || [] };
902
+ }
903
+
904
+ deleteSession(id) {
905
+ try {
906
+ const c = this.ctrls.get(id);
907
+ if (c) {
908
+ c.abort();
909
+ this.ctrls.delete(id);
910
+ }
911
+ /* #17 silinecek ajanın bekleyen öz-kurtarma zamanlayıcısını iptal et —
912
+ yoksa send() oturum dosyasını yeniden yaratıp işi diriltir */
913
+ if (this._bgJobs.has(String(id))) {
914
+ const j = this._bgJobs.get(String(id));
915
+ j.revive = false;
916
+ j.status = 'aborted';
917
+ this._clearKicks(id);
918
+ }
919
+ fs.unlinkSync(this._file(String(id)));
920
+ } catch {}
921
+ try { fs.rmSync(this._file(String(id)) + '.tmp', { force: true }); } catch {}
922
+ const cached = this.cache.get(String(id));
923
+ if (cached && cached.code) this._codeIndex.delete(cached.code);
924
+ this.cache.delete(String(id));
925
+ this.todos.delete(String(id));
926
+ this.sessionPerm.delete(String(id));
927
+ this._errCount && this._errCount.delete(String(id));
928
+ /* paralel ajan oturumuysa iş kaydını da düşür — hayalet kayıt kalmasın */
929
+ if (this._bgJobs.has(String(id))) {
930
+ this._bgJobs.delete(String(id));
931
+ this._saveBgJobs();
932
+ this._bgEmit();
933
+ }
934
+ this.emit({ type: 'sessions' });
935
+ return true;
936
+ }
937
+
938
+ /* ---------- prompt assembly (the frugal part) ---------- */
939
+
940
+ buildSystem(queryText) {
941
+ const mem = memory.loadAll();
942
+ const relevantMemory = memory.relevantFor(String(queryText || ''), {});
943
+ const rules = memory.listRules();
944
+ const sk = skills.scan().map((s) => `- ${s.name}: ${s.description} [${s.path}]`);
945
+ const parts = [mem.soul, FORMAT_RULES];
946
+ if (mem.user) parts.push('# USER\n' + mem.user);
947
+ if (rules.length) parts.push('# KURALLAR (kullanıcının kalıcı talimatları — daima uy)\n' + rules.map((r) => '- ' + r).join('\n'));
948
+ if (relevantMemory) parts.push('# İLGİLİ HAFIZA\n' + relevantMemory);
949
+ /* #v13.2 tanışma modu: hafıza tamamen boşsa ajan sahibini tanımaya çalışır;
950
+ ilk bilgiler geldikçe soru sormayı bırakır (yazmaya devam eder) */
951
+ const sparse = !mem.soul.includes('SAHİBİ') && memory.entries().length < 1 && !mem.user;
952
+ if (sparse) {
953
+ parts.push(
954
+ '# TANIŞMA MODU (hafıza henüz boş)\n' +
955
+ 'Bu kullanıcıyı henüz tanımıyorsun. Sohbette doğal bir anda KISA sorarak ' +
956
+ 'adını/hitap şeklini öğren ve öğrendiğin an HEMEN memory_write ile kaydet. ' +
957
+ 'İlk kayıt düştükten sonra soru sormayı bırak; sonraki bilgiler sohbet akışında kendiliğinden not edilir.'
958
+ );
959
+ }
960
+ if (sk.length) {
961
+ parts.push(
962
+ '# SKILLS\nKullanmadan önce ilgili SKILL.md dosyasını read_file ile oku.\n' + sk.join('\n')
963
+ );
964
+ }
965
+ const nowD = new Date();
966
+ const localDate = nowD.toLocaleDateString('tr-TR');
967
+ const localTime = nowD.toLocaleTimeString('tr-TR', { hour: '2-digit', minute: '2-digit' });
968
+ let tz = '';
969
+ try {
970
+ tz = Intl.DateTimeFormat().resolvedOptions().timeZone || '';
971
+ } catch {}
972
+ parts.push(
973
+ `# ORTAM\nWindows + PowerShell. Çalışma klasörü: ${this.workspace}\n` +
974
+ `Yerel tarih: ${localDate} (${['Pazar','Pazartesi','Salı','Çarşamba','Perşembe','Cuma','Cumartesi'][nowD.getDay()]})\n` +
975
+ `Yerel saat: ${localTime}${tz ? ` (saat dilimi: ${tz})` : ''}\n` +
976
+ 'Tarih/saat gerektiren her işte bu YEREL zamanı esas al; UTC varsayma, kendin dönüşüm yapma. Bu satırlar her mesajda güncellenir.\n' +
977
+ 'Kısa ve net cevap ver. Konuşmaya özgü geçici detayları hafızaya yazma — oturum kayıtları zaten saklar. Sadece kalıcı/genel bilgileri memory_write ile kaydet.'
978
+ );
979
+ parts.push(
980
+ '# ARAÇ KURALLARI\n' +
981
+ [
982
+ 'Basit soruları araç kullanmadan doğrudan cevapla — hız önceliklidir.',
983
+ 'Kullanıcı bir tarihte/saatte hatırlatılmasını isterse set_reminder kullan; when değerini ORTAMdaki bugüne göre hesapla (yerel saat). "Her sabah/gün/hafta" gibi tekrarlı isteklerde repeat alanını da ver (daily/weekly/monthly/weekdays veya cron).',
984
+ '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.',
985
+ '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.',
986
+ '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).',
987
+ '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.',
988
+ this.ceoMode
989
+ ? 'Bağımsız alt-işleri run_background ile PARALEL ajana devret; işi KENDİN YÜRÜTME — emri ver, takip et, raporla.'
990
+ : 'Bağımsız alt-işleri delegate_task ile devret; kendi başına halledebileceğin işleri devretme.',
991
+ 'Güncel/dış bilgi gerekiyorsa web_search kullan (Python çoklu-motor paralel — hızlı, anahtarsız); bir sayfayı okumak için http_fetch kullan.',
992
+ 'ARAMA-DİSİPLİN: bir bilgiyi 2-3 denemede bulamazsan TAKILMA — farklı bir açıya/sorguya geç, yine yoksa bulabildiğin kısmi bilgiyle cevap ver ve neyi bulamadığını açıkça söyle. Kapalı/gizli içerik (private profil, login arkası veri) peşinde KOŞMA — bulunamayacağı belliyse hemen vazgeç.',
993
+ 'Beast\u2019in DAHİLİ TARAYICISI VARDIR: ekranın sağındaki panelde açılır ve kullanıcı da görebilir. Etkileşimli/görüntülü siteler için onu kullan: browser_open ile aç, önce browser_snapshot al; ref numaralarıyla browser_click/browser_type/browser_select ile hareket et, browser_read ile metin oku. Sadece görsel yerleşim/grafik gerekiyorsa browser_screenshot çek.' +
994
+ ' Görselleri GÖREMEMİYORSAN (metin-model) görüntüdeki metni okumak için ocr_read kullan: source:"browser" ile tarayıcı sayfasını, "screen" ile masaüstünü OCR\u2019la okursun (captcha/canvas/görsel metin dahil).' +
995
+ (this.ceoMode ? ' (CEO: bu araçları KENDİN ÇAĞIRMA — içinde tarayıcı geçen işi run_background ile paralel ajana devret.)' : ''),
996
+ 'Tarayıcı eylemlerinin yanıtındaki recent günlüğü ve navigated bilgisini takip et; sayfa değişmişse yeni snapshot al.',
997
+ 'CONUŞMA ODAĞI SENDE KALSIN: kullanıcı seninle konuşurken iş çıkmışsa — uzun da olsa UFACIK da (tek komutluk dizin listesi, tek dosya okuma, tek arama…) — run_background ile PARALEL ajana devret; ana sohbet hiçbir işi beklemez; bittiğinde özet otomatik düşer.',
998
+ 'Python işleri için python_run kullan: küçük betikler inline code ile; tekrarlayan işler %APPDATA%\\beast\\scripts klasöründeki dosyalarla (ör. news.py = RSS haber toplayıcı: args ["--limit","8","--json"]). Python kurulu olmasa bile ilk çağrıda taşınabilir gömülü runtime otomatik iner.',
999
+ 'PYTHON DURUMU: makinede sistem Python\'u görünmese bile ŞAŞIRMA ve "python yok" DEME — python_run aracı kendi taşınabilir runtime\'ını (%APPDATA%\\beast\\py\\python.exe) otomatik indirir/kullanır ve bu klasör run_command PATH\'inde önceliklidir; yani run_command içinde de `python` çalışır. Ham Google/Bing scrape yerine önce web_search aracını kullan (zaten Python çoklu-motor + Exa destekli), script gerektiğinde python_run yaz.',
1000
+ 'Eski bir hafıza kaydına ihtiyacın olursa memory_search ile ara; kalıcı bilgi/birikim için kb_search kullan, yeni bilgi öğrenirsen kb_add ile kaynak belirt.',
1001
+ 'Bir oturumda 3+ kez memory_write yaptıysan iş bitince memory_hygiene çağır (duplike/eskime temizliği).',
1002
+ ].join('\n')
1003
+ );
1004
+ if (this.ceoMode) {
1005
+ parts.push(
1006
+ '# CEO MODU (yönetici rolü)\n' +
1007
+ [
1008
+ 'Sen yürütücü DEĞİL yöneticisin (CEO): sohbet etmek, anlamak, planlamak, karar vermek ve EMİR VERMEK senin işin; uygulamak değil.',
1009
+ 'Konuşma harici HER somut işi KENDİN YAPMA — UFUĞU YOK: komut çalıştırma, dosya okuma/yazma, web araştırması, tarayıcıda gezinme, ekran kontrolü, analiz. "Masaüstünde hangi dosyalar var?" gibi tek adımlık minik görevler bile run_background ile PARALEL ajana devredilir; sen asla çalıştırmazsın.',
1010
+ 'Devrettikten sonra o konuda elinden geleni yapmışsın gibi DAVRANMA; sonuç gelene kadar beklemedigini bil ve kullanıcıyı yanlış bilgilendirme. Ara durumlarda task_status ile bak.',
1011
+ 'SUPERYORIZON GÖREVİN: devrettiğin her işi SAHİPLEN — ajanı kendi başına BIRAKMA. tasks_list/task_status ile düzenli kontrol et; bitti raporu geldiğinde sonucu DOĞRULA (görevle eşleşiyor mu? eksik var mı?), eksik/yanlışsa tamamlama veya düzeltme görevi için yeni run_background aç.',
1012
+ 'Sistem sana [SUPERYORIZON] uyarısı düşürürse (takılma / uzun süre) bu bir emirdir: hemen task_status ile dökümü oku, gerekirse task_cancel + düzeltilmiş yeni run_background, ve kullanıcıya tek cümleyle durumu bildir. Uyarıyı asla cevapsız bırak.',
1013
+ 'Görev tanımı kendi başına YETERLİ olsun: tüm bağlam, dosya/URL yolları, kısıtlar ve beklenen çıktı görev metnine yazılır — paralel ajan senin bağlamını göremez.',
1014
+ 'Birden fazla bağımsız iş varsa hepsini aynı anda ayrı run_background çağrılarıyla devret (paralel çalışsınlar).',
1015
+ 'HIZ FAN-OUT: aynı işin farklı adımlarını run_background_many ile TEK SEFERDE paralel aç (örnek: haber taraması → TR / Dünya / Ekonomi ayrı ajan; araştırma+veri çekme+analiz ayrı ajan). Hepsi bitince sana TEK birleşik rapor düşer — hız dramatik artar. Bağımsız 2+ adım görürsen bunu tercih et.',
1016
+ 'Takip araçları: tasks_list (genel tablo), task_status (tek ajanın canlı dökümü), task_cancel (iptal). Kullanıcı ilerleme sorarsa buradan raporla.',
1017
+ 'Bilgi sorularını, sohbeti, planlamayı, hafıza/hatırlatıcı/izleyici kurulumunu YİNE SEN yaparsın — bunlar "iş" değildir.',
1018
+ ].join('\n')
1019
+ );
1020
+ }
1021
+ return parts.filter(Boolean).join('\n\n');
1022
+ }
1023
+
1024
+ _payloadTokens(payload) {
1025
+ let t = 0;
1026
+ for (const m of payload) t += estMsgTokens(m);
1027
+ return Math.ceil(t * this.tokRatio);
1028
+ }
1029
+
1030
+ _contentLen(m) {
1031
+ if (typeof m.content === 'string') return m.content.length;
1032
+ if (Array.isArray(m.content)) {
1033
+ return m.content.reduce(
1034
+ (a, p) => a + (p.type === 'text' ? String(p.text || '').length : (String(p.image_url && p.image_url.url || '').length * 0.25)),
1035
+ 0
1036
+ );
1037
+ }
1038
+ return 0;
1039
+ }
1040
+
1041
+ _buildPayload(system, messages, notes) {
1042
+ const units = [];
1043
+ let i = 0;
1044
+ while (i < messages.length) {
1045
+ const m = messages[i];
1046
+ if (m.role === 'assistant' && m.tool_calls && m.tool_calls.length) {
1047
+ const grp = [m];
1048
+ i++;
1049
+ while (i < messages.length && messages[i].role === 'tool') grp.push(messages[i++]);
1050
+ units.push(grp);
1051
+ } else {
1052
+ units.push([m]);
1053
+ i++;
1054
+ }
1055
+ }
1056
+ const budget = this.historyTokenBudget;
1057
+ const maxUnits = notes ? NOTES_MAX_UNITS : units.length; // notlar varken pencereyi sıkılaştır
1058
+ const picked = [];
1059
+ let used = 0;
1060
+ for (let u = units.length - 1; u >= 0; u--) {
1061
+ if (picked.length >= maxUnits) break;
1062
+ const cost = units[u].reduce(
1063
+ (a, m) =>
1064
+ a +
1065
+ clamp(Math.ceil(estMsgTokens(m) * this.tokRatio), 1, this.historyTokenBudget),
1066
+ 0
1067
+ );
1068
+ if (picked.length && used + cost > budget) break;
1069
+ picked.unshift(units[u]);
1070
+ used += cost;
1071
+ }
1072
+ // Eski kullanıcı mesajlarındaki görselleri metne indir (token tasarrufu)
1073
+ const flat = picked.flat().map((m, idx, arr) => {
1074
+ if (
1075
+ m.role === 'user' &&
1076
+ Array.isArray(m.content) &&
1077
+ idx < arr.length - 2
1078
+ ) {
1079
+ const txt = m.content.filter((p) => p.type === 'text').map((p) => p.text).join('\n');
1080
+ return { ...m, content: txt + '\n[resimler bağlam dışı bırakıldı]' };
1081
+ }
1082
+ return m;
1083
+ });
1084
+ for (let k = 0; k < flat.length; k++) {
1085
+ const m = flat[k];
1086
+ if (m.role === 'tool' && String(m.content || '').length > TOOL_OUT_KEEP * 2) {
1087
+ flat[k] = { ...m, content: String(m.content).slice(0, TOOL_OUT_KEEP) + '\n…[kırpıldı]' };
1088
+ }
1089
+ }
1090
+ return [{ role: 'system', content: system }, ...flat];
1091
+ }
1092
+
1093
+ _lastUserText(session) {
1094
+ for (let i = session.messages.length - 1; i >= 0; i--) {
1095
+ const m = session.messages[i];
1096
+ if (m.role !== 'user') continue;
1097
+ if (typeof m.content === 'string') return m.content;
1098
+ return m.content.map((p) => (p.type === 'text' ? p.text : '')).join('\n');
1099
+ }
1100
+ return '';
1101
+ }
1102
+
1103
+ /* ---------- oturum notları ---------- */
1104
+
1105
+ _plainText(m) {
1106
+ if (typeof m.content === 'string') return m.content;
1107
+ if (Array.isArray(m.content)) {
1108
+ return m.content.filter((p) => p.type === 'text').map((p) => p.text || '').join(' ');
1109
+ }
1110
+ return '';
1111
+ }
1112
+
1113
+ _renderTranscript(msgs) {
1114
+ const lines = [];
1115
+ for (const m of msgs) {
1116
+ if (m.role === 'user') lines.push('[Kullanıcı] ' + this._plainText(m).slice(0, 1500));
1117
+ else if (m.role === 'assistant' && String(m.content || '').trim()) lines.push('[Asistan] ' + this._plainText(m).slice(0, 1200));
1118
+ else if (m.role === 'assistant' && m.tool_calls && m.tool_calls.length) {
1119
+ lines.push('[Araç çağrısı] ' + m.tool_calls.map((t) => t.function && t.function.name).filter(Boolean).join(', '));
1120
+ } else if (m.role === 'tool') lines.push('[Araç sonucu] ' + String(m.content || '').slice(0, 400));
1121
+ }
1122
+ return lines.filter(Boolean).join('\n');
1123
+ }
1124
+
1125
+ /* Not üretimi sonrası: notlara dönüştürülen [0..cut) aralığı diskten düşer;
1126
+ meta+notes+todos+son NOTES_KEEP_RECENT*2 mesaj kalır. notesAt=0 sıfırlanır.
1127
+ Atomik — başarısız olursa mevcut dosya sağlam kalır. */
1128
+ _compactToNotes(session, cut) {
1129
+ try {
1130
+ const file = this._file(session.id);
1131
+ /* korunan pencere cut'tan sonra başlar — özetlenen bölgeyle ÇAKIŞMAZ */
1132
+ const keepFrom = cut;
1133
+ const out = [];
1134
+ out.push(JSON.stringify({ t: 'meta', id: session.id, code: session.code, createdAt: session.createdAt }));
1135
+ for (const line of fs.readFileSync(file, 'utf8').split('\n')) {
1136
+ if (!line.trim()) continue;
1137
+ let r;
1138
+ try { r = JSON.parse(line); } catch { continue; }
1139
+ if (r.t === 'todo') out.push(line); // en güncel todo satırı kalır (append-only, sonuncusu kazanır)
1140
+ }
1141
+ out.push(JSON.stringify({ t: 'notes', text: session.notes || '', at: 0 }));
1142
+ /* korunan pencere */
1143
+ let seen = 0;
1144
+ const msgLines = [];
1145
+ for (const line of fs.readFileSync(file, 'utf8').split('\n')) {
1146
+ if (!line.trim()) continue;
1147
+ let r;
1148
+ try { r = JSON.parse(line); } catch { continue; }
1149
+ if (r.t !== 'msg') continue;
1150
+ if (seen++ >= keepFrom) msgLines.push(line);
1151
+ }
1152
+ out.push(...msgLines);
1153
+ out.push(JSON.stringify({ t: 'compacted', at: nowIso(), droppedBefore: keepFrom, noteBased: true }));
1154
+ const tmp = file + '.tmp';
1155
+ fs.writeFileSync(tmp, out.join('\n') + '\n');
1156
+ fs.renameSync(tmp, file);
1157
+ } catch {}
1158
+ }
1159
+
1160
+ /* Cevap verildikten sonra çağrılır: son not update'ten bu yana NOTES_EVERY
1161
+ (14) mesaj biriktiginde özet çıkarıp oturum notlarını günceller.
1162
+ Notlar oturum dosyasına (code ile eşli) yazılır ve _chatTurn'da aynı
1163
+ oturuma enjekte edilir. */
1164
+ async _updateSessionNotes(session, signal) {
1165
+ try {
1166
+ if (!this.sel) return { ok: false, reason: 'model yok' };
1167
+ const msgs = session.messages;
1168
+ const mark = clamp(Number(session.notesAt) || 0, 0, msgs.length);
1169
+ /* #24 KESİN tetik: son nottan bu yana >= NOTES_EVERY mesaj biriktiyse */
1170
+ const pending = msgs.length - mark;
1171
+ if (pending < NOTES_TRIGGER) return { ok: false, reason: `bekleme: ${pending}/${NOTES_TRIGGER}` };
1172
+ const cut = Math.max(mark, msgs.length - NOTES_KEEP_RECENT);
1173
+ if (cut <= mark) return { ok: false, reason: 'aralık boş' };
1174
+ const transcript = this._renderTranscript(slice);
1175
+ if (!transcript.trim()) {
1176
+ session.notesAt = cut;
1177
+ return { ok: false, reason: 'özetlenecek metin yok' };
1178
+ }
1179
+ const prev = session.notes ? `Önceki notlar (koru ve birleştir):\n${session.notes}\n\n` : '';
1180
+ const prompt =
1181
+ `OTURUM KODU: ${session.code || '?'} — üretilen not bu oturuma aittir.\n` +
1182
+ `${prev}Aşağıdaki sohbet parçasının ÖNEMLİ noktalarını çıkar. Şunları yakala:\n` +
1183
+ `- kullanıcının hedefleri ve net talepleri\n` +
1184
+ `- alınan kararlar, önemli bilgi parçaları (isim, sayı, dosya/klasör yolu, hata ve çözümü)\n` +
1185
+ `- tamamlanan işler ve sonuçları\n` +
1186
+ `Madde madde yaz, en fazla 120 kelime, başlık/selamlama yok. Alakasız ayrıntıyı at.\n\n` +
1187
+ `# SOHBET PARÇASI\n${transcript}`;
1188
+ const res = await chatOnce(
1189
+ this.sel,
1190
+ { messages: [{ role: 'user', content: prompt }], temperature: 0.2 },
1191
+ { signal }
1192
+ );
1193
+ const txt = String(res.content || '').trim();
1194
+ if (!txt) return { ok: false, reason: 'model boş döndü' };
1195
+ session.notes = txt.slice(0, 4000);
1196
+ session.notesAt = cut;
1197
+ try { this._appendNotes(session); } catch {}
1198
+ /* #6 bağlam sıkıştırma: öze dönüşen eski mesajlar bellekten VE diskten
1199
+ düşer (compacted işaretiyle); notlar + son pencere her zaman kalır.
1200
+ Böylece notesAt=0'dan yeniden başlar, uzun sohbette şişme olmaz. */
1201
+ this._compactToNotes(session, cut);
1202
+ /* bellek kopyasını da eşele: özetlenen bölge [0..cut) atılır */
1203
+ session.messages = session.messages.slice(cut);
1204
+ session.notesAt = 0;
1205
+ return { ok: true };
1206
+ } catch (e) {
1207
+ return { ok: false, reason: String((e && e.message) || e).slice(0, 120) }; // not üretimi asla sohbeti bozmasın
1208
+ }
1209
+ }
1210
+
1211
+ /* ---------- agent loop ---------- */
1212
+
1213
+ send(sessionId, payload) {
1214
+ const s = this._load(String(sessionId));
1215
+ if (!s || this.ctrls.has(s.id)) return false;
1216
+
1217
+ let msg;
1218
+ if (typeof payload === 'string') {
1219
+ msg = { role: 'user', content: payload.slice(0, USER_MAX) };
1220
+ } else {
1221
+ const text = String((payload && payload.text) || '').slice(0, USER_MAX);
1222
+ const atts = Array.isArray(payload && payload.attachments) ? payload.attachments : [];
1223
+ const imgs = atts.filter((a) => a.type === 'image' && a.dataUrl).slice(0, 4);
1224
+ const files = atts.filter((a) => a.type === 'file');
1225
+ let extra = '';
1226
+ for (const f of files) {
1227
+ extra += `\n\n[Ek dosya: ${f.name}]\n${String(f.content || '').slice(0, 20000)}`;
1228
+ }
1229
+ if (imgs.length) {
1230
+ const parts = [{ type: 'text', text: (text + extra).trim() }];
1231
+ for (const im of imgs) parts.push({ type: 'image_url', image_url: { url: im.dataUrl } });
1232
+ msg = { role: 'user', content: parts };
1233
+ } else {
1234
+ msg = { role: 'user', content: ((text + extra).trim() || '(ek)').slice(0, USER_MAX + extra.length) };
1235
+ }
1236
+ }
1237
+
1238
+ if (!String(typeof msg.content === 'string' ? msg.content : msg.content[0].text || '').trim()) return false;
1239
+ /* #12 kişilik kalibrasyonu: kullanıcı cümlelerini örnek havuzuna düşür */
1240
+ try { memory.addStyleSample(this._plainText(msg)); } catch {}
1241
+ s.messages.push(msg);
1242
+ try {
1243
+ this._append(s, msg);
1244
+ } catch {}
1245
+ this.emit({ type: 'message', sessionId: s.id, message: msg });
1246
+ this.emit({ type: 'sessions' });
1247
+ this._run(s).catch(() => {});
1248
+ return true;
1249
+ }
1250
+
1251
+ /* #14 paralel ajanlar: ANA oturumu hiç bloklamayan arka plan çalıştırıcı.
1252
+ Ayrı gizli oturum açar, işi orada koşturur; bitince özet bildirim
1253
+ istenen sohbete düşer. Birden fazla iş aynı anda paralel koşabilir —
1254
+ her biri _bgJobs kaydına düşer ve UI'da canlı izlenir. */
1255
+ runBackground(parentSessionId, task, title, opts = {}) {
1256
+ const t = String(task || '').trim();
1257
+ if (!t) return { ok: false, error: 'görev boş' };
1258
+ const parent = String(parentSessionId || '');
1259
+ /* createSession() view döndürür (messages yok) — tam oturumu cache'ten al.
1260
+ Aksi halde send() s.messages.push'da sessizce patlardı ve iş koşmazdı. */
1261
+ const bg = this._load(this.createSession().id);
1262
+ bg.messages = bg.messages || [];
1263
+ bg.bgJob = true; // arka plan oturumu: auto-memory/kalibrasyon kapalı
1264
+ this.cache.set(bg.id, bg);
1265
+ const ttl = String(title || t).replace(/\s+/g, ' ').trim().slice(0, 80) || 'arka plan görevi';
1266
+ /* #17 cache'teki nesneye de işaret koy — listSessions temiz başlık göstersin */
1267
+ bg.bgTitle = ttl;
1268
+ bg.bgOf = parent;
1269
+ try {
1270
+ // arka plan oturumunu sakla — listSessions'a karışmasın diye isimlendirme:
1271
+ fs.appendFileSync(
1272
+ this._file(bg.id),
1273
+ JSON.stringify({ t: 'meta2', bgOf: parent, title: ttl, at: new Date().toISOString() }) + '\n'
1274
+ );
1275
+ } catch {}
1276
+ this._bgJobs.set(bg.id, {
1277
+ id: bg.id,
1278
+ code: bg.code,
1279
+ title: ttl,
1280
+ task: t.slice(0, 500),
1281
+ parentId: parent,
1282
+ groupId: opts.groupId || null,
1283
+ status: 'running', // queued | running | done | error | aborted
1284
+ slot: false, // #5 gerçekten bir eşzamanlı LLM slotu işgal ediyor mu (admit'te true)
1285
+ startedAt: nowIso(),
1286
+ lastActivityAt: nowIso(), // superyorizyon: son hareket zamanı
1287
+ lastNudgeAt: null, // son [SUPERYORIZON] uyarısı
1288
+ checks: 0,
1289
+ fixes: 0, // öz-kurtarma denemeleri (BG_FIX_MAX'a kadar; bitince CEO)
1290
+ endedAt: null,
1291
+ error: null,
1292
+ });
1293
+ this._bgMeta = this._bgMeta || new Map();
1294
+ this._bgMeta.set(bg.id, { parentId: parent, title: ttl });
1295
+ this._bgEmit();
1296
+ setTimeout(() => this._bgAdmit(bg.id, t), 60);
1297
+ return { ok: true, backgroundId: bg.id, code: bg.code };
1298
+ }
1299
+
1300
+ /* ---------- #5 eşzamanlılık kuyruğu ----------
1301
+ Slot doluysa iş 'queued' görünür; bir iş bitince FIFO sıradaki başlar. */
1302
+
1303
+ _runningBgCount() {
1304
+ let n = 0;
1305
+ for (const j of this._bgJobs.values()) if (j.status === 'running' && j.slot) n++;
1306
+ return n;
1307
+ }
1308
+
1309
+ _bgAdmit(sid, fullTask) {
1310
+ try {
1311
+ const j = this._bgJobs.get(String(sid));
1312
+ if (!j || j.status !== 'running' || j.slot) return;
1313
+ if (this._runningBgCount() >= this._bgLimit) {
1314
+ j.status = 'queued';
1315
+ this._bgPendingStart.set(String(sid), fullTask || j.task);
1316
+ this._bgEmit();
1317
+ return;
1318
+ }
1319
+ j.slot = true;
1320
+ this.send(sid, {
1321
+ text:
1322
+ `[ARKA PLAN GÖREV] Aşağıdaki işi baştan sona bitir. Ara sonuçları not et; ` +
1323
+ `en sonda SADECE 3-5 satırlık net bir sonuç raporu yaz.\n\n${fullTask}`,
1324
+ });
1325
+ } catch {}
1326
+ }
1327
+
1328
+ _bgResumeQueue() {
1329
+ while (this._runningBgCount() < this._bgLimit) {
1330
+ let next = null;
1331
+ for (const j of this._bgJobs.values()) {
1332
+ if (j.status !== 'queued') continue;
1333
+ if (!next || String(j.startedAt).localeCompare(String(next.startedAt)) < 0) next = j;
1334
+ }
1335
+ if (!next) break;
1336
+ next.status = 'running';
1337
+ next.slot = true;
1338
+ const text = this._bgPendingStart.get(String(next.id)) || next.task;
1339
+ this._bgPendingStart.delete(String(next.id));
1340
+ this._bgEmit();
1341
+ try {
1342
+ this.send(next.id, {
1343
+ text:
1344
+ `[ARKA PLAN GÖREV] Aşağıdaki işi baştan sona bitir. Ara sonuçları not et; ` +
1345
+ `en sonda SADECE 3-5 satırlık net bir sonuç raporu yaz.\n\n${text}`,
1346
+ });
1347
+ } catch {}
1348
+ }
1349
+ }
1350
+
1351
+ /* #18 aynı işin farklı adımları için paralel fan-out.
1352
+ Her adım AYRI ajan koşar; hepsi bitince TEK birleşik rapor düşer —
1353
+ örnek: haber toplama → TR / Dünya / Ekonomi üç ajan paralel. */
1354
+ runBackgroundMany(parentSessionId, tasks, groupTitle) {
1355
+ const list = Array.isArray(tasks) ? tasks : [];
1356
+ if (!list.length) return { ok: false, error: 'tasks boş' };
1357
+ const parent = String(parentSessionId || '');
1358
+ const groupId = uid();
1359
+ this._bgGroups.set(groupId, { parentId: parent, total: list.length, results: [], dead: false });
1360
+ const ids = [];
1361
+ let n = 0;
1362
+ for (const item of list) {
1363
+ const task = String((item && item.task) || item || '').trim();
1364
+ if (!task) continue;
1365
+ const r = this.runBackground(parent, task, (item && item.title) || `${groupTitle || 'grup'} #${++n}`, { groupId });
1366
+ if (r.ok) ids.push(r.backgroundId);
1367
+ }
1368
+ if (!ids.length) {
1369
+ this._bgGroups.delete(groupId);
1370
+ return { ok: false, error: 'geçerli görev yok' };
1371
+ }
1372
+ const g = this._bgGroups.get(groupId);
1373
+ g.total = ids.length;
1374
+ return { ok: true, groupId, ids, total: ids.length };
1375
+ }
1376
+
1377
+ /* #17 kalıcı arka plan hatası → owner'a tek seferlik uyarı maili (fire-and-forget) */
1378
+ _notifyOwnerTaskFailed(job, msg) {
1379
+ if (this.notifyOwnerFail === false) return;
1380
+ if (!this.email || typeof this.email.send !== 'function') return;
1381
+ if (job._mailed) return;
1382
+ job._mailed = true;
1383
+ Promise.resolve(
1384
+ this.email.send({
1385
+ to: OWNER_FAIL_EMAIL,
1386
+ subject: `[Beast] Paralel ajan başarısız: ${job.title}`,
1387
+ body:
1388
+ `Görev: ${job.title}\nKod: ${job.code || '?'}\nDurum: öz-kurtarma hakları tükendi\n\n` +
1389
+ `Hata:\n${String(msg || 'bilinmeyen hata').slice(0, 800)}\n\n` +
1390
+ `Sohbet geçmişi sağ paneldeki Paralel Ajanlar bölümünde.`,
1391
+ })
1392
+ ).catch(() => {});
1393
+ }
1394
+
1395
+ /* grup üyesi kapandığında sonuç yazılır; sonuncuysa birleşik rapor basılır */
1396
+ _bgGroupRecord(title, status, summary) {
1397
+ for (const [gid, g] of this._bgGroups) {
1398
+ if (g.dead) continue;
1399
+ g.results.push({ title, status, summary: String(summary || '').slice(0, 900) });
1400
+ if (g.results.length >= g.total) {
1401
+ this._bgGroups.delete(gid);
1402
+ const okN = g.results.filter((r) => r.status === 'done').length;
1403
+ const body = g.results
1404
+ .map((r) => (r.status === 'done' ? `\u2713 ${r.title}\n${r.summary}` : `\u2718 ${r.title} (${r.status})\n${r.summary || ''}`))
1405
+ .join('\n\n')
1406
+ .slice(0, 4000);
1407
+ const text =
1408
+ `[ARKA PLAN GRUP BİTTİ: ${okN}/${g.total}]\n${body}\n\n` +
1409
+ `(Supervizyon: parçaları görevle karşılaştır — eksik varsa tamamlama görevi aç.)`;
1410
+ emitSafe(this, g.parentId, { type: 'status', status: 'paralel grup tamamlandı' });
1411
+ if (!this.isBusy(g.parentId)) {
1412
+ this.send(g.parentId, { text });
1413
+ } else {
1414
+ this._pendingReports = this._pendingReports || [];
1415
+ this._pendingReports.push({ parentId: g.parentId, text });
1416
+ }
1417
+ return true;
1418
+ }
1419
+ return true; // henüz bekleyen üye var — bireysel rapor bastırılır
1420
+ }
1421
+ return false; // grup bulunamadı → eski bireysel akış
1422
+ }
1423
+
1424
+ /* ---------- paralel ajan kaydı (UI + CEO takip araçları) ---------- */
1425
+
1426
+ /* #17 iş kayıtlarını diske yaz — uygulama kapansa da geçmiş dursun.
1427
+ Oturum dosyası silinmiş işler (elle silinmiş) geri yazılmaz. */
1428
+ _saveBgJobs() {
1429
+ try {
1430
+ const jobs = [];
1431
+ for (const j of this.listBgJobs().slice(0, 200)) {
1432
+ try { fs.accessSync(this._file(j.id)); } catch { continue; }
1433
+ const c = { ...j };
1434
+ delete c._persisted;
1435
+ jobs.push(c);
1436
+ }
1437
+ fs.writeFileSync(this._bgJobsFile, JSON.stringify({ v: 1, jobs }) + '\n');
1438
+ } catch {}
1439
+ }
1440
+
1441
+ _loadBgJobsPersisted() {
1442
+ let migrated = false;
1443
+ try {
1444
+ if (!fs.existsSync(this._bgJobsFile)) return;
1445
+ const data = JSON.parse(fs.readFileSync(this._bgJobsFile, 'utf8'));
1446
+ for (const j of Array.isArray(data.jobs) ? data.jobs : []) {
1447
+ if (!j || !j.id) continue;
1448
+ /* eski süreçte koşuyor/kuyrukta idi → kimse koşturmuyor: kesildi say */
1449
+ if (j.status === 'running' || j.status === 'queued') {
1450
+ j.status = 'aborted';
1451
+ j.endedAt = new Date().toISOString();
1452
+ j.error = String(j.error || '') || 'uygulama kapatıldı';
1453
+ migrated = true;
1454
+ }
1455
+ j._persisted = true;
1456
+ this._bgJobs.set(String(j.id), j);
1457
+ }
1458
+ } catch {}
1459
+ if (migrated) this._saveBgJobs();
1460
+ }
1461
+
1462
+ _bgEmit() {
1463
+ this._saveBgJobs();
1464
+ try { this.emit({ type: 'agents', jobs: this.listBgJobs() }); } catch {}
1465
+ }
1466
+
1467
+ listBgJobs() {
1468
+ return [...this._bgJobs.values()].sort((a, b) =>
1469
+ String(b.startedAt).localeCompare(String(a.startedAt))
1470
+ );
1471
+ }
1472
+
1473
+ /* Tek ajanın detayı: durum + mesaj dökümünün kısa kuyruğu */
1474
+ bgDetail(idOrCode) {
1475
+ const key = String(idOrCode || '').trim();
1476
+ let job = this._bgJobs.get(key);
1477
+ if (!job) job = this.listBgJobs().find((j) => j.code === key.toUpperCase()) || null;
1478
+ if (!job) return { ok: false, error: 'ajan bulunamadı: ' + key };
1479
+ const s = this._load(job.id);
1480
+ const brief = (m) => {
1481
+ const who = m.role === 'user' ? 'GÖREV' : m.role === 'assistant' ? 'AJAN' : m.role === 'tool' ? 'ARAÇ' : m.role;
1482
+ const body = typeof m.content === 'string'
1483
+ ? m.content
1484
+ : Array.isArray(m.content)
1485
+ ? m.content.map((p) => (p.type === 'text' ? p.text : '[görsel]')).join(' ')
1486
+ : '';
1487
+ return `${who}: ${String(body || '').replace(/\s+/g, ' ').trim().slice(0, 300)}`;
1488
+ };
1489
+ const tail = [];
1490
+ for (const m of s.messages) {
1491
+ if (m.tool_calls && m.tool_calls.length) {
1492
+ tail.push(`ARAÇ ÇAĞRISI: ${m.tool_calls.map((t) => (t.function && t.function.name) || '?').join(', ')}`);
1493
+ continue;
1494
+ }
1495
+ const line = brief(m);
1496
+ if (line.trim()) tail.push(line);
1497
+ }
1498
+ return { ok: true, job, messages: tail.slice(-30) };
1499
+ }
1500
+
1501
+ /* İş bitişi kaydı: status=done|error|aborted. Hata olursa üst sohbete haber gider. */
1502
+ _bgFinish(sid, status, errorMsg) {
1503
+ const job = this._bgJobs.get(String(sid));
1504
+ if (!job || job.status !== 'running') return;
1505
+ job.status = status;
1506
+ job.revive = false;
1507
+ job.endedAt = nowIso();
1508
+ if (errorMsg) job.error = String(errorMsg).slice(0, 400);
1509
+ this._bgEmit();
1510
+ this._bgResumeQueue(); // #5 slot boşaldı — sıradaki iş başlar
1511
+ /* grup üyeliği geçersizse bireysel akışa dön */
1512
+ let grouped = false;
1513
+ if (job.groupId) {
1514
+ const g = this._bgGroups.get(job.groupId);
1515
+ if (!g || g.dead) job.groupId = null;
1516
+ else grouped = true;
1517
+ }
1518
+ if (status === 'error') {
1519
+ const msg = String(errorMsg || '');
1520
+ const cfgIssue = /Model yapılandır|config/i.test(msg);
1521
+ /* #16 hatadan sonra otomatik toparlama: aynı oturumda devam hakkı varsa
1522
+ CEO'ya hata bildirmeden önce kendini düzeltmeyi dene */
1523
+ if (!cfgIssue && (job.fixes || 0) < BG_FIX_MAX) {
1524
+ job.fixes = (job.fixes || 0) + 1;
1525
+ const quick = /tur limiti/i.test(msg);
1526
+ const text = quick
1527
+ ? `[ÖZ-DÜZELTME] Tur limiti doldu — araç çağrısı YAPMA, elindeki bulgularla 3-5 satırlık final raporunu yaz ve bitir.`
1528
+ : `[HATA SONRASI ÖZ-DÜZELTME ${job.fixes}/${BG_FIX_MAX}] Görev sırasında hata oluştu:\n${msg.slice(0, 220)}\n\nKALDIĞIN YERDEN DEVAM ET: hatayı telafi et (farklı yöntem/araç/parametre); tam yol yoksa elindekiyle EN İYİ KISMİ SONUCU raporla ve bitir.`;
1529
+ this._bgKick(job, text);
1530
+ return;
1531
+ }
1532
+ /* #17 öz-kurtarma hakları tükendi → owner'a anlık altyapı uyarısı */
1533
+ this._notifyOwnerTaskFailed(job, msg);
1534
+ if (grouped) {
1535
+ /* #18 hatalı üye de gruba yazılır — birleşik raporda görünecek */
1536
+ this._bgGroupRecord(job.title, 'error', 'HATA: ' + msg.slice(0, 300));
1537
+ return;
1538
+ }
1539
+ const text =
1540
+ `[ARKA PLAN HATA: ${job.title}]\n${msg || 'bilinmeyen hata'}\n` +
1541
+ `(tasks_list / task_status ile detaya bakabilirsin.)`;
1542
+ this._pendingReports = this._pendingReports || [];
1543
+ this._pendingReports.push({ parentId: job.parentId, text });
1544
+ this.flushPendingReports(job.parentId);
1545
+ } else if (status === 'aborted' && grouped) {
1546
+ /* iptal edilen üye gruptan düşmüş sayılır */
1547
+ this._bgGroupRecord(job.title, 'aborted', '');
1548
+ }
1549
+ }
1550
+
1551
+ /* ---------- superyorizyon: CEO ajanları SAHİPLENİR (#15) ---------- */
1552
+
1553
+ /* Ajan olaylarında son aktivite zamanını güncelle (emit sarmalayıcıdan çağrılır) */
1554
+ _bgTrack(ev) {
1555
+ if (!ev || !ev.sessionId || !this._bgJobs) return;
1556
+ const job = this._bgJobs.get(String(ev.sessionId));
1557
+ if (!job || job.status !== 'running') return;
1558
+ if (!['status', 'message', 'token', 'tool-start', 'tool-end'].includes(ev.type)) return;
1559
+ if (ev.type === 'status' && ev.status === 'idle') return;
1560
+ job.lastActivityAt = nowIso();
1561
+ }
1562
+
1563
+ /* Saf sınıflandırma (test edilebilir): null | 'stuck' | 'wrapup' | 'long' */
1564
+ static superviseReason(job, nowMs) {
1565
+ if (!job || job.status !== 'running') return null;
1566
+ const started = Date.parse(job.startedAt || '') || nowMs;
1567
+ const lastAct = Date.parse(job.lastActivityAt || '') || started;
1568
+ const runMin = (nowMs - started) / 60000;
1569
+ const idleMin = (nowMs - lastAct) / 60000;
1570
+ const nudgedMin = job.lastNudgeAt
1571
+ ? (nowMs - (Date.parse(job.lastNudgeAt) || 0)) / 60000
1572
+ : Infinity;
1573
+ if (nudgedMin < SUP_NUDGE_COOLDOWN_MIN) return null; // yeni uyardık — boğmayalım
1574
+ if (runMin >= SUP_STUCK_START_MIN && idleMin >= SUP_IDLE_MIN) return 'stuck';
1575
+ if (!job.wrapAt && runMin >= BG_WRAP_MIN) return 'wrapup';
1576
+ if (runMin >= SUP_LONG_MIN) return 'long';
1577
+ return null;
1578
+ }
1579
+
1580
+ /* Periyodik denetim: uygun iş varsa önce ajanın KENDİNİ düzeltmesine izin ver,
1581
+ haklar biterse ana oturuma (CEO) müdahale emri düşür */
1582
+ _supervise() {
1583
+ const now = Date.now();
1584
+ for (const job of this._bgJobs.values()) {
1585
+ const reason = Engine.superviseReason(job, now);
1586
+ if (!reason) continue;
1587
+ /* #16 öz-kurtarma: takılan ajan CEO'yu meşgul etmeden kendini toparlar */
1588
+ if (reason === 'stuck' && (job.fixes || 0) < BG_FIX_MAX) {
1589
+ this._bgSelfHeal(job, now);
1590
+ continue;
1591
+ }
1592
+ /* zarif bitirme: süre dolunca yeni araştırma yasak → SON RAPORU yazsın */
1593
+ if (reason === 'wrapup') {
1594
+ this._bgWrapUp(job, now);
1595
+ continue;
1596
+ }
1597
+ job.lastNudgeAt = nowIso();
1598
+ job.checks = (job.checks || 0) + 1;
1599
+ const det = this.bgDetail(job.id);
1600
+ const tail = det.ok && det.messages.length ? det.messages.slice(-5).join('\n') : '(henüz çıktı yok)';
1601
+ const runMin = Math.max(0, Math.round((now - Date.parse(job.startedAt)) / 60000));
1602
+ const idleMin = Math.max(0, Math.round((now - (Date.parse(job.lastActivityAt) || now)) / 60000));
1603
+ const text =
1604
+ reason === 'stuck'
1605
+ ? `[SUPERYORIZON] "${job.title}" paralel ajanı ${runMin} dk'dır koşuyor ama son ${idleMin} dk'dır HİÇ hareket yok — büyük olasılıkla TAKILDI.\nSon çıktı:\n${tail}\n\nYAPILACAKLAR: task_status id=${job.id} ile dökümü oku → sorun netse task_cancel id=${job.id} ve düzeltilmiş görevle YENİ run_background aç → kullanıcıya TEK cümleyle durumu bildir. Bu uyarıyı ASLA görmezden gelme.`
1606
+ : `[SUPERYORIZON] "${job.title}" paralel ajanı ${runMin} dk'dır koşuyor (aktivite sürüyor).\nSon çıktı:\n${tail}\n\nARA KONTROL yap: hedefe gidiyor mu? Sapma varsa müdahale et (iptal + yeniden görevlendir); yolundaysa beklemeye devam — kullanıcıya gereksiz gürültü çıkarma.`;
1607
+ if (!this.isBusy(job.parentId)) {
1608
+ this.send(job.parentId, { text });
1609
+ } else {
1610
+ this._pendingReports = this._pendingReports || [];
1611
+ this._pendingReports.push({ parentId: job.parentId, text });
1612
+ }
1613
+ }
1614
+ }
1615
+
1616
+ /* #16 takılan paralel ajanın KENDİ kendini düzeltmesi: asılı tur kesilir,
1617
+ aynı oturuma öz-kurtarma emri düşer — CEO sadece haklar bitince devreye girer */
1618
+ _bgSelfHeal(job, now) {
1619
+ job.lastNudgeAt = nowIso();
1620
+ job.checks = (job.checks || 0) + 1;
1621
+ job.fixes = (job.fixes || 0) + 1;
1622
+ const fixNo = job.fixes;
1623
+ const det = this.bgDetail(job.id);
1624
+ const tail = det.ok && det.messages.length ? det.messages.slice(-3).join('\n') : '(henüz çıktı yok)';
1625
+ const idleMin = Math.max(1, Math.round((now - (Date.parse(job.lastActivityAt) || now)) / 60000));
1626
+ const text =
1627
+ `[ÖZ-KURTARMA ${fixNo}/${BG_FIX_MAX}] "${job.title}" görevinde son ~${idleMin} dk'dır hiç hareket yok — muhtemelen TAKILDIN.\nSon çıktı:\n${tail}\n\n` +
1628
+ `ŞİMDİ KENDİNİ DÜZELT (bunu tartışma, doğrudan uygula):\n` +
1629
+ `1) Takılan aracı/adımı bırak, FARKLI bir yöntemle devam et (farklı komut, daha kısa yol, farklı kaynak).\n` +
1630
+ `2) Gerekirse görevi küçült: en kritik sonucu üretecek kısma odaklan.\n` +
1631
+ `3) Her yolla çıkılmıyorsa engeli TEK cümlede açıkla ve şu ana kadarki EN İYİ KISMİ SONUCU rapor olarak yazıp bitir.`;
1632
+ this._bgKick(job, text);
1633
+ }
1634
+
1635
+ /* Zarif bitirme: süre dolunca ajanı kesip SON RAPORU yazmasını iste.
1636
+ Uzatma yok, öldürme yok — mevcut bulgularla düzgün kapanış. */
1637
+ _bgWrapUp(job, now) {
1638
+ job.wrapAt = nowIso();
1639
+ job.lastNudgeAt = nowIso();
1640
+ job.checks = (job.checks || 0) + 1;
1641
+ const runMin = Math.max(1, Math.round((now - Date.parse(job.startedAt)) / 60000));
1642
+ const det = this.bgDetail(job.id);
1643
+ const tail = det.ok && det.messages.length ? det.messages.slice(-3).join('\n') : '';
1644
+ const text =
1645
+ `[SÜRE UYARISI] "${job.title}" görevi ${runMin} dk'dır sürüyor — süre doldu.\n` +
1646
+ `YENİ arama/fetch/sayfa açma YAPMA. Şu ana kadar bulduklarınla SON RAPORU şimdi yaz:\n` +
1647
+ `- 3-5 madde: net sonuç + yapılamayanlar açıkça "bulunamadı" diye.\n` +
1648
+ `Raporu yazınca görev tamamlanır — fazladan tur harcama.` +
1649
+ (tail ? `\n\nSon çıktın:\n${tail}` : '');
1650
+ this._bgKick(job, text);
1651
+ }
1652
+
1653
+ /* Arka plan oturumuna müdahale: asılı turu kes (ctrl.abort), sonra
1654
+ kurtarma mesajını bas. abort → eski _run sonlanır → send yeni tur açar.
1655
+ revive bayrağı: /stop veya silme sonrası bekleyen müdahaleler patlamasın. */
1656
+ _bgKick(job, text) {
1657
+ const sid = String(job.id);
1658
+ job.revive = true;
1659
+ const ctrl = this.ctrls.get(sid);
1660
+ if (ctrl) {
1661
+ try { ctrl.abort(); } catch {}
1662
+ if (!this._kickTimers) this._kickTimers = new Map();
1663
+ clearTimeout(this._kickTimers.get(sid));
1664
+ this._kickTimers.set(sid, setTimeout(() => {
1665
+ this._kickTimers.delete(sid);
1666
+ this._bgKickSend(sid, text);
1667
+ }, 1200));
1668
+ } else {
1669
+ this._bgKickSend(sid, text);
1670
+ }
1671
+ }
1672
+
1673
+ _clearKicks(sid) {
1674
+ if (this._kickTimers && this._kickTimers.has(String(sid))) {
1675
+ clearTimeout(this._kickTimers.get(String(sid)));
1676
+ this._kickTimers.delete(String(sid));
1677
+ }
1678
+ }
1679
+
1680
+ _bgKickSend(sid, text) {
1681
+ try {
1682
+ const j = this._bgJobs.get(String(sid));
1683
+ /* silinmiş/durdurulmuş işe geri dönüş yok — hayalet dosya yaratma */
1684
+ if (!j || j.revive !== true) return;
1685
+ j.revive = false;
1686
+ if (!this._load(sid)) return;
1687
+ if (j.status !== 'running') { j.status = 'running'; j.endedAt = null; }
1688
+ this.send(sid, { text });
1689
+ this._bgEmit();
1690
+ } catch {}
1691
+ }
1692
+
1693
+ /* Eski motor örneğini kapat (reloadBackend) — supervisor zamanlayıcısı durur */
1694
+ dispose() {
1695
+ if (this._supTimer) {
1696
+ clearInterval(this._supTimer);
1697
+ this._supTimer = null;
1698
+ }
1699
+ }
1700
+
1701
+ /* Arka plan oturumu 'done' olduğunda ana sohbete özet basar (main çağırır) */
1702
+ async reportBackgroundDone(bgSessionId) {
1703
+ try {
1704
+ const meta = this._bgMeta && this._bgMeta.get(String(bgSessionId));
1705
+ if (!meta) return false;
1706
+ /* Öz-kurtarma/wrap-up kick'i bekliyorsa bu bir ABORT turudur — gerçek
1707
+ bitiş DEĞİL; erken "ARKA PLAN BİTTİ (sonuca ulaşmadan)" ilanı basma */
1708
+ const pendingKick = this._bgJobs.get(String(bgSessionId));
1709
+ if (pendingKick && pendingKick.revive === true) return false;
1710
+ /* #18 grup üyesiyse bireysel rapor yok — birleşik rapor için sonucu yaz */
1711
+ const jobRec = this._bgJobs.get(String(bgSessionId));
1712
+ const inGroup = !!(jobRec && jobRec.groupId);
1713
+ this._bgFinish(bgSessionId, 'done');
1714
+ const s = this._load(String(bgSessionId));
1715
+ let lastA = '';
1716
+ for (let i = s.messages.length - 1; i >= 0; i--) {
1717
+ const m = s.messages[i];
1718
+ if (m.role === 'assistant' && m.content && !m.tool_calls) {
1719
+ lastA = typeof m.content === 'string' ? m.content : '';
1720
+ break;
1721
+ }
1722
+ }
1723
+ const summary = lastA.slice(0, 900) || '(arka plan görevi sonuca ulaşmadan bitti)';
1724
+ if (inGroup) {
1725
+ this._bgGroupRecord(meta.title, 'done', summary);
1726
+ this._bgMeta.delete(String(bgSessionId));
1727
+ return true;
1728
+ }
1729
+ emitSafe(this, meta.parentId, { type: 'status', status: 'arka plan görevi tamamlandı' });
1730
+ const text =
1731
+ `[ARKA PLAN BİTTİ: ${meta.title}]\n${summary}\n` +
1732
+ `(Supervizyon: raporu görevle karşılaştır — EKSİK/YANLIŞ varsa hemen tamamlama görevi aç; "arka plan raporunu göster" dersen tam dökümü incelersin.)`;
1733
+ if (!this.isBusy(meta.parentId)) {
1734
+ this.send(meta.parentId, { text });
1735
+ } else {
1736
+ // ana oturum meşgulse sessizce bekle: kullanıcı mesajıyla birlikte akar
1737
+ this._pendingReports = this._pendingReports || [];
1738
+ this._pendingReports.push({ parentId: meta.parentId, text });
1739
+ }
1740
+ this._bgMeta.delete(String(bgSessionId));
1741
+ return true;
1742
+ } catch {
1743
+ return false;
1744
+ }
1745
+ }
1746
+
1747
+ /* Ana oturum işini bitirince bekleyen arka plan raporlarını boşalt */
1748
+ flushPendingReports(sessionId) {
1749
+ if (!this._pendingReports || !this._pendingReports.length) return;
1750
+ const rest = [];
1751
+ for (const r of this._pendingReports) {
1752
+ if (r.parentId === sessionId && !this.isBusy(sessionId)) {
1753
+ try { this.send(sessionId, { text: r.text }); } catch {}
1754
+ } else rest.push(r);
1755
+ }
1756
+ this._pendingReports = rest;
1757
+ }
1758
+
1759
+ /* #21 arka plan ajanı İNCE sistem promptu: dev CEO/hafıza/skill blokları yok
1760
+ → her turda çok daha az prompt tokeni = hızlı ön-bellek + düşük maliyet */
1761
+ buildBgSystem(session) {
1762
+ const job = (session && this._bgJobs.get(String(session.id))) || {};
1763
+ return (
1764
+ `Sen odaklı bir ARKA PLAN ajanısın. Tek görevini baştan sona bitir.\n` +
1765
+ (job.task ? `GÖREV: ${job.task}\n` : '') +
1766
+ `Ortam: Windows + PowerShell; çalışma klasörü: ${this.workspace}\n` +
1767
+ `HIZ KURALLARI:\n` +
1768
+ `- Döngülü işleri (çok URL/dosya/sayfa, tekrarlı parse-hesap) TEK python_run betiğinde topluca bitir.\n` +
1769
+ `- Web için web_search (çoklu-motor) kullan; etkileşimli sayfada dahili tarayıcı: browser_open → browser_snapshot → browser_click/type/read. Görseli göremiyorsan metni ocr_read ile oku (source:"browser").\n` +
1770
+ `- Bağımsız araç çağrılarını aynı turda PARALEL ver.\n` +
1771
+ `- ARAŞTIRMA SINIRI: 3-5 kaynak yeter; süre hedefi ~3 dakika. 2-3 denemede bulunamayan bilgiyi BIRAK — bulabildiğin kısmi sonucu raporla ve neyi bulamadığını yaz. Kapalı/gizli içerik peşinde koşma.\n` +
1772
+ FORMAT_RULES + '\n' +
1773
+ `SON ÇIKTI: 3-5 satırlık net sonuç raporu, madde madde. Soru sorma, sohbet etme.`
1774
+ );
1775
+ }
1776
+
1777
+ /* uzun bg işlerinde geçmiş şişer → payload büyür, hız düşer. Son N mesaj kalsın
1778
+ (+ asıl görev mesajı); kopuk tool çifti sınırı hizalanır */
1779
+ static BG_KEEP_MSGS = 24;
1780
+ _bgTrim(session) {
1781
+ const max = Engine.BG_KEEP_MSGS;
1782
+ const msgs = session.messages;
1783
+ if (!session.bgJob || msgs.length <= max) return;
1784
+ const keep = msgs.slice(-max);
1785
+ while (keep.length && keep[0].role === 'tool') keep.shift();
1786
+ const firstUser = msgs.find((m) => m.role === 'user');
1787
+ if (firstUser && !keep.includes(firstUser)) keep.unshift(firstUser);
1788
+ session.messages = keep;
1789
+ }
1790
+
1791
+ async _chatTurn(session, signal, onDelta, toolsList = TOOLS) {
1792
+ const promptText = this._lastUserText(session);
1793
+ let system = session.bgJob ? this.buildBgSystem(session) : this.buildSystem(promptText);
1794
+ // Granül izin: oturumun yetki seviyesine göre araç seti daraltılır
1795
+ const perm = this.sessionPermFor(session.id);
1796
+ const allowedSet = PERM_TOOL_SETS[perm] || null;
1797
+ let activeTools = allowedSet ? toolsList.filter((t) => allowedSet.has(t.function.name)) : toolsList;
1798
+ if (session.bgJob) {
1799
+ /* arka plan ajanı: yönetim araçlarını görmez — işini bitirsin */
1800
+ activeTools = activeTools.filter((t) => !BG_HIDDEN_TOOLS.has(t.function.name));
1801
+ } else if (this.ceoMode) {
1802
+ /* CEO: uygulayıcı araçlar kapalı — her şey paralel ajana devredilir */
1803
+ activeTools = activeTools.filter((t) => !CEO_EXEC_TOOLS.has(t.function.name));
1804
+ }
1805
+ if (perm === 'chat') {
1806
+ system +=
1807
+ '\n\n# KISITLI MOD\nTüm araçların (komut, dosya, web, tarayıcı, hafıza) kapalı. Sadece yazarak cevap ver. ' +
1808
+ 'Bilgisayarla ilgili bir işlem istenirse bu modda yapamayacağını kibarca söyle.';
1809
+ } else if (perm === 'web') {
1810
+ system +=
1811
+ '\n\n# SINIRLI YETKİ (web)\nSadece web ve tarayıcı araçlarına erişimin var; dosya okuma/yazma ve komut çalıştırma YOK. ' +
1812
+ 'Böyle bir istek gelirse yetkin olmadığını söyle.';
1813
+ } else if (perm === 'read') {
1814
+ system +=
1815
+ '\n\n# SINIRLI YETKİ (salt-okunur)\nBilgisayarı SADECE OKUYABİLİRSİN (klasör listeleme, dosya okuma) + web/tarayıcı. ' +
1816
+ 'Dosya yazma, silme ve komut çalıştırma YOK; istenirse yapamayacağını söyle.';
1817
+ }
1818
+ if (session.notes) {
1819
+ system +=
1820
+ `\n\n# OTURUM NOTLARI (oturum ${session.code || '?'} — bu oturumun önceki konuşma özeti; birebir geçmişi buradan hatırla)\n` +
1821
+ session.notes;
1822
+ }
1823
+ const payload = this._buildPayload(system, session.messages, session.notes);
1824
+ // Vision model sadece mesajda görsel varsa devreye girer; terminal ise prompt'un
1825
+ // shell/command işi olduğunda. Aksi halde ana model kullanılır.
1826
+ let role = this._lastUserHasImage(session) ? 'vision' : null;
1827
+ if (!role && /^(?:bash|sh|powershell|cmd|run_command|powershell script|terminal)\b/i.test(String(promptText || '').trim())) {
1828
+ role = 'terminal';
1829
+ }
1830
+ let sel = this.modelFor(role);
1831
+ if (role && this.roleModels[role]) {
1832
+ emitSafe(this, session.id, { type: 'status', status: `rol: ${role} → ${sel.providerName} · ${sel.model}` });
1833
+ }
1834
+ // FALLOUT destekli çağrı: hata olursa zincirdeki sıradaki sağlayıcıya geç
1835
+ const res = await this._streamWithFallbacks(
1836
+ session,
1837
+ payload,
1838
+ activeTools,
1839
+ signal,
1840
+ onDelta,
1841
+ sel,
1842
+ role === 'vision'
1843
+ );
1844
+ // Gerçek kullanım ile tahmini bütçeyi kalibre et
1845
+ const actual = res.usage && res.usage.prompt_tokens;
1846
+ if (actual > 0) {
1847
+ const predicted = this._payloadTokens(payload);
1848
+ if (predicted > 50) {
1849
+ const r = clamp(actual / predicted, 0.4, 3);
1850
+ this.tokRatio = clamp(this.tokRatio * 0.7 + r * 0.3, 0.4, 3);
1851
+ }
1852
+ }
1853
+ return res;
1854
+ }
1855
+
1856
+ async _run(session) {
1857
+ const ctrl = new AbortController();
1858
+ this.ctrls.set(session.id, ctrl);
1859
+ const sid = session.id;
1860
+ const emit = (ev) => this.emit({ ...ev, sessionId: sid });
1861
+ try {
1862
+ if (!this.sel) throw new Error('Model yapılandırılmadı — %APPDATA%\\beast\\config.yaml ve .env kontrol et');
1863
+
1864
+ for (let turn = 0; turn < MAX_TURNS; turn++) {
1865
+ this._bgTrim(session); // #21 bg geçmişini olabildiğince ince tut
1866
+ emit({ type: 'status', status: 'thinking' });
1867
+ const res = await this._chatTurn(session, ctrl.signal, (delta) =>
1868
+ emit({ type: 'token', delta })
1869
+ );
1870
+
1871
+ const assistant = { role: 'assistant', content: res.content || '' };
1872
+ if (res.toolCalls && res.toolCalls.length) assistant.tool_calls = res.toolCalls;
1873
+ session.messages.push(assistant);
1874
+ try {
1875
+ this._append(session, assistant);
1876
+ } catch {}
1877
+ emit({ type: 'message', message: assistant });
1878
+
1879
+ if (!res.toolCalls || !res.toolCalls.length) {
1880
+ // cevap tamam — pencereden düşecek kısmın oturum notlarını güncelle
1881
+ /* #21 bg ajanı: görev sonu ekstra LLM çağrıları YOK — hızlı kapansın */
1882
+ if (!session.bgJob) {
1883
+ const r = await this._updateSessionNotes(session, ctrl.signal);
1884
+ if (r && r.ok) {
1885
+ emitSafe(this, sid, { type: 'status', status: `oturum notları güncellendi (${session.code || ''})` });
1886
+ } else if (r && r.reason && !/^bekleme/.test(r.reason)) {
1887
+ emitSafe(this, sid, { type: 'status', status: `oturum notu YAZILAMADI: ${r.reason}` });
1888
+ }
1889
+ }
1890
+ /* otomatik memory döngüsü: kalıcı değerli bilgiyi MEMORY.md'ye düşür */
1891
+ if (!ctrl.signal.aborted && !session.bgJob) {
1892
+ try {
1893
+ await this._autoMemory(session, ctrl.signal);
1894
+ } catch {}
1895
+ }
1896
+ /* deneyimden skill doğurma (#2): 5+ yeni araç çağrısı biriktiyse yansıt */
1897
+ if (this.reflection.enabled && !ctrl.signal.aborted && !session.bgJob) {
1898
+ try {
1899
+ const made = await this._maybeReflectSkill(session);
1900
+ if (made) emitSafe(this, sid, { type: 'status', status: `skill taslağı doğdu: ${made}` });
1901
+ } catch {}
1902
+ }
1903
+ this._clearCrash(); // temiz bitiş — çökme kaydını sil
1904
+ this._maybeCompact(sid); // oturum dosyası şiştiyse sıkıştır
1905
+ emit({ type: 'done', usage: res.usage || null, meta: res.meta || null });
1906
+ this._bgFinish(sid, 'done');
1907
+ this.flushPendingReports(sid); // bekleyen arka plan raporları
1908
+ /* #12: yeterli örnek birikmişse üslup kalibrasyonu (sessiz) */
1909
+ try {
1910
+ const cal = memory.calibratePersona();
1911
+ if (cal.ok && cal.traits && cal.traits.length) {
1912
+ emitSafe(this, sid, { type: 'status', status: 'kişilik kalibre edildi' });
1913
+ }
1914
+ } catch {}
1915
+ return;
1916
+ }
1917
+
1918
+ // Paralel yürütme — bağımsız çağrılar beklemesin
1919
+ session.toolsSinceReflect = (session.toolsSinceReflect || 0) + res.toolCalls.length;
1920
+ await Promise.all(
1921
+ res.toolCalls.map(async (tc) => {
1922
+ const name = tc.function && tc.function.name;
1923
+ let args = {};
1924
+ try {
1925
+ args = JSON.parse((tc.function && tc.function.arguments) || '{}');
1926
+ } catch {}
1927
+ emit({ type: 'tool-start', callId: tc.id, name, args });
1928
+ emit({ type: 'status', status: name });
1929
+ let out = await this._execTool(name, args, ctrl.signal, sid);
1930
+ // Ekran görüntüsü gibi araçlar görseli sonraki tura enjekte eder
1931
+ let injectedImage = null;
1932
+ try {
1933
+ const obj = JSON.parse(out);
1934
+ if (obj && obj.__injectImage) {
1935
+ injectedImage = String(obj.__injectImage);
1936
+ delete obj.__injectImage;
1937
+ out = JSON.stringify(obj);
1938
+ }
1939
+ } catch {}
1940
+ const toolMsg = { role: 'tool', tool_call_id: tc.id, name, content: out.slice(0, TOOL_OUT_KEEP * 6) };
1941
+ session.messages.push(toolMsg);
1942
+ try {
1943
+ this._append(session, toolMsg);
1944
+ } catch {}
1945
+ emit({ type: 'tool-end', callId: tc.id, ok: !/"error"/.test(out.slice(0, 200)), result: out.slice(0, 4000) });
1946
+ if (injectedImage) {
1947
+ const imgMsg = {
1948
+ role: 'user',
1949
+ content: [
1950
+ { type: 'text', text: '[tarayıcı ekran görüntüsü — bu kareyi analiz et ve devam et]' },
1951
+ { type: 'image_url', image_url: { url: injectedImage } },
1952
+ ],
1953
+ };
1954
+ session.messages.push(imgMsg);
1955
+ try { this._append(session, imgMsg); } catch {}
1956
+ emit({ type: 'message', message: imgMsg });
1957
+ }
1958
+ })
1959
+ );
1960
+ }
1961
+ emit({ type: 'done', usage: null });
1962
+ this._bgFinish(sid, 'error', 'tur limiti doldu (MAX_TURNS)');
1963
+ } catch (e) {
1964
+ const aborted = e && (e.name === 'AbortError' || ctrl.signal.aborted);
1965
+ if (aborted) {
1966
+ this._clearCrash(); // kullanıcı durdurdu — kurtarma yok
1967
+ emit({ type: 'done', aborted: true });
1968
+ this._bgFinish(sid, 'aborted');
1969
+ } else {
1970
+ // FALLOUT: tüm zincir tükendi — durumu kaydet, açılışta kaldığı yerden devam edilir
1971
+ this._saveCrash(sid, e, 'chain-exhausted');
1972
+ emit({ type: 'error', error: String((e && e.message) || e) });
1973
+ this._bgFinish(sid, 'error', e.message);
1974
+ /* #11 self-heal: aynı oturumda 2. hatada log okuyup teşhis başlat */
1975
+ this._maybeSelfHeal(sid, e);
1976
+ }
1977
+ } finally {
1978
+ this.ctrls.delete(sid);
1979
+ emit({ type: 'status', status: 'idle' });
1980
+ }
1981
+ }
1982
+
1983
+ /* ---------- yansıma: deneyimden skill taslağı (#2) ---------- */
1984
+
1985
+ /* #11 kendini tamir: aynı oturumda 2. hatada son log satırlarını okuyup
1986
+ teşhis görevini AYNI oturuma düşürür; agent kendi hatasını analiz eder. */
1987
+ _maybeSelfHeal(sid, err) {
1988
+ try {
1989
+ const key = String(sid);
1990
+ this._errCount = this._errCount || new Map();
1991
+ const n = (this._errCount.get(key) || 0) + 1;
1992
+ this._errCount.set(key, n);
1993
+ if (n < 2) return;
1994
+
1995
+ /* tekrar döngüsüne girmesin: sayaç sıfırla, teşhis tek sefer */
1996
+ this._errCount.set(key, 0);
1997
+
1998
+ let tail = '';
1999
+ try {
2000
+ const logfile = path.join(this.sessionsDir, '..', 'wa.log');
2001
+ tail = fs.readFileSync(logfile, 'utf8').split('\n').slice(-30).join('\n');
2002
+ } catch {}
2003
+ if (!tail.trim()) {
2004
+ // app log yoksa oturum kaydının son satırlarıyla idare et
2005
+ try {
2006
+ const raw = fs.readFileSync(this._file(sid), 'utf8').split('\n').filter(Boolean);
2007
+ tail = raw.slice(-10).join('\n');
2008
+ } catch {}
2009
+ }
2010
+
2011
+ const msg =
2012
+ `[SELF-HEAL] Bu oturumda art arda 2 hata oluştu. Son hata: ` +
2013
+ `${String((err && err.message) || err).slice(0, 200)}\n` +
2014
+ `Son günlük satırları:\n${tail.slice(-2500)}\n\n` +
2015
+ `Günlüğü incele: sorunun kök nedenini TEK cümleyle teşhis et ve kullanıcıya ` +
2016
+ `sadece teşhis + önerilen çözümü bildir. Yeni iş başlatma.`;
2017
+
2018
+ setTimeout(() => {
2019
+ try {
2020
+ if (!this.isBusy(sid)) {
2021
+ emitSafe(this, sid, { type: 'status', status: 'self-heal: teşhis yapılıyor' });
2022
+ this.send(sid, { text: msg });
2023
+ }
2024
+ } catch {}
2025
+ }, 1200);
2026
+ } catch {}
2027
+ }
2028
+
2029
+ /* Oturumda son yansımadan bu yana kaç araç çağrısı biriktiğini sayar.
2030
+ Mesajları yeniden taramak yerine imza tutarız: session.toolsSinceReflect */
2031
+ /* ---------- otomatik memory döngüsü ----------
2032
+ Sohbetten kalıcı değerli bilgiyi (isim, tercih, proje, alışkanlık)
2033
+ yakalayıp MEMORY.md'ye düşürür. Şişmeyi engelleme: her turda DEĞİL,
2034
+ NOTES_TRIGGER mesajda bir; kısa tutulmuş madde; hygiene dedup'ı. */
2035
+ async _autoMemory(session, signal) {
2036
+ try {
2037
+ if (!this.sel) return;
2038
+ const msgs = session.messages;
2039
+ if (msgs.length < 4) return;
2040
+ /* son taramadan beri yeterli yeni içerik var mı */
2041
+ const since = Number(session.memScanAt ?? -6);
2042
+ if (msgs.length - since < 4) return;
2043
+ session.memScanAt = msgs.length;
2044
+
2045
+ const slice = msgs.slice(Math.max(0, msgs.length - 10));
2046
+ const transcript = this._renderTranscript(slice);
2047
+ if (transcript.trim().length < 200) return;
2048
+
2049
+ const existing = memory.entries().slice(-30).join('\n');
2050
+ const prompt =
2051
+ 'Aşağıdaki konuşma parçasından UZUN VADELİ hatırlanmaya değer bilgileri çıkar ' +
2052
+ '(kullanıcının kalıcı tercihleri, projeleri/işleri, isimler, rutinleri).\n' +
2053
+ 'KURALLAR:\n' +
2054
+ '- Sadece haftalar sonra bile işe yarayacak şeyler; geçici detay asla\n' +
2055
+ '- En fazla 3 madde, her biri max 120 karakter, "kullanıcı ..." diye başla\n' +
2056
+ '- Değerli bilgi yoksa boş dizi\n' +
2057
+ `ZATEN KAYITLI olanlar (tekrar ekleme):\n${existing || '(boş)'}\n\n` +
2058
+ '# SOHBET\n' + transcript.slice(0, 4000) +
2059
+ '\n\nSADECE JSON dön: {"memories": ["...", "..."]} veya {"memories": []}';
2060
+
2061
+ const res = await chatOnce(
2062
+ this.sel,
2063
+ { messages: [{ role: 'user', content: prompt }], temperature: 0.1 },
2064
+ { signal }
2065
+ );
2066
+ let parsed = null;
2067
+ try { parsed = JSON.parse(String(res.content || '').replace(/```(?:json)?|```/g, '').trim()); } catch {}
2068
+ const mems = parsed && Array.isArray(parsed.memories) ? parsed.memories : [];
2069
+ for (const m of mems.slice(0, 3)) {
2070
+ const t = String(m || '').trim().slice(0, 200);
2071
+ if (!t) continue;
2072
+ const r = memory.append(t);
2073
+ if (r.ok && !r.duplicate) emitSafe(this, session.id, { type: 'status', status: 'hafıza güncellendi' });
2074
+ }
2075
+ /* şişme kontrolü: kayıt sayısı cap'a yaklaşınca hijyen koş */
2076
+ if (memory.entries().length > 320) {
2077
+ try { memory.hygiene({}); } catch {}
2078
+ }
2079
+ } catch {}
2080
+ }
2081
+
2082
+ async _maybeReflectSkill(session) {
2083
+ const since = (session.toolsSinceReflect || 0);
2084
+ if (since < this.reflection.minTools) return null;
2085
+ if (!this.sel) return null;
2086
+ /* derin döngülerde tekrar tetiklenmesin: hemen sıfırla */
2087
+ session.toolsSinceReflect = 0;
2088
+
2089
+ const transcript = this._renderTranscript(
2090
+ session.messages.filter((m) => m.role !== 'tool').slice(-40)
2091
+ );
2092
+ if (transcript.length < 400) return null; // çok kısa deneyim değmez
2093
+
2094
+ const prompt =
2095
+ 'Aşağıdaki agent oturumunda TEKRARLANABİLİR, yeniden kullanılabilir bir prosedür/bilgi ' +
2096
+ 'birikti mi? Kullanıcıya özel geçici detaylar sayılmaz. Karar: evet ise SKILL.md taslağı üret.\n\n' +
2097
+ 'SADECE şu JSON formatında cevap ver, başka metin yok:\n' +
2098
+ '{"create": true|false, "name": "kisa-kebab-ad", "description": "tek cümle ne işe yarar", ' +
2099
+ '"body": "# Başlık\\n\\nmaddeler halinde adım adım prosedür"}\n\n' +
2100
+ '# OTURUM ÖZETİ\n' + transcript.slice(0, 6000);
2101
+
2102
+ const res = await chatOnce(
2103
+ this.sel,
2104
+ { messages: [{ role: 'user', content: prompt }], temperature: 0.2 },
2105
+ {}
2106
+ );
2107
+ const draft = parseReflectionJson(res.content || '');
2108
+ if (!draft || !draft.create || !draft.name || !draft.body) return null;
2109
+ const skills = require('./skills');
2110
+ const r = skills.addDraft({
2111
+ name: String(draft.name),
2112
+ description: String(draft.description || ''),
2113
+ body: String(draft.body),
2114
+ });
2115
+ return r.ok ? skills.slugify(draft.name) : null;
2116
+ }
2117
+
2118
+ /* ---------- alt-agent ---------- */
2119
+
2120
+ async _subagent(task, context, parentSignal, sessionId) {
2121
+ const ctrl = new AbortController();
2122
+ const onAbort = () => ctrl.abort();
2123
+ if (parentSignal) {
2124
+ if (parentSignal.aborted) throw new Error('iptal edildi');
2125
+ parentSignal.addEventListener('abort', onAbort, { once: true });
2126
+ }
2127
+ const subTools = TOOLS.filter(
2128
+ (t) =>
2129
+ t.function.name !== 'delegate_task' &&
2130
+ t.function.name !== 'set_reminder' &&
2131
+ !t.function.name.startsWith('email_')
2132
+ );
2133
+ const role = 'subagent';
2134
+ const sel = this.modelFor(role) || this.sel;
2135
+ if (this.roleModels[role]) {
2136
+ emitSafe(this, sessionId, { type: 'status', status: `rol: ${role} → ${sel.providerName} · ${sel.model}` });
2137
+ }
2138
+ const system =
2139
+ `Sen odaklı bir alt-agentsın. Ana ajansın verdiği TEK görevi bitir ve sadece nihai sonucu döndür.\n` +
2140
+ `Windows + PowerShell, çalışma klasörü: ${this.workspace}\n` +
2141
+ `HIZ KURALI: döngü gerektiren işleri (çok sayfa okuma, çok URL çekme, tekrarlı hesap/parse) TEK python_run betiğinde topluca yap — her adım için ayrı araç turu harcama; 10 fetch = 1 betik = çok daha hızlı görev.\n` +
2142
+ `ARAŞTIRMA SINIRI: 3-5 kaynak yeter; 2-3 denemede bulamazsan kısmi sonucu getir, takılma.\n` +
2143
+ `Kısa çalış, gereksiz soru sorma, sonucu madde madde raporla.\n` +
2144
+ FORMAT_RULES;
2145
+ const user = context ? `${task}\n\n# BAĞLAM\n${context}` : task;
2146
+ const msgs = [
2147
+ { role: 'system', content: system },
2148
+ { role: 'user', content: user },
2149
+ ];
2150
+ try {
2151
+ for (let turn = 0; turn < SUB_MAX_TURNS; turn++) {
2152
+ const res = await chatStream(
2153
+ sel,
2154
+ { messages: [{ role: 'system', content: system }, ...msgs.slice(1)], tools: subTools, reasoningEffort: this._thinkEffort() },
2155
+ { signal: ctrl.signal }
2156
+ );
2157
+ const assistant = { role: 'assistant', content: res.content || '' };
2158
+ if (res.toolCalls && res.toolCalls.length) assistant.tool_calls = res.toolCalls;
2159
+ msgs.push(assistant);
2160
+ if (!res.toolCalls || !res.toolCalls.length) return res.content || '(alt-agent boş döndü)';
2161
+ for (const tc of res.toolCalls) {
2162
+ let args = {};
2163
+ try {
2164
+ args = JSON.parse((tc.function && tc.function.arguments) || '{}');
2165
+ } catch {}
2166
+ const out = await this._subExecTool(tc.function.name, args, ctrl.signal);
2167
+ msgs.push({ role: 'tool', tool_call_id: tc.id, name: tc.function.name, content: out });
2168
+ }
2169
+ }
2170
+ const last = [...msgs].reverse().find((m) => m.role === 'assistant' && m.content);
2171
+ return (last && last.content) || '(tur limiti doldu)';
2172
+ } finally {
2173
+ if (parentSignal) parentSignal.removeEventListener('abort', onAbort);
2174
+ }
2175
+ }
2176
+
2177
+ /* Dahili OCR aracı: görüntüden metin çıkarır (görsel desteklemeyen modeller için).
2178
+ Kaynak: 'browser' (dahili tarayıcı görüntüsü, varsayılan) | 'screen' (masaüstü) | dosya yolu. */
2179
+ async _ocrRead(args, signal) {
2180
+ if (!this.ocr) return { ok: false, error: 'OCR kullanılamıyor' };
2181
+ const src = String((args && args.source) || 'browser');
2182
+ let image = null;
2183
+ let via = 'file';
2184
+ try {
2185
+ if (src === 'browser') {
2186
+ via = 'browser';
2187
+ if (!this.browser || typeof this.browser.screenshot !== 'function') {
2188
+ return { ok: false, error: 'dahili tarayıcı kullanılamıyor' };
2189
+ }
2190
+ const shot = await this.browser.screenshot(signal);
2191
+ if (!shot || !shot.ok) return { ok: false, error: (shot && shot.error) || 'görüntü alınamadı' };
2192
+ image = shot.__injectImage || shot.image || null;
2193
+ } else if (src === 'screen') {
2194
+ via = 'screen';
2195
+ if (!this.computer || typeof this.computer.look !== 'function') {
2196
+ return { ok: false, error: 'ekran erişimi yok' };
2197
+ }
2198
+ const shot = await this.computer.look();
2199
+ image = typeof shot === 'string' ? shot : (shot && (shot.image || shot.dataUrl)) || null;
2200
+ } else {
2201
+ const abs = path.isAbsolute(src) ? src : path.join(this.workspace, src);
2202
+ if (!fs.existsSync(abs)) return { ok: false, error: 'görsel bulunamadı: ' + src };
2203
+ image = abs;
2204
+ }
2205
+ } catch (e) {
2206
+ return { ok: false, error: String((e && e.message) || e) };
2207
+ }
2208
+ const r = await this.ocr({ image, lang: String((args && args.lang) || 'tur+eng') });
2209
+ return { ...r, via };
2210
+ }
2211
+
2212
+ async _subExecTool(name, args, signal) {
2213
+ try {
2214
+ if (name === 'memory_write') return JSON.stringify(memory.append(args.text));
2215
+ if (name === 'memory_search') {
2216
+ return JSON.stringify({
2217
+ ok: true,
2218
+ query: String(args.query || ''),
2219
+ results: memory.search(String(args.query || ''), clamp(Number(args.limit) || 5, 1, 15)),
2220
+ });
2221
+ }
2222
+ if (name === 'ocr_read') return JSON.stringify(await this._ocrRead(args || {}, signal));
2223
+ if (!(name === 'run_command' || name === 'read_file' || name === 'write_file' || name === 'list_dir' ||
2224
+ name === 'web_search' || name === 'http_fetch' || name === 'python_run')) {
2225
+ return JSON.stringify({ ok: false, error: `unknown tool ${name}` });
2226
+ }
2227
+ return await tools.exec(name, args, { cwd: this.workspace, signal });
2228
+ } catch (e) {
2229
+ return JSON.stringify({ ok: false, error: String((e && e.message) || e) });
2230
+ }
2231
+ }
2232
+
2233
+ /* ---------- tool dispatch ---------- */
2234
+
2235
+ /* Onay gerektiren araçlar — tepkiyle onay kapısına takılır */
2236
+ static RISKY_TOOLS = new Set(['run_command', 'write_file', 'python_run', 'email_send', 'watcher_add']);
2237
+
2238
+ async _execTool(name, args, signal, sessionId) {
2239
+ try {
2240
+ /* onay kapısı: riskli araçta dış onay bekle; reddedilirse araç çalışmaz.
2241
+ "always" onaylı araçlar doğrudan geçer. */
2242
+ if (
2243
+ Engine.RISKY_TOOLS.has(name) &&
2244
+ !this.alwaysAllowTools.has(name) &&
2245
+ this.approvals && typeof this.approvals.request === 'function'
2246
+ ) {
2247
+ emitSafe(this, sessionId, { type: 'status', status: `onay bekleniyor: ${name}` });
2248
+ let ok = false;
2249
+ try {
2250
+ ok = !!(await this.approvals.request({ sessionId, tool: name, args: args || {} }));
2251
+ } catch {}
2252
+ if (!ok) {
2253
+ return JSON.stringify({
2254
+ ok: false,
2255
+ error:
2256
+ 'kullanıcı bu işlemi ONAYLAMADI (iptal edildi veya zaman aşımı). İşlemi yeniden deneme; kullanıcıya sormadan alternatif öner.',
2257
+ });
2258
+ }
2259
+ }
2260
+ if (name === 'memory_write') {
2261
+ return JSON.stringify(memory.append(args.text));
2262
+ }
2263
+ if (name === 'memory_search') {
2264
+ return JSON.stringify({
2265
+ ok: true,
2266
+ query: String(args.query || ''),
2267
+ results: memory.search(String(args.query || ''), clamp(Number(args.limit) || 5, 1, 15)),
2268
+ });
2269
+ }
2270
+ if (name === 'todo_write') {
2271
+ const items = sanitizeTodoItems(args.items);
2272
+ this.todos.set(sessionId, items);
2273
+ try {
2274
+ fs.appendFileSync(
2275
+ this._file(sessionId),
2276
+ JSON.stringify({ t: 'todo', items }) + '\n'
2277
+ );
2278
+ } catch {}
2279
+ this.emit({ type: 'todos', sessionId, todos: items });
2280
+ const done = items.filter((t) => t.status === 'done').length;
2281
+ return JSON.stringify({ ok: true, total: items.length, done, note: 'kullanıcıya gösterildi; her adımda güncelle' });
2282
+ }
2283
+ if (name === 'delegate_task') {
2284
+ const task = String(args.task || '').trim();
2285
+ if (!task) return JSON.stringify({ ok: false, error: 'task gerekli' });
2286
+ emitSafe(this, sessionId, { type: 'status', status: 'delegate_task' });
2287
+ const result = await this._subagent(task, String(args.context || ''), signal, sessionId);
2288
+ return JSON.stringify({ ok: true, task, result: String(result).slice(0, 12000) });
2289
+ }
2290
+ if (name === 'send_file') {
2291
+ if (typeof this.fileSend !== 'function') {
2292
+ return JSON.stringify({ ok: false, error: 'dosya gönderim köprüsü yok' });
2293
+ }
2294
+ const p = String((args && args.path) || '').trim();
2295
+ if (!p) return JSON.stringify({ ok: false, error: 'path gerekli' });
2296
+ const r = await this.fileSend(sessionId, p, String((args && args.caption) || ''));
2297
+ return JSON.stringify(r);
2298
+ }
2299
+ if (name === 'run_background') {
2300
+ const r = this.runBackground(sessionId, args.task, args.title);
2301
+ return JSON.stringify({
2302
+ ...r,
2303
+ note: 'paralel ajan başladı — ana sohbet açık; bittiğinde özet otomatik gelir',
2304
+ });
2305
+ }
2306
+ if (name === 'run_background_many') {
2307
+ const rawList = Array.isArray(args.tasks) ? args.tasks : [];
2308
+ const tasks = rawList
2309
+ .map((t) => ({ title: String((t && t.title) || ''), task: String((t && t.task) || t || '') }))
2310
+ .filter((t) => t.task.trim());
2311
+ const r = tasks.length
2312
+ ? this.runBackgroundMany(sessionId, tasks, String(args.title || ''))
2313
+ : { ok: false, error: 'tasks boş' };
2314
+ return JSON.stringify({
2315
+ ...r,
2316
+ note: 'fan-out başladı — eşzamanlılık kuyruğuna girer; hepsi bitince TEK birleşik rapor gelir',
2317
+ });
2318
+ }
2319
+ if (name === 'tasks_list') {
2320
+ const jobs = this.listBgJobs();
2321
+ return JSON.stringify({ ok: true, jobs });
2322
+ }
2323
+ if (name === 'task_status') {
2324
+ return JSON.stringify(this.bgDetail(String(args.id || args.code || '')));
2325
+ }
2326
+ if (name === 'task_cancel') {
2327
+ const id = String(args.id || '');
2328
+ const ok = this.interrupt(id);
2329
+ return JSON.stringify(ok ? { ok: true, id } : { ok: false, error: 'çalışan ajan yok: ' + id });
2330
+ }
2331
+ if (name === 'set_reminder') {
2332
+ if (!this.reminders || typeof this.reminders.add !== 'function') {
2333
+ return JSON.stringify({ ok: false, error: 'hatırlatıcı kullanılamıyor' });
2334
+ }
2335
+ const r = await this.reminders.add({
2336
+ when: args.when,
2337
+ message: args.message,
2338
+ repeat: args.repeat,
2339
+ sessionId,
2340
+ });
2341
+ return JSON.stringify(r);
2342
+ }
2343
+ if (name === 'watcher_add' || name === 'watcher_list' || name === 'watcher_remove') {
2344
+ if (!this.watchers) {
2345
+ return JSON.stringify({ ok: false, error: 'izleyici servisi kullanılamıyor' });
2346
+ }
2347
+ if (name === 'watcher_list') {
2348
+ const rows = this.watchers.list().map((w) => ({
2349
+ id: w.id,
2350
+ name: w.name,
2351
+ kind: w.kind,
2352
+ url: w.url || undefined,
2353
+ op: w.op,
2354
+ value: w.value,
2355
+ everyMin: w.everyMin,
2356
+ enabled: w.enabled,
2357
+ lastValue: w.lastValue,
2358
+ lastError: w.lastError || undefined,
2359
+ }));
2360
+ return JSON.stringify({ ok: true, watchers: rows });
2361
+ }
2362
+ if (name === 'watcher_add') {
2363
+ return JSON.stringify(this.watchers.add({ ...(args || {}), sessionId }));
2364
+ }
2365
+ return JSON.stringify(this.watchers.remove(String((args && args.id) || '')));
2366
+ }
2367
+ if (name === 'kb_add' || name === 'kb_search') {
2368
+ const kb = require('./kb');
2369
+ if (name === 'kb_add') {
2370
+ return JSON.stringify(
2371
+ kb.add(String(args.title || ''), String(args.text || ''), {
2372
+ source: args.source || `oturum:${sessionId}`,
2373
+ tags: args.tags,
2374
+ })
2375
+ );
2376
+ }
2377
+ const rows = kb.search(String(args.query || ''), clamp(Number(args.limit) || 5, 1, 10));
2378
+ return JSON.stringify({
2379
+ ok: true,
2380
+ query: String(args.query || ''),
2381
+ results: rows,
2382
+ note: rows.length ? 'kaynakları (citation) cevabında aynen belirt' : 'eşleşme yok',
2383
+ });
2384
+ }
2385
+ if (name === 'memory_hygiene') {
2386
+ const r = memory.hygiene({ maxAgeDays: Number(args.maxAgeDays) || undefined });
2387
+ return JSON.stringify(r);
2388
+ }
2389
+ if (name === 'event_subscribe' || name === 'event_list' || name === 'event_unsubscribe') {
2390
+ if (!this.bus) {
2391
+ return JSON.stringify({ ok: false, error: 'olay merkezi kapalı — Ayarlar > Entegrasyonlar' });
2392
+ }
2393
+ if (name === 'event_list') {
2394
+ const rows = this.bus.listSubs().filter((s) => s.sessionId === sessionId);
2395
+ return JSON.stringify({ ok: true, subscriptions: rows });
2396
+ }
2397
+ if (name === 'event_subscribe') {
2398
+ const r = this.bus.addSub({
2399
+ type: args && args.type,
2400
+ sessionId,
2401
+ op: args && args.op,
2402
+ value: args && args.value,
2403
+ cooldownMin: args && args.cooldownMin,
2404
+ });
2405
+ if (r.ok) {
2406
+ emitSafe(this, sessionId, { type: 'status', status: `olay aboneliği: ${args.type}` });
2407
+ }
2408
+ return JSON.stringify(r);
2409
+ }
2410
+ return JSON.stringify(this.bus.removeSub(String((args && args.id) || '')));
2411
+ }
2412
+ if (name === 'email_list' || name === 'email_read' || name === 'email_send') {
2413
+ if (!this.email || typeof this.email[name === 'email_list' ? 'list' : name === 'email_read' ? 'read' : 'send'] !== 'function') {
2414
+ return JSON.stringify({ ok: false, error: 'e-posta kullanılamıyor — Entegrasyonlar\u0027da ayarla' });
2415
+ }
2416
+ if (name === 'email_list') return JSON.stringify(await this.email.list(args || {}));
2417
+ if (name === 'email_read') return JSON.stringify(await this.email.read(args || {}));
2418
+ return JSON.stringify(await this.email.send(args || {}));
2419
+ }
2420
+ if (name === 'computer_look') {
2421
+ if (!this.computer || typeof this.computer.look !== 'function') {
2422
+ return JSON.stringify({ ok: false, error: 'ekran erişimi yok' });
2423
+ }
2424
+ emitSafe(this, sessionId, { type: 'status', status: 'ekrana bakıyor' });
2425
+ const shot = await this.computer.look();
2426
+ if (!shot) return JSON.stringify({ ok: false, error: 'ekran görüntüsü alınamadı' });
2427
+ /* görsel sonraki tura vision mesajı olarak enjekte edilir */
2428
+ return JSON.stringify({
2429
+ ok: true,
2430
+ note: 'ekran görüntüsü alındı — görsel aşağıda; koordinatlar 1280x720 tabanlı',
2431
+ __injectImage: shot,
2432
+ });
2433
+ }
2434
+ if (name === 'computer_act') {
2435
+ if (!this.computer || typeof this.computer.act !== 'function') {
2436
+ return JSON.stringify({ ok: false, error: 'fare/klavye erişimi yok' });
2437
+ }
2438
+ const op = String(args.op || '');
2439
+ /* type ve key güvenlik: RISKY davranır ama onay kapısı kapalı; hız sınırı module içinde */
2440
+ emitSafe(this, sessionId, { type: 'status', status: `bilgisayar: ${op}` });
2441
+ const r = await this.computer.act(op, args || {});
2442
+ return JSON.stringify(r);
2443
+ }
2444
+ if (name === 'web_search') {
2445
+ /* SIRA: 0) TinyFish (anahtar girildiyse ZİRVEDE)
2446
+ 1) dahili tarayıcı (gerçek Chromium ile DİREK GOOGLE — bot koruması yok)
2447
+ 2) python çoklu-motor (ddgs / DDG+Bing+Mojeek)
2448
+ 3) Exa (anahtar girildiyse, son çare) */ const q = String((args && args.query) || '');
2449
+ let r = null;
2450
+ try { r = await tools.tinyfishSearch(q, Number((args && args.max_results) || 8), signal); } catch {}
2451
+ if (!r || !r.ok || !(r.results || []).length) {
2452
+ if (this.browser && typeof this.browser.search === 'function') {
2453
+ try { r = await this.browser.search(q, signal); } catch {}
2454
+ }
2455
+ }
2456
+ if (!r || !r.ok || !(r.results || []).length) {
2457
+ try {
2458
+ r = JSON.parse(await tools.exec('web_search', args, { cwd: this.workspace, signal }));
2459
+ } catch {}
2460
+ }
2461
+ return JSON.stringify(r || { ok: false, error: 'web arama başarısız — tüm motorlar boş döndü' });
2462
+ }
2463
+ if (name === 'ocr_read') {
2464
+ return JSON.stringify(await this._ocrRead(args || {}, signal));
2465
+ }
2466
+ if (name === 'browser_open') {
2467
+ if (!this.browser || typeof this.browser.openUrl !== 'function') {
2468
+ return JSON.stringify({ ok: false, error: 'dahili tarayıcı kullanılamıyor' });
2469
+ }
2470
+ const r = await this.browser.openUrl(String(args.url || ''), signal);
2471
+ return JSON.stringify(r);
2472
+ }
2473
+ if (name === 'browser_read') {
2474
+ if (!this.browser || typeof this.browser.readText !== 'function') {
2475
+ return JSON.stringify({ ok: false, error: 'dahili tarayıcı kullanılamıyor' });
2476
+ }
2477
+ const r = await this.browser.readText(signal);
2478
+ return JSON.stringify(r);
2479
+ }
2480
+ if (name === 'browser_screenshot') {
2481
+ if (!this.browser || typeof this.browser.screenshot !== 'function') {
2482
+ return JSON.stringify({ ok: false, error: 'dahili tarayıcı kullanılamıyor' });
2483
+ }
2484
+ const r = await this.browser.screenshot(signal);
2485
+ return JSON.stringify(r);
2486
+ }
2487
+ if (name === 'browser_snapshot') {
2488
+ if (!this.browser || typeof this.browser.snapshot !== 'function') {
2489
+ return JSON.stringify({ ok: false, error: 'dahili tarayıcı kullanılamıyor' });
2490
+ }
2491
+ const r = await this.browser.snapshot(signal);
2492
+ return JSON.stringify(r);
2493
+ }
2494
+ if (name === 'browser_click' || name === 'browser_type' || name === 'browser_press' || name === 'browser_scroll' || name === 'browser_select') {
2495
+ if (!this.browser || typeof this.browser.act !== 'function') {
2496
+ return JSON.stringify({ ok: false, error: 'dahili tarayıcı kullanılamıyor' });
2497
+ }
2498
+ const r = await this.browser.act(name.slice(8), args, signal);
2499
+ return JSON.stringify(r);
2500
+ }
2501
+ if (!(name === 'run_command' || name === 'read_file' || name === 'write_file' || name === 'list_dir' ||
2502
+ name === 'web_search' || name === 'http_fetch' || name === 'python_run')) {
2503
+ return JSON.stringify({ ok: false, error: `unknown tool ${name}` });
2504
+ }
2505
+ return await tools.exec(name, args, { cwd: this.workspace, signal });
2506
+ } catch (e) {
2507
+ return JSON.stringify({ ok: false, error: String((e && e.message) || e) });
2508
+ }
2509
+ }
2510
+
2511
+ interrupt(sessionId) {
2512
+ const c = this.ctrls.get(String(sessionId));
2513
+ if (!c) return false;
2514
+ c.abort();
2515
+ return true;
2516
+ }
2517
+
2518
+ /* /stop anahtarı: koşan HER ŞEYİ kes — tüm oturum turları (masaüstü + WA +
2519
+ paralel ajanlar) ve bekleyen rapor kuyruğu. Cron/izleyici/olay merkezi
2520
+ main katmanında BAĞIMSIZ yaşar — /stop onlara dokunmaz. */
2521
+ stopAll() {
2522
+ let aborted = 0;
2523
+ /* fan-out gruplarını kapat — yarım grup artık birleşik rapor beklemesin */
2524
+ if (this._bgGroups) {
2525
+ for (const g of this._bgGroups.values()) g.dead = true;
2526
+ }
2527
+ if (this._bgPendingStart) this._bgPendingStart.clear();
2528
+ for (const [sid, c] of this.ctrls) {
2529
+ try { c.abort(); aborted++; } catch {}
2530
+ /* abort sonrası _run finally bloğu temizler; paralel ajan kaydını biz işaretleyelim */
2531
+ if (this._bgJobs.has(sid)) {
2532
+ const job = this._bgJobs.get(sid);
2533
+ if (job.status === 'running' || job.status === 'queued') {
2534
+ job.status = 'aborted';
2535
+ job.endedAt = nowIso();
2536
+ }
2537
+ job.revive = false; // bekleyen öz-kurtarma gönderimini de öldür
2538
+ this._clearKicks(sid);
2539
+ }
2540
+ }
2541
+ /* kuyrukta bekleyenleri de kes */
2542
+ for (const j of this._bgJobs.values()) {
2543
+ if (j.status === 'queued') {
2544
+ j.status = 'aborted';
2545
+ j.endedAt = nowIso();
2546
+ aborted++;
2547
+ }
2548
+ }
2549
+ if (this._pendingReports && this._pendingReports.length) {
2550
+ this._pendingReports = [];
2551
+ }
2552
+ this._bgEmit();
2553
+ return aborted;
2554
+ }
2555
+
2556
+ isBusy(sessionId) {
2557
+ return this.ctrls.has(String(sessionId));
2558
+ }
2559
+ }
2560
+
2561
+ function emitSafe(engine, sessionId, ev) {
2562
+ engine.emit({ ...ev, sessionId });
2563
+ }
2564
+
2565
+ const TOOLS = [
2566
+ ...tools.definitions,
2567
+ {
2568
+ type: 'function',
2569
+ function: {
2570
+ name: 'memory_write',
2571
+ description:
2572
+ 'Append one permanent fact/preference/correction to long-term memory. Use for important user info worth remembering in future chats.',
2573
+ parameters: {
2574
+ type: 'object',
2575
+ properties: {
2576
+ text: { type: 'string', description: 'One concise line to remember forever' },
2577
+ },
2578
+ required: ['text'],
2579
+ },
2580
+ },
2581
+ },
2582
+ {
2583
+ type: 'function',
2584
+ function: {
2585
+ name: 'memory_search',
2586
+ description:
2587
+ 'Search long-term memory by keywords. Use when you suspect an old stored fact/preference is relevant but was not included in context.',
2588
+ parameters: {
2589
+ type: 'object',
2590
+ properties: {
2591
+ query: { type: 'string' },
2592
+ limit: { type: 'number', description: 'default 5' },
2593
+ },
2594
+ required: ['query'],
2595
+ },
2596
+ },
2597
+ },
2598
+ {
2599
+ type: 'function',
2600
+ function: {
2601
+ name: 'todo_write',
2602
+ description:
2603
+ '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.',
2604
+ parameters: {
2605
+ type: 'object',
2606
+ properties: {
2607
+ items: {
2608
+ type: 'array',
2609
+ maxItems: 20,
2610
+ items: {
2611
+ type: 'object',
2612
+ properties: {
2613
+ title: { type: 'string' },
2614
+ status: { type: 'string', enum: ['pending', 'active', 'done'] },
2615
+ },
2616
+ required: ['title'],
2617
+ },
2618
+ },
2619
+ },
2620
+ required: ['items'],
2621
+ },
2622
+ },
2623
+ },
2624
+ {
2625
+ type: 'function',
2626
+ function: {
2627
+ name: 'delegate_task',
2628
+ description:
2629
+ 'Spawn a fresh sub-agent that independently works on ONE self-contained sub-task (with the same tools except delegation) and returns only its final answer. Use for heavy/isolated research or multi-file jobs you want kept off the main context.',
2630
+ parameters: {
2631
+ type: 'object',
2632
+ properties: {
2633
+ task: { type: 'string', description: 'Clear, self-contained instructions for the sub-agent' },
2634
+ context: { type: 'string', description: 'Optional data/notes the sub-agent needs' },
2635
+ },
2636
+ required: ['task'],
2637
+ },
2638
+ },
2639
+ },
2640
+ {
2641
+ type: 'function',
2642
+ function: {
2643
+ name: 'set_reminder',
2644
+ description:
2645
+ 'Schedule a reminder for the user. Without repeat: one-time at the given date+time (compute exact local datetime from ORTAM). With repeat: recurring task — time-of-day comes from `when` and it fires forever at that cadence ("her sabah 09:00" => when=<next 09:00>, repeat=daily). Delivered back to this chat.',
2646
+ parameters: {
2647
+ type: 'object',
2648
+ properties: {
2649
+ when: {
2650
+ type: 'string',
2651
+ description:
2652
+ 'Local datetime, format YYYY-MM-DDTHH:mm (24h), e.g. 2026-08-27T09:00. For recurring reminders this fixes the hour/minute (and weekday/day-of-month).',
2653
+ },
2654
+ message: { type: 'string', description: 'What to remind, short and clear' },
2655
+ repeat: {
2656
+ type: 'string',
2657
+ description:
2658
+ "Omit for one-time. Recurring presets: 'daily' | 'weekdays' | 'weekly' | 'monthly'; or a raw 5-field cron expression like '0 9 * * *' or '*/30 * * * *'.",
2659
+ },
2660
+ },
2661
+ required: ['when', 'message'],
2662
+ },
2663
+ },
2664
+ },
2665
+ {
2666
+ type: 'function',
2667
+ function: {
2668
+ name: 'watcher_add',
2669
+ description:
2670
+ 'Create a background watcher that periodically checks something and notifies THIS chat when a condition becomes true (edge-triggered: fires once per crossing; cooldown limits repeats). Use for price alarms ("X altına düşerse haber ver"), battery levels, page change detection.',
2671
+ parameters: {
2672
+ type: 'object',
2673
+ properties: {
2674
+ name: { type: 'string', description: 'Short label, e.g. "GOLD 2300 alarmı"' },
2675
+ kind: { type: 'string', enum: ['web', 'battery'], description: 'web: fetch URL and extract value; battery: local battery percent' },
2676
+ url: { type: 'string', description: 'http(s) URL to poll (kind=web, required)' },
2677
+ path: {
2678
+ type: 'string',
2679
+ description:
2680
+ "If response is JSON: dot-path to watched value, e.g. 'price.usd' or 'data.0.close' (recommended over re)",
2681
+ },
2682
+ re: { type: 'string', description: 'If response is HTML/text: regex; first capture group becomes the value' },
2683
+ op: {
2684
+ type: 'string',
2685
+ enum: ['lt', 'lte', 'gt', 'gte', 'eq', 'neq', 'changed'],
2686
+ description: "Comparison vs value. Use 'changed' with no value for page-change alerts.",
2687
+ },
2688
+ value: { type: 'number', description: 'Threshold for numeric ops (not needed for changed)' },
2689
+ everyMin: { type: 'number', description: 'Check interval in minutes (default 15, min 1, max 1440)' },
2690
+ everySec: { type: 'number', description: 'Check interval in SECONDS (10-8640) — overrides everyMin for fast watchers (e.g. 30 = every 30s)' },
2691
+ cooldownMin: { type: 'number', description: 'Min minutes between notifications (default 60)' },
2692
+ },
2693
+ required: ['name', 'kind'],
2694
+ },
2695
+ },
2696
+ },
2697
+ {
2698
+ type: 'function',
2699
+ function: {
2700
+ name: 'watcher_list',
2701
+ description: 'List configured background watchers with their last values and errors.',
2702
+ parameters: { type: 'object', properties: {} },
2703
+ },
2704
+ },
2705
+ {
2706
+ type: 'function',
2707
+ function: {
2708
+ name: 'watcher_remove',
2709
+ description: 'Delete a background watcher by its id (from watcher_list).',
2710
+ parameters: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'] },
2711
+ },
2712
+ },
2713
+ {
2714
+ type: 'function',
2715
+ function: {
2716
+ name: 'kb_add',
2717
+ description:
2718
+ 'Store a durable piece of knowledge (learned fact, procedure, domain insight) into the knowledge base for future sessions. Do NOT store transient chat details. Always set source (where this came from).',
2719
+ parameters: {
2720
+ type: 'object',
2721
+ properties: {
2722
+ title: { type: 'string', description: 'Short title, e.g. "FxPro GOLD seans saatleri"' },
2723
+ text: { type: 'string', description: 'The knowledge itself, self-contained, max ~2000 chars.' },
2724
+ source: { type: 'string', description: 'Origin: url, file path, or conversation.' },
2725
+ tags: { type: 'array', items: { type: 'string' }, description: 'Up to 8 short tags.' },
2726
+ },
2727
+ required: ['title', 'text'],
2728
+ },
2729
+ },
2730
+ },
2731
+ {
2732
+ type: 'function',
2733
+ function: {
2734
+ name: 'kb_search',
2735
+ description:
2736
+ 'Search the knowledge base with TF-IDF ranking. Results come with citations — quote them verbatim when you use a result.',
2737
+ parameters: {
2738
+ type: 'object',
2739
+ properties: {
2740
+ query: { type: 'string' },
2741
+ limit: { type: 'number', description: 'default 5, max 10' },
2742
+ },
2743
+ required: ['query'],
2744
+ },
2745
+ },
2746
+ },
2747
+ {
2748
+ type: 'function',
2749
+ function: {
2750
+ name: 'memory_hygiene',
2751
+ description:
2752
+ 'Run memory cleanup: dedupe near-identical MEMORY.md entries and drop very old ones. Run it when the user asks to clean memory, or after many memory_write calls in one session.',
2753
+ parameters: {
2754
+ type: 'object',
2755
+ properties: {
2756
+ maxAgeDays: { type: 'number', description: 'Age cutoff in days (default 120).' },
2757
+ },
2758
+ },
2759
+ },
2760
+ },
2761
+ {
2762
+ type: 'function',
2763
+ function: {
2764
+ name: 'event_subscribe',
2765
+ description:
2766
+ 'Subscribe this chat to a live event source (no polling/cron needed). Types: mail:new (incoming email), price:tick (price feed with optional op/value filter e.g. alert when gold-pegged PAXG drops below 2600), fs:changed (workspace file changes), webhook (custom POST events), wa:presence. Notifies THIS chat when the event fires; cooldownMin limits repeats.',
2767
+ parameters: {
2768
+ type: 'object',
2769
+ properties: {
2770
+ type: {
2771
+ type: 'string',
2772
+ enum: ['mail:new', 'price:tick', 'fs:changed', 'webhook', 'wa:presence'],
2773
+ description: 'Event source to subscribe to.',
2774
+ },
2775
+ op: { type: 'string', enum: ['lt', 'lte', 'gt', 'gte', 'eq'], description: 'Optional numeric filter on event value (mainly price:tick).' },
2776
+ value: { type: 'number', description: 'Threshold for the numeric filter.' },
2777
+ cooldownMin: { type: 'number', description: 'Minimum minutes between notifications (default 10).' },
2778
+ },
2779
+ required: ['type'],
2780
+ },
2781
+ },
2782
+ },
2783
+ {
2784
+ type: 'function',
2785
+ function: {
2786
+ name: 'event_list',
2787
+ description: 'List this chat\u2019s active event subscriptions.',
2788
+ parameters: { type: 'object', properties: {} },
2789
+ },
2790
+ },
2791
+ {
2792
+ type: 'function',
2793
+ function: {
2794
+ name: 'event_unsubscribe',
2795
+ description: 'Cancel an event subscription by its id (from event_list).',
2796
+ parameters: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'] },
2797
+ },
2798
+ },
2799
+ {
2800
+ type: 'function',
2801
+ function: {
2802
+ name: 'run_background',
2803
+ description:
2804
+ 'Spawn a PARALLEL background agent for a self-contained subtask (e.g. "read this terminal output", "research X", "monitor and summarize"). The main chat is NOT blocked; multiple agents can run at once; when each finishes, its summary report is delivered to this chat automatically. In CEO mode EVERY concrete task goes through here — you never execute work yourself. Keep research SHALLOW: 3-5 sources, ~3 minutes target; if info cannot be found after 2-3 attempts, finish with partial findings and clearly state what was not found — never get stuck.',
2805
+ parameters: {
2806
+ type: 'object',
2807
+ properties: {
2808
+ task: { type: 'string', description: 'Self-contained task description with ALL needed context (paths, URLs, constraints, expected output) — the agent cannot see your context.' },
2809
+ title: { type: 'string', description: 'Short label shown in the agents tab and notification.' },
2810
+ },
2811
+ required: ['task'],
2812
+ },
2813
+ },
2814
+ },
2815
+ {
2816
+ type: 'function',
2817
+ function: {
2818
+ name: 'tasks_list',
2819
+ description:
2820
+ 'List parallel background agents with status (queued/running/done/error/aborted), titles and timings. Use to report overall progress.',
2821
+ parameters: { type: 'object', properties: {} },
2822
+ },
2823
+ },
2824
+ {
2825
+ type: 'function',
2826
+ function: {
2827
+ name: 'run_background_many',
2828
+ description:
2829
+ "Fan-out: spawn MULTIPLE parallel background agents at once for different steps of the SAME job (e.g. news collection → TR / World / Economy agents in parallel; or research A + scrape B + analyze C). They run concurrently; when ALL finish you get ONE merged report instead of N separate pings. Each task must be self-contained. Use this over repeated run_background when steps are independent.",
2830
+ parameters: {
2831
+ type: 'object',
2832
+ properties: {
2833
+ title: { type: 'string', description: 'Group label (e.g. "Haber Taraması")' },
2834
+ tasks: {
2835
+ type: 'array',
2836
+ description: '2-6 independent sub-tasks',
2837
+ items: {
2838
+ type: 'object',
2839
+ properties: {
2840
+ title: { type: 'string', description: 'Short label of this step' },
2841
+ task: { type: 'string', description: 'Self-contained instruction with all context' },
2842
+ },
2843
+ required: ['task'],
2844
+ },
2845
+ },
2846
+ },
2847
+ required: ['tasks'],
2848
+ },
2849
+ },
2850
+ },
2851
+ {
2852
+ type: 'function',
2853
+ function: {
2854
+ name: 'task_status',
2855
+ description:
2856
+ 'Get one background agent\u2019s live detail by id/code: current status plus a tail of its working transcript. Use when the user asks what an agent found or whether it finished.',
2857
+ parameters: {
2858
+ type: 'object',
2859
+ properties: {
2860
+ id: { type: 'string', description: 'job id or session code from tasks_list' },
2861
+ },
2862
+ required: ['id'],
2863
+ },
2864
+ },
2865
+ },
2866
+ {
2867
+ type: 'function',
2868
+ function: {
2869
+ name: 'send_file',
2870
+ description:
2871
+ "Deliver a local file to the user in THIS chat (desktop shows a file card; if this session is a WhatsApp chat the file is sent there as image/document). Use when the user asks for a produced report/PDF/image/export, e.g. 'raporu bana gönder', 'grafik dosyasını at'. path = absolute or workspace-relative.",
2872
+ parameters: {
2873
+ type: 'object',
2874
+ properties: {
2875
+ path: { type: 'string', description: 'File path (absolute or relative to workspace)' },
2876
+ caption: { type: 'string', description: 'Optional short caption' },
2877
+ },
2878
+ required: ['path'],
2879
+ },
2880
+ },
2881
+ },
2882
+ {
2883
+ type: 'function',
2884
+ function: {
2885
+ name: 'task_cancel',
2886
+ description: 'Cancel a running background agent by id (from tasks_list).',
2887
+ parameters: {
2888
+ type: 'object',
2889
+ properties: {
2890
+ id: { type: 'string' },
2891
+ },
2892
+ required: ['id'],
2893
+ },
2894
+ },
2895
+ },
2896
+ {
2897
+ type: 'function',
2898
+ function: {
2899
+ name: 'computer_look',
2900
+ description:
2901
+ 'Take a screenshot of the user\u2019s screen and receive it as an image. Use before computer_act to see the GUI; coordinates are on a 1280x720 basis.',
2902
+ parameters: { type: 'object', properties: {} },
2903
+ },
2904
+ },
2905
+ {
2906
+ type: 'function',
2907
+ function: {
2908
+ name: 'computer_act',
2909
+ description:
2910
+ 'Control mouse/keyboard on the user\u2019s Windows desktop (GUI automation). Ops: click{x,y}, dblclick{x,y}, rightclick{x,y}, move{x,y}, type{text}, key{combo e.g. "ctrl+s","enter","alt+tab"}, scroll{x,y,dy}. Look first with computer_look, act step by step, look again after acting.',
2911
+ parameters: {
2912
+ type: 'object',
2913
+ properties: {
2914
+ op: {
2915
+ type: 'string',
2916
+ enum: ['click', 'dblclick', 'rightclick', 'move', 'type', 'key', 'scroll'],
2917
+ },
2918
+ x: { type: 'number' },
2919
+ y: { type: 'number' },
2920
+ text: { type: 'string', description: 'for op=type (max 2000 chars)' },
2921
+ combo: { type: 'string', description: 'for op=key: "enter", "ctrl+s", "alt+tab", "win"' },
2922
+ dy: { type: 'number', description: 'for op=scroll: positive = down (-10..10)' },
2923
+ },
2924
+ required: ['op'],
2925
+ },
2926
+ },
2927
+ },
2928
+ {
2929
+ type: 'function',
2930
+ function: {
2931
+ name: 'email_list',
2932
+ description:
2933
+ 'List recent emails from the configured inbox (newest first). Use when the user asks about incoming mail / inbox summary. Returns uid, from, subject, date.',
2934
+ parameters: {
2935
+ type: 'object',
2936
+ properties: {
2937
+ limit: { type: 'number', description: 'default 10, max 30' },
2938
+ unread: { type: 'boolean', description: 'only unseen mails' },
2939
+ },
2940
+ },
2941
+ },
2942
+ },
2943
+ {
2944
+ type: 'function',
2945
+ function: {
2946
+ name: 'email_read',
2947
+ description: 'Read one email\u0027s full body text by its uid (from email_list).',
2948
+ parameters: {
2949
+ type: 'object',
2950
+ properties: {
2951
+ uid: { type: 'number' },
2952
+ },
2953
+ required: ['uid'],
2954
+ },
2955
+ },
2956
+ },
2957
+ {
2958
+ type: 'function',
2959
+ function: {
2960
+ name: 'email_send',
2961
+ description: 'Send an email from the configured account.',
2962
+ parameters: {
2963
+ type: 'object',
2964
+ properties: {
2965
+ to: { type: 'string', description: 'recipient address' },
2966
+ subject: { type: 'string' },
2967
+ body: { type: 'string', description: 'plain text body' },
2968
+ },
2969
+ required: ['to', 'subject', 'body'],
2970
+ },
2971
+ },
2972
+ },
2973
+ {
2974
+ type: 'function',
2975
+ function: {
2976
+ name: 'browser_open',
2977
+ description:
2978
+ 'Open a URL in the built-in visible browser panel (JS-rendered pages work). Use for pages that need JavaScript or when showing the user a page is useful; prefer http_fetch for plain reading.',
2979
+ parameters: {
2980
+ type: 'object',
2981
+ properties: {
2982
+ url: { type: 'string' },
2983
+ },
2984
+ required: ['url'],
2985
+ },
2986
+ },
2987
+ },
2988
+ {
2989
+ type: 'function',
2990
+ function: {
2991
+ name: 'browser_read',
2992
+ description:
2993
+ 'Read the visible text of the page currently open in the built-in browser panel.',
2994
+ parameters: { type: 'object', properties: {}, required: [] },
2995
+ },
2996
+ },
2997
+ {
2998
+ type: 'function',
2999
+ function: {
3000
+ name: 'browser_screenshot',
3001
+ description:
3002
+ 'Take a screenshot of the built-in browser panel; the image is shown to you on the next step. Use to visually inspect layouts/charts/captchas-ish pages before acting.',
3003
+ parameters: { type: 'object', properties: {}, required: [] },
3004
+ },
3005
+ },
3006
+ {
3007
+ type: 'function',
3008
+ function: {
3009
+ name: 'browser_snapshot',
3010
+ description:
3011
+ 'List all interactive elements of the built-in browser page as numbered refs: [3] <button> "Gönder". ALWAYS start with this; then act using ref numbers. Much more reliable than CSS selectors.',
3012
+ parameters: { type: 'object', properties: {}, required: [] },
3013
+ },
3014
+ },
3015
+ {
3016
+ type: 'function',
3017
+ function: {
3018
+ name: 'ocr_read',
3019
+ description:
3020
+ 'Built-in OCR (Tesseract) — extract TEXT from an image. Essential for text-only models that cannot see images: instead of browser_screenshot, call this with source="browser" to READ the current built-in browser page (captchas, canvas text, images). source: "browser" (default, current browser page) | "screen" (full desktop) | image file path. lang default "tur+eng". Returns {text}.',
3021
+ parameters: {
3022
+ type: 'object',
3023
+ properties: {
3024
+ source: { type: 'string', description: '"browser" (default) | "screen" | image file path' },
3025
+ lang: { type: 'string', description: 'OCR language, default "tur+eng"' },
3026
+ },
3027
+ },
3028
+ },
3029
+ },
3030
+ {
3031
+ type: 'function',
3032
+ function: {
3033
+ name: 'browser_click',
3034
+ description:
3035
+ 'Click an element in the built-in browser. Prefer ref from browser_snapshot (e.g. {"ref":3}); CSS selector or text=X also accepted.',
3036
+ parameters: {
3037
+ type: 'object',
3038
+ properties: {
3039
+ ref: { type: 'number', description: 'element number from browser_snapshot' },
3040
+ selector: { type: 'string', description: 'CSS selector or text=X alternative' },
3041
+ },
3042
+ },
3043
+ },
3044
+ },
3045
+ {
3046
+ type: 'function',
3047
+ function: {
3048
+ name: 'browser_type',
3049
+ description:
3050
+ 'Type text into an input/textarea/contenteditable in the built-in browser (prefer ref). Set submit=true to press Enter afterwards.',
3051
+ parameters: {
3052
+ type: 'object',
3053
+ properties: {
3054
+ ref: { type: 'number' },
3055
+ selector: { type: 'string' },
3056
+ text: { type: 'string' },
3057
+ submit: { type: 'boolean' },
3058
+ },
3059
+ required: ['text'],
3060
+ },
3061
+ },
3062
+ },
3063
+ {
3064
+ type: 'function',
3065
+ function: {
3066
+ name: 'browser_press',
3067
+ description:
3068
+ 'Press a key (Enter, Tab, Escape, ArrowDown…) on the focused element in the built-in browser; optionally focus a ref first.',
3069
+ parameters: {
3070
+ type: 'object',
3071
+ properties: {
3072
+ key: { type: 'string' },
3073
+ ref: { type: 'number', description: 'optional element to focus first' },
3074
+ },
3075
+ required: ['key'],
3076
+ },
3077
+ },
3078
+ },
3079
+ {
3080
+ type: 'function',
3081
+ function: {
3082
+ name: 'browser_scroll',
3083
+ description: 'Scroll the built-in browser page. direction: up | down.',
3084
+ parameters: {
3085
+ type: 'object',
3086
+ properties: {
3087
+ direction: { type: 'string', enum: ['up', 'down'] },
3088
+ amount: { type: 'number', description: 'pixels, default ~0.9x viewport' },
3089
+ },
3090
+ required: ['direction'],
3091
+ },
3092
+ },
3093
+ },
3094
+ {
3095
+ type: 'function',
3096
+ function: {
3097
+ name: 'browser_select',
3098
+ description:
3099
+ 'Pick an <option> of a dropdown (<select>) in the built-in browser by value/text (prefer ref).',
3100
+ parameters: {
3101
+ type: 'object',
3102
+ properties: {
3103
+ ref: { type: 'number' },
3104
+ selector: { type: 'string' },
3105
+ value: { type: 'string' },
3106
+ },
3107
+ required: ['value'],
3108
+ },
3109
+ },
3110
+ },
3111
+ ];
3112
+
3113
+ module.exports = Engine;
3114
+ module.exports.Engine = Engine;
3115
+ module.exports.sanitizeTodoItems = sanitizeTodoItems;
3116
+ module.exports.parseReflectionJson = parseReflectionJson;
3117
+ module.exports.CEO_EXEC_TOOLS = CEO_EXEC_TOOLS;
3118
+ module.exports.BG_HIDDEN_TOOLS = BG_HIDDEN_TOOLS;