beast-agent 2.4.0 → 2.4.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "beast-agent",
3
3
  "productName": "Beast Agent",
4
- "version": "2.4.0",
4
+ "version": "2.4.5",
5
5
  "description": "Ultra-fast local agent shell for Windows.",
6
6
  "author": "algokodcom (AlgoKod)",
7
7
  "license": "MIT",
@@ -987,9 +987,11 @@ class Engine {
987
987
  if (session.code) this._codeIndex.set(session.code, session.id);
988
988
  }
989
989
 
990
- /* Oturum dosyasındaki son 'msg' satırını (user) güncel içerikle değiştir —
991
- yanıtsız kalmış user mesajıyla yeni mesaj birleştirildiğinde kullanılır */
992
- _rewriteLastMsg(id, content, attachments) {
990
+ /* Oturum dosyasındaki son 'msg' satırını güncel içerikle değiştir —
991
+ yanıtsız kalmış user mesajıyla yeni mesaj birleştirildiğinde (role:'user')
992
+ ya da proaktif not son asistan mesajına birleştirildiğinde (role:'assistant') */
993
+ _rewriteLastMsg(id, content, attachments, role) {
994
+ const wantRole = role === 'assistant' ? 'assistant' : 'user';
993
995
  try {
994
996
  const file = this._file(String(id));
995
997
  const lines = fs.readFileSync(file, 'utf8').split('\n');
@@ -997,8 +999,8 @@ class Engine {
997
999
  if (!lines[i].trim()) continue;
998
1000
  let r;
999
1001
  try { r = JSON.parse(lines[i]); } catch { continue; }
1000
- if (r.t === 'msg' && r.role === 'user') {
1001
- const out = { t: 'msg', role: 'user', content: String(content || '') };
1002
+ if (r.t === 'msg' && r.role === wantRole) {
1003
+ const out = { t: 'msg', role: wantRole, content: String(content || '') };
1002
1004
  if (Array.isArray(attachments) && attachments.length) out.attachments = attachments;
1003
1005
  lines[i] = JSON.stringify(out);
1004
1006
  break;
@@ -2284,6 +2286,40 @@ class Engine {
2284
2286
  return true;
2285
2287
  }
2286
2288
 
2289
+ /* PROAKTİF MESAJ ENJEKSİYONU (Empati Loop): ajanın az önce KULLANICIYA
2290
+ bildirdiği metni, yeni bir tur BAŞLATMADAN geçmişe kendi asistan
2291
+ mesajı olarak işler. Model bir sonraki turda bu mesajı bağlamında
2292
+ "benim söylediğim söz" olarak görür — kullanıcı cevap verdiğinde
2293
+ sohbet aynı oturumda kopmadan devam eder. Son mesaj araçsız bir
2294
+ asistan mesajıysa metin ona BİRLEŞTİRİLİR — katı sağlayıcılarda
2295
+ art arda asistan mesajı gitmez. */
2296
+ injectAssistant(sessionId, text) {
2297
+ const s = this._load(String(sessionId));
2298
+ if (!s) return false;
2299
+ const body = String(text || '').slice(0, USER_MAX);
2300
+ if (!body.trim()) return false;
2301
+ const lastMsg = s.messages[s.messages.length - 1];
2302
+ if (
2303
+ lastMsg &&
2304
+ lastMsg.role === 'assistant' &&
2305
+ typeof lastMsg.content === 'string' &&
2306
+ !(Array.isArray(lastMsg.tool_calls) && lastMsg.tool_calls.length)
2307
+ ) {
2308
+ lastMsg.content = (lastMsg.content + '\n\n' + body).slice(0, USER_MAX);
2309
+ this._rewriteLastMsg(s.id, lastMsg.content, null, 'assistant');
2310
+ this.emit({ type: 'sessions' });
2311
+ return true;
2312
+ }
2313
+ const msg = { role: 'assistant', content: body };
2314
+ s.messages.push(msg);
2315
+ try {
2316
+ this._append(s, msg);
2317
+ } catch {}
2318
+ this.emit({ type: 'message', sessionId: s.id, message: msg });
2319
+ this.emit({ type: 'sessions' });
2320
+ return true;
2321
+ }
2322
+
2287
2323
  /* Paralel ajan GEÇMİŞİNİ topluca sil: çalışanlar/ bekleyenler iptal edilir,
2288
2324
  tüm bg oturum dosyaları + kalıcı kayıt temizlenir. Döndürür: silinen iş sayısı */
2289
2325
  clearAllBgJobs() {
@@ -93,6 +93,161 @@ function saveState(st) {
93
93
  } catch {}
94
94
  }
95
95
 
96
+ /* ---------- empati hafızası: sohbet kaydı + öğrenilen ilgi alanları ----------
97
+ Sohbet akışı (chat/WA/TG/DC) main tarafından buraya düşürülür; kullanıcı
98
+ mesajlarından anlamlı kelimeler toplanır, sık geçenler "öğrenilen ilgi
99
+ alanı" olur. Tarama filtresi ve bildirim mesajı bu hafızayla kişiselleşir:
100
+ elle girilen ilgi alanları + öğrenilenler birlikte prompta girer. */
101
+
102
+ const CHAT_CAP = 120; // son sohbet kaydı tavanı
103
+ const INTEREST_CAP = 24; // öğrenilen ilgi etiketi tavanı
104
+ const CHAT_SNIPPET = 220;
105
+
106
+ const TR_FOLD_MEM = { ç: 'c', ğ: 'g', ı: 'i', ö: 'o', ş: 's', ü: 'u', â: 'a', î: 'i', û: 'u' };
107
+
108
+ function memFold(s) {
109
+ return String(s || '').toLowerCase().replace(/[çğıöşüâîû]/g, (ch) => TR_FOLD_MEM[ch] || ch);
110
+ }
111
+
112
+ /* gereksiz kelimeler — ilgi alanı sayılmaz. Set FOLD'LU kurulur: kelimeler
113
+ zaten memFold ile ASCII'ye indirgendikten sonra arandığı için 'bugün' gibi
114
+ stopword'ler fold'lu haliyle ('bugun') listede olmalı. */
115
+ const MEM_STOP_RAW =
116
+ 'the and for with this that have from was are were been will would could should ' +
117
+ 'bir bu şu o ve ile için gibi ama fakat çok daha en ne mi mı mu mü de da ki ise ya ' +
118
+ 'veya yani bana bize sana ona beni bizi seni onun benim bizim senin onları var yok ' +
119
+ 'olur olan olarak nasıl neden niye şey şeyi her hem acaba lazım gerek evet hayır ' +
120
+ 'tamam tmm kanka abi hocam merhaba selam hey lütfen teşekkür sağol eyvallah bugün ' +
121
+ 'yarın dün şimdi sonra önce hala henüz yine tekrar biraz az kadar çünkü eğer ' +
122
+ 'yapabilir misin musun isterim istiyorum lazım gerekiyor beast asistan önemli ' +
123
+ 'hakkında üzerine konuştuk konuşuyoruz konuşmak kendi aynı başka diğer belki ' +
124
+ 'sanırım galiba gerekli olsun olmuyor oluyor bakalım biriyle hangi kim nerede';
125
+
126
+ const MEM_STOP = new Set(MEM_STOP_RAW.split(/\s+/).map((w) => memFold(w)).filter(Boolean));
127
+
128
+ function chatFile() {
129
+ return path.join(beastRoot(), 'perception', 'chatmem.json');
130
+ }
131
+
132
+ function loadChatMem() {
133
+ try {
134
+ const r = JSON.parse(fs.readFileSync(chatFile(), 'utf8'));
135
+ return {
136
+ chats: Array.isArray(r.chats) ? r.chats : [],
137
+ learned: r.learned && typeof r.learned === 'object' ? r.learned : {},
138
+ };
139
+ } catch {
140
+ return { chats: [], learned: {} };
141
+ }
142
+ }
143
+
144
+ function saveChatMem(cm) {
145
+ try {
146
+ fs.mkdirSync(path.dirname(chatFile()), { recursive: true });
147
+ fs.writeFileSync(chatFile(), JSON.stringify(cm));
148
+ } catch {}
149
+ }
150
+
151
+ /* Sohbet parçasını hafızaya yaz (kullanıcı + beast cevapları + proaktif mesajlar) */
152
+ function rememberConversation(text, channel) {
153
+ try {
154
+ const t = String(text || '').replace(/\s+/g, ' ').trim().slice(0, CHAT_SNIPPET);
155
+ if (t.length < 4) return false;
156
+ const cm = loadChatMem();
157
+ /* birebir tekrar yut */
158
+ if (cm.chats.length && cm.chats[cm.chats.length - 1].t === t) return false;
159
+ cm.chats.push({ ts: new Date().toISOString(), ch: String(channel || 'sohbet').slice(0, 12), t });
160
+ while (cm.chats.length > CHAT_CAP) cm.chats.shift();
161
+ saveChatMem(cm);
162
+ return true;
163
+ } catch {
164
+ return false;
165
+ }
166
+ }
167
+
168
+ /* Kullanıcı mesajından ilgi alanı öğren: fold + stopword temizliği, sayaca yaz */
169
+ function learnInterests(text) {
170
+ try {
171
+ const toks = (memFold(text).match(/[a-z0-9_+#.]{3,}/g) || [])
172
+ .filter((w) => !MEM_STOP.has(w) && !/^\d+$/.test(w))
173
+ .slice(0, 12);
174
+ if (!toks.length) return false;
175
+ const cm = loadChatMem();
176
+ const now = Date.now();
177
+ for (const w of toks) {
178
+ const cur = cm.learned[w] || { c: 0, at: 0 };
179
+ cur.c++;
180
+ cur.at = now;
181
+ cm.learned[w] = cur;
182
+ }
183
+ /* tavana in: en sık + en taze etiketler kalır */
184
+ const keys = Object.keys(cm.learned);
185
+ if (keys.length > INTEREST_CAP) {
186
+ keys.sort((a, b) => (cm.learned[b].c - cm.learned[a].c) || (cm.learned[b].at - cm.learned[a].at));
187
+ for (const k of keys.slice(INTEREST_CAP)) delete cm.learned[k];
188
+ }
189
+ saveChatMem(cm);
190
+ return true;
191
+ } catch {
192
+ return false;
193
+ }
194
+ }
195
+
196
+ /* Elle girilen + öğrenilen ilgi alanları birleşik (promptlar bunu kullanır) */
197
+ function combinedInterests(cfg) {
198
+ const manual = String((cfg && cfg.interests) || '').trim();
199
+ try {
200
+ const cm = loadChatMem();
201
+ const learned = Object.keys(cm.learned)
202
+ .sort((a, b) => cm.learned[b].c - cm.learned[a].c)
203
+ .slice(0, 12);
204
+ return [manual, learned.join(', ')].filter(Boolean).join(', ');
205
+ } catch {
206
+ return manual;
207
+ }
208
+ }
209
+
210
+ /* Son konuşma satırları — compose/filtre promptuna bağlam olarak girer */
211
+ function chatContextLine(max = 8) {
212
+ try {
213
+ const cm = loadChatMem();
214
+ const items = cm.chats.slice(-Math.max(1, Number(max) || 8));
215
+ if (!items.length) return '';
216
+ const when = (iso) => {
217
+ try {
218
+ const d = new Date(iso);
219
+ return ('0' + d.getDate()).slice(-2) + '.' + ('0' + (d.getMonth() + 1)).slice(-2);
220
+ } catch { return ''; }
221
+ };
222
+ return (
223
+ 'SON KONUŞMALAR (kullanıcıyla yakın zamanda konuşulanlar):\n' +
224
+ items.map((c) => '- [' + when(c.ts) + ' ' + (c.ch || 'sohbet') + '] ' + c.t).join('\n')
225
+ );
226
+ } catch {
227
+ return '';
228
+ }
229
+ }
230
+
231
+ /* UI için hafıza görünümü: öğrenilen etiketler + son konuşmalar */
232
+ function memSnapshot(limit = 40) {
233
+ const cm = loadChatMem();
234
+ const learned = Object.keys(cm.learned)
235
+ .sort((a, b) => (cm.learned[b].c - cm.learned[a].c) || (cm.learned[b].at - cm.learned[a].at))
236
+ .slice(0, 30)
237
+ .map((w) => ({ w, c: cm.learned[w].c || 0 }));
238
+ const n = Math.max(1, Math.min(80, Number(limit) || 40));
239
+ return { learned, chats: cm.chats.slice(-n).reverse(), total: cm.chats.length };
240
+ }
241
+
242
+ function memClear() {
243
+ try {
244
+ fs.rmSync(chatFile(), { force: true });
245
+ return { ok: true };
246
+ } catch (e) {
247
+ return { ok: false, error: String((e && e.message) || e) };
248
+ }
249
+ }
250
+
96
251
  /* ---------- normalize / dedup ---------- */
97
252
 
98
253
  function normTitle(t) {
@@ -174,9 +329,11 @@ function heuristicScores(ev, cfg) {
174
329
  const URGENT = URGENT_RE.test(t);
175
330
  let importance = ev.type === 'news' ? 40 : 65;
176
331
  if (URGENT) importance += 20;
177
- const words = String(cfg.interests || '').toLowerCase().split(/[,\s]+/).filter((w) => w.length > 2);
332
+ /* ilgi kelimeleri fold'lanır (öğrenilen etiketler zaten fold'lu) */
333
+ const words = String(cfg.interests || '').split(/[,\s]+/).map((w) => memFold(w).trim()).filter((w) => w.length > 2);
178
334
  let hits = 0;
179
- for (const w of words) if (t.includes(w)) hits++;
335
+ const hay = memFold(t);
336
+ for (const w of words) if (hay.includes(w)) hits++;
180
337
  const relevance = hits ? Math.min(100, 55 + hits * 15) : (ev.type === 'news' ? 30 : 60);
181
338
  const urgency = URGENT ? 75 : (ev.type === 'news' ? 35 : 45);
182
339
  const novelty = ev.type === 'news' ? 80 : 60;
@@ -203,7 +360,7 @@ const FILTER_SYSTEM =
203
360
  '"notify": kullanıcıya bildirmeye gerçekten değer mi (önemsiz/spam/klasik haberlerde false). ' +
204
361
  '"reason": en fazla 60 karakter kısa gerekçe.';
205
362
 
206
- function filterPrompt(events, interests) {
363
+ function filterPrompt(events, interests, chatCtx) {
207
364
  const list = events.map((e) => ({
208
365
  id: e.id,
209
366
  type: e.type,
@@ -212,6 +369,7 @@ function filterPrompt(events, interests) {
212
369
  }));
213
370
  return (
214
371
  'KULLANICI İLGİLERİ: ' + (String(interests || '').trim() || '(belirtilmemiş)') + '\n' +
372
+ (String(chatCtx || '').trim() ? String(chatCtx).trim() + '\n' : '') +
215
373
  'OLAYLAR:\n' + JSON.stringify(list) + '\n\n' +
216
374
  'Her olay için {"id","relevant","importance","urgency","novelty","notify","reason"} içeren ' +
217
375
  'TEK bir JSON dizisi döndür. Sadece JSON, başka metin yok.'
@@ -244,13 +402,15 @@ const COMPOSE_SYSTEM =
244
402
  'Alarm spam\'i yok; başlık/emoji/Markdown ekleme, yalnız düz metin yaz.';
245
403
 
246
404
  function composePrompt(ev, cfg) {
405
+ const ctx = chatContextLine(5);
247
406
  return (
248
407
  'OLAY: ' + ev.title + '\n' +
249
408
  (ev.detail ? 'DETAY: ' + ev.detail + '\n' : '') +
250
409
  'KAYNAK: ' + ev.source + '\n' +
251
410
  'ÖNCELİK: ' + ev.priority + '/100 · ' + (ev.reason || '') + '\n' +
252
- 'KULLANICI İLGİLERİ: ' + (String((cfg && cfg.interests) || '').trim() || '(belirtilmemiş)') + '\n\n' +
253
- 'Bu olay için kullanıcıya gönderilecek proaktif kısa mesajı yaz.'
411
+ 'KULLANICI İLGİLERİ: ' + (combinedInterests(cfg) || '(belirtilmemiş)') + '\n' +
412
+ (ctx ? ctx + '\n' : '') +
413
+ '\nBu olay için kullanıcıya gönderilecek proaktif kısa mesajı yaz.'
254
414
  );
255
415
  }
256
416
 
@@ -310,11 +470,14 @@ async function runCycle({ cfg, signals, llmFilter, now = new Date(), log = () =>
310
470
  for (const k of seenKeys.slice(0, seenKeys.length - SEEN_CAP)) delete st.seen[k];
311
471
  }
312
472
 
313
- /* UCUZ FİLTRE: tüm adaylar TEK toplu çağrıda (maliyet freni). Çökerse deterministik. */
473
+ /* UCUZ FİLTRE: tüm adaylar TEK toplu çağrıda (maliyet freni). Çökerse deterministik.
474
+ İlgi alanları: elle girilen + sohbetlerden öğrenilenler; son konuşmalar bağlam. */
475
+ const interests = combinedInterests(cfg);
476
+ const chatCtx = chatContextLine(8);
314
477
  let graded = null;
315
478
  if (typeof llmFilter === 'function' && pending.length) {
316
479
  try {
317
- const out = await llmFilter(filterPrompt(pending.slice(0, FILTER_MAX), cfg.interests));
480
+ const out = await llmFilter(filterPrompt(pending.slice(0, FILTER_MAX), interests, chatCtx));
318
481
  graded = parseFilterJson(out);
319
482
  if (graded.size) log('filtre: LLM ' + graded.size + ' olayı puanladı');
320
483
  } catch {
@@ -334,7 +497,7 @@ async function runCycle({ cfg, signals, llmFilter, now = new Date(), log = () =>
334
497
  urgency: Math.max(0, Math.min(100, Math.round(Number(g.urgency) || 0))),
335
498
  novelty: Math.max(0, Math.min(100, Math.round(Number(g.novelty) || 0))),
336
499
  }
337
- : heuristicScores(ev, cfg);
500
+ : heuristicScores(ev, { ...cfg, interests });
338
501
  ev.priority = compositePriority(ev.scores, cfg);
339
502
  ev.reason = g ? String(g.reason || '').slice(0, 80) : 'deterministik puan';
340
503
  if (g && g.notify === false && ev.priority < cfg.minNotifyPriority) {
@@ -438,6 +601,12 @@ module.exports = {
438
601
  markNotified,
439
602
  listEvents,
440
603
  lastRunAt,
604
+ rememberConversation,
605
+ learnInterests,
606
+ combinedInterests,
607
+ chatContextLine,
608
+ memSnapshot,
609
+ memClear,
441
610
  FILTER_SYSTEM,
442
611
  COMPOSE_SYSTEM,
443
612
  filterPrompt,
package/src/main.js CHANGED
@@ -2937,6 +2937,7 @@ function reloadBackend() {
2937
2937
  },
2938
2938
  emit: (ev) => {
2939
2939
  if (win && !win.isDestroyed()) win.webContents.send('agent:event', ev);
2940
+ try { empatiRememberFromEvent(ev); } catch {} /* empati hafızası: sohbet → kayıt + ilgi öğrenme */
2940
2941
  flushDesktopOnDone(ev); /* biriken desktop mesajlarını sıraya bas */
2941
2942
  bcFlushOnDone(ev); /* Beast Code kuyruğunu iş bitiminde boşalt */
2942
2943
  stFlushOnDone(ev); /* Beast Studio kuyruğunu iş bitiminde boşalt */
@@ -6200,6 +6201,33 @@ function empatiCfg() {
6200
6201
  return empati.mergeCfg(settings.empati || {});
6201
6202
  }
6202
6203
 
6204
+ /* EMPATİ HAFIZASI: sohbet akışını kaydet + kullanıcı ilgi alanlarını öğren.
6205
+ - user mesajı → hafızaya yaz + sık kelimelerden ilgi etiketi öğren
6206
+ - assistant cevabı → hafızaya yaz (konuşmanın iki yanı da kalsın)
6207
+ Beast Code/Studio iş mesajları, /komutlar ve bağlam enjeksiyonları kayda girmez. */
6208
+ function empatiRememberFromEvent(ev) {
6209
+ if (!ev || ev.type !== 'message' || !ev.message || !ev.message.role) return;
6210
+ const m = ev.message;
6211
+ if (m.role !== 'user' && m.role !== 'assistant') return;
6212
+ const txt = typeof m.content === 'string' ? m.content : '';
6213
+ if (!txt.trim()) return;
6214
+ if (txt.startsWith((engine && engine.OBSERVE_MARK) || '[BAĞLAM')) return; /* sessiz bağlam — sohbet değil */
6215
+ if (txt.startsWith('🫡 *Beast proaktif:') || txt.startsWith('🫡 **Beast proaktif:')) return; /* proaktif not — döngü '[proaktif]' etiketiyle zaten hafızaya yazdı */
6216
+ if (m.role === 'user' && txt.startsWith('/')) return; /* slash komut gürültüsü */
6217
+ let bc = false;
6218
+ try {
6219
+ const s = engine && engine.cache && engine.cache.get(String(ev.sessionId || ''));
6220
+ bc = !!(s && (s.bcCode || s.bcMode));
6221
+ } catch {}
6222
+ if (bc) return; /* kod işi — ilgi alanını bulanıklaştırır */
6223
+ if (m.role === 'user') {
6224
+ empati.rememberConversation(txt, 'sohbet');
6225
+ empati.learnInterests(txt);
6226
+ } else {
6227
+ empati.rememberConversation(txt, 'beast');
6228
+ }
6229
+ }
6230
+
6203
6231
  function empatiLog(line) {
6204
6232
  try { waLog('[EMPATİ] ' + line); } catch {}
6205
6233
  }
@@ -6275,6 +6303,53 @@ function empatiLlmCompose(prompt) {
6275
6303
  .finally(() => clearTimeout(kill));
6276
6304
  }
6277
6305
 
6306
+ /* PROAKTİF NOT → OTURUM BAĞLAMI: bildirim hangi kanala gittiyse AYNI metin
6307
+ o kanalın sohbet oturumuna asistan mesajı olarak da işlenir (yeni tur
6308
+ başlamaz). Böylece kullanıcı bildirime cevap verdiğinde model o mesajı
6309
+ KENDİSİNİN attığını bilir — aynı oturum, kesintisiz bağlam. */
6310
+ const PROACTIVE_MARK = '🫡 *Beast proaktif:*';
6311
+
6312
+ function empatiInjectToSession(sid, out) {
6313
+ try {
6314
+ if (!sid || engine.isBusy(sid)) return; /* tur ortasında geçmişi karıştırma — bildirim kanala zaten gitti */
6315
+ engine.injectAssistant(sid, out);
6316
+ } catch {}
6317
+ }
6318
+
6319
+ /* Oturum yoksa oluştur (processTgMessage/processDcMessage ile aynı kalıp) */
6320
+ function ensureTgSession(chatId) {
6321
+ let sid = tgChats.get(chatId);
6322
+ if (!sid) {
6323
+ const v = engine.createSession();
6324
+ sid = v.id;
6325
+ tgChats.set(chatId, sid);
6326
+ tgRememberSession(chatId, sid);
6327
+ saveTgChats();
6328
+ }
6329
+ return sid;
6330
+ }
6331
+ function ensureDcSession(channelId) {
6332
+ let sid = dcChats.get(channelId);
6333
+ if (!sid) {
6334
+ const v = engine.createSession();
6335
+ sid = v.id;
6336
+ dcChats.set(channelId, sid);
6337
+ dcRememberSession(channelId, sid);
6338
+ saveDcChats();
6339
+ }
6340
+ return sid;
6341
+ }
6342
+
6343
+ /* Masaüstü yedeği: en güncel (bg/bot-DM'siz, meşgul olmayan) sohbet — yoksa yeni */
6344
+ function empatiDesktopSid() {
6345
+ try {
6346
+ for (const v of engine.listSessions()) {
6347
+ if (!engine.isBusy(v.id)) return String(v.id);
6348
+ }
6349
+ } catch {}
6350
+ return engine.createSession().id;
6351
+ }
6352
+
6278
6353
  /* bildirim hedefi: sekmeden seçilen entegrasyon; seçilmemişse bağlı olanlar.
6279
6354
  Hiçbir entegrasyon yazılamazsa masaüstü chat UI (toast) kalır. */
6280
6355
  function empatiNotify(text, ev) {
@@ -6283,17 +6358,43 @@ function empatiNotify(text, ev) {
6283
6358
  const tryWa = () => {
6284
6359
  try {
6285
6360
  const own = waOwnerNum();
6286
- if (own && wa && wa.connected) senders.push(() => sendWaSafe(own + '@s.whatsapp.net', '🫡 *Beast proaktif:*\n' + text));
6361
+ if (own && wa && wa.connected) {
6362
+ const jid = own + '@s.whatsapp.net';
6363
+ const out = PROACTIVE_MARK + '\n' + text;
6364
+ senders.push(() =>
6365
+ Promise.resolve(sendWaSafe(jid, out))
6366
+ .then(() => empatiInjectToSession(ensureWaSession(jid), out))
6367
+ .catch(() => {})
6368
+ );
6369
+ }
6287
6370
  } catch {}
6288
6371
  };
6289
6372
  const tryTg = () => {
6290
6373
  try {
6291
- if (tg && tg.connected) for (const id of tgOwnerIds()) senders.push(() => sendTgSafe(id, '🫡 *Beast proaktif:*\n' + text));
6374
+ if (tg && tg.connected) {
6375
+ const out = PROACTIVE_MARK + '\n' + text;
6376
+ for (const id of tgOwnerIds()) {
6377
+ senders.push(() =>
6378
+ Promise.resolve(sendTgSafe(id, out))
6379
+ .then(() => empatiInjectToSession(ensureTgSession(String(id)), out))
6380
+ .catch(() => {})
6381
+ );
6382
+ }
6383
+ }
6292
6384
  } catch {}
6293
6385
  };
6294
6386
  const tryDc = () => {
6295
6387
  try {
6296
- if (dc && dc.connected) for (const id of dcOwnerIds()) senders.push(() => sendDcSafe(id, '🫡 **Beast proaktif:**\n' + text));
6388
+ if (dc && dc.connected) {
6389
+ const out = '🫡 **Beast proaktif:**\n' + text;
6390
+ for (const id of dcOwnerIds()) {
6391
+ senders.push(() =>
6392
+ Promise.resolve(sendDcSafe(id, out))
6393
+ .then(() => empatiInjectToSession(ensureDcSession(String(id)), out))
6394
+ .catch(() => {})
6395
+ );
6396
+ }
6397
+ }
6297
6398
  } catch {}
6298
6399
  };
6299
6400
  if (cfg.notifyTarget === 'whatsapp') tryWa();
@@ -6304,9 +6405,11 @@ function empatiNotify(text, ev) {
6304
6405
  for (const fn of senders) {
6305
6406
  try { fn(); sent++; } catch {}
6306
6407
  }
6307
- /* hiçbir entegrasyona yazılamadıysa yalnız masaüstü chat UI'a düş */
6408
+ /* hiçbir entegrasyona yazılamadıysa yalnız masaüstü chat UI'a düş
6409
+ metin güncel sohbete de asistan mesajı olarak işlenir */
6308
6410
  try {
6309
6411
  if (!sent && win && !win.isDestroyed()) {
6412
+ empatiInjectToSession(empatiDesktopSid(), PROACTIVE_MARK + '\n' + text);
6310
6413
  win.webContents.send('agent:event', { type: 'proactive', id: ev.id, level: ev.level, title: ev.title, text });
6311
6414
  }
6312
6415
  } catch {}
@@ -6318,13 +6421,18 @@ async function empatiCycle(manual) {
6318
6421
  if (!cfg.enabled && !manual) return { ok: false, error: 'kapalı' };
6319
6422
  empatiRuntime.running = true;
6320
6423
  try {
6321
- const r = await empati.runCycle({
6322
- cfg,
6323
- signals: { self: empatiSignalSelf, news: empatiSignalNews },
6324
- llmFilter: empatiLlmFilter,
6325
- now: new Date(),
6326
- log: empatiLog,
6327
- });
6424
+ /* zaman aşımı emniyeti: sinyal/LLM takılırsa `running` bayrağı sonsuz kilitlenmesin
6425
+ (yoksa "Şimdi Tara" sürekli 'döngü zaten çalışıyor' der) */
6426
+ const r = await Promise.race([
6427
+ empati.runCycle({
6428
+ cfg,
6429
+ signals: { self: empatiSignalSelf, news: empatiSignalNews },
6430
+ llmFilter: empatiLlmFilter,
6431
+ now: new Date(),
6432
+ log: empatiLog,
6433
+ }),
6434
+ new Promise((_, rej) => setTimeout(() => rej(new Error('döngü zaman aşımı (2 dk)')), 120000).unref()),
6435
+ ]);
6328
6436
  let notified = 0;
6329
6437
  for (const a of r.actions) {
6330
6438
  let text = '';
@@ -6332,6 +6440,7 @@ async function empatiCycle(manual) {
6332
6440
  if (!text) text = empati.composeFallback(a.event);
6333
6441
  empatiNotify(text, a.event);
6334
6442
  empati.markNotified(a.event.id, a.level, text, cfg.cooldownMin);
6443
+ try { empati.rememberConversation('[proaktif] ' + text, 'empati'); } catch {}
6335
6444
  notified++;
6336
6445
  }
6337
6446
  if (notified && win && !win.isDestroyed()) {
@@ -6374,6 +6483,8 @@ ipcMain.handle('empati:scan', async () => {
6374
6483
  try { return await empatiCycle(true); } catch (e) { return { ok: false, error: String((e && e.message) || e).slice(0, 200) }; }
6375
6484
  });
6376
6485
  ipcMain.handle('empati:events', () => empati.listEvents(80));
6486
+ ipcMain.handle('empati:memory', () => empati.memSnapshot(40));
6487
+ ipcMain.handle('empati:memclear', () => empati.memClear());
6377
6488
 
6378
6489
  ipcMain.handle('cron:list', () => cron.list());
6379
6490
  /* #23 Fallout: provider → kayıtlı API key haritası.
package/src/preload.js CHANGED
@@ -211,4 +211,6 @@ contextBridge.exposeInMainWorld('beast', {
211
211
  empatiSet: (patch) => ipcRenderer.invoke('empati:set', patch),
212
212
  empatiScan: () => ipcRenderer.invoke('empati:scan'),
213
213
  empatiEvents: () => ipcRenderer.invoke('empati:events'),
214
+ empatiMemory: () => ipcRenderer.invoke('empati:memory'),
215
+ empatiMemClear: () => ipcRenderer.invoke('empati:memclear'),
214
216
  });
@@ -148,6 +148,16 @@
148
148
  em_st_ignored: 'yok sayıldı',
149
149
  em_lv_high: 'YÜKSEK',
150
150
  em_lv_medium: 'ORTA',
151
+ em_interests_auto: 'Bunlara ek olarak sohbetlerinden öğrenilen ilgi alanları da otomatik devreye girer.',
152
+ em_scan_busy: 'Tarama hâlâ çalışıyor — birkaç saniye sonra tekrar dene.',
153
+ em_mem_h2: 'Empati Hafızası',
154
+ em_mem_sub: 'Konuşmalarınız burada birikir; sohbetlerinden ilgi alanların öğrenilir. Tarama ve bildirim mesajları bu hafızayla sana göre kişiselleşir.',
155
+ em_mem_learned: 'Sohbetlerden öğrenilen ilgi alanları',
156
+ em_mem_no_learned: 'Henüz öğrenilen ilgi yok — sohbet ettikçe burada birikir.',
157
+ em_mem_recent: 'Son konuşma kayıtları',
158
+ em_mem_no_recent: 'Henüz kayıt yok.',
159
+ em_mem_clear: 'Hafızayı Temizle',
160
+ em_mem_cleared: 'Empati hafızası temizlendi',
151
161
  tab_cron: 'Cron',
152
162
  tab_usage: 'Maliyet · Limit',
153
163
  tab_logs: 'Log',
@@ -187,7 +197,6 @@
187
197
  btn_close: 'Kapat',
188
198
  tip_win_min: 'Simge durumunda küçült',
189
199
  tip_win_max: 'Pencereyi büyüt/küçült',
190
- em_pass_masked: 'Mevcut şifre korunuyor — değiştirmek için yeniden gir',
191
200
  p_h2: 'Provider',
192
201
  p_sub: 'Beast config.yaml\u2019dan gelen modeller — aktif olanı seç',
193
202
  p_del_title: 'Modeli listeden sil',
@@ -257,11 +266,12 @@
257
266
  tts_chat_auto: 'Chat\'te ajanın yazdıklarını otomatik seslendir',
258
267
  tts_on: 'TTS açık — cevaplar sesli de gider',
259
268
  tts_off: 'TTS kapalı',
260
- em_h2: 'E-posta (Gmail)',
261
- em_sub: 'IMAP/SMTP — Gmail hesabında "Uygulama Şifresi" oluşturup buraya gir. Agent mailleri okur, özetler ve senin adına gönderir.',
262
- em_save: 'E-posta Kaydet',
263
- em_note: 'Kaydettikten sonra agent\u2019a "gelen mailleri özetle", "X\u2019e mail at" gibi komutlar verebilirsin.',
264
- em_saved: 'E-posta kaydedildi — agent artık maillere erişebilir',
269
+ mail_h2: 'E-posta (Gmail)',
270
+ mail_sub: 'IMAP/SMTP — Gmail hesabında "Uygulama Şifresi" oluşturup buraya gir. Agent mailleri okur, özetler ve senin adına gönderir.',
271
+ mail_save: 'E-posta Kaydet',
272
+ mail_note: 'Kaydettikten sonra agent\u2019a "gelen mailleri özetle", "X\u2019e mail at" gibi komutlar verebilirsin.',
273
+ mail_saved: 'E-posta kaydedildi — agent artık maillere erişebilir',
274
+ mail_pass_masked: 'Mevcut şifre korunuyor — değiştirmek için yeniden gir',
265
275
  it_h2: 'Entegrasyonlar',
266
276
  it_sub: 'Mesajlaşma köprüleri — gelen mesajlar Beast\u2019e gider, cevap geri döner',
267
277
  it_wa_sub: 'Özel mesajlar · gruplar (@mention ile) · slash komutları (/help)',
@@ -759,6 +769,16 @@
759
769
  em_st_ignored: 'ignored',
760
770
  em_lv_high: 'HIGH',
761
771
  em_lv_medium: 'MEDIUM',
772
+ em_interests_auto: 'On top of these, interest areas learned from your chats kick in automatically.',
773
+ em_scan_busy: 'A scan is still running — try again in a few seconds.',
774
+ em_mem_h2: 'Empathy Memory',
775
+ em_mem_sub: 'Your conversations accumulate here; interest areas are learned from your chats. Scans and notification texts are personalized with this memory.',
776
+ em_mem_learned: 'Interest areas learned from chats',
777
+ em_mem_no_learned: 'No learned interests yet — they accumulate as you chat.',
778
+ em_mem_recent: 'Recent conversation records',
779
+ em_mem_no_recent: 'No records yet.',
780
+ em_mem_clear: 'Clear Memory',
781
+ em_mem_cleared: 'Empathy memory cleared',
762
782
  tab_cron: 'Cron',
763
783
  tab_usage: 'Cost · Limit',
764
784
  tab_logs: 'Logs',
@@ -798,7 +818,6 @@
798
818
  btn_close: 'Close',
799
819
  tip_win_min: 'Minimize',
800
820
  tip_win_max: 'Maximize/Restore',
801
- em_pass_masked: 'Current password kept — type a new one to change it',
802
821
  p_h2: 'Provider',
803
822
  p_sub: 'Models from Beast config.yaml — pick the active one',
804
823
  p_del_title: 'Remove model from list',
@@ -868,11 +887,12 @@
868
887
  tts_chat_auto: 'Auto-speak agent replies in chat',
869
888
  tts_on: 'TTS on — replies also go as voice',
870
889
  tts_off: 'TTS off',
871
- em_h2: 'E-mail (Gmail)',
872
- em_sub: 'IMAP/SMTP — create an "App Password" in your Gmail account and enter it here. The agent reads, summarizes and sends mail on your behalf.',
873
- em_save: 'Save E-mail',
874
- em_note: 'After saving you can give the agent commands like "summarize incoming mail", "send mail to X".',
875
- em_saved: 'E-mail saved — the agent can now access mail',
890
+ mail_h2: 'E-mail (Gmail)',
891
+ mail_sub: 'IMAP/SMTP — create an "App Password" in your Gmail account and enter it here. The agent reads, summarizes and sends mail on your behalf.',
892
+ mail_save: 'Save E-mail',
893
+ mail_note: 'After saving you can give the agent commands like "summarize incoming mail", "send mail to X".',
894
+ mail_saved: 'E-mail saved — the agent can now access mail',
895
+ mail_pass_masked: 'Current password kept — type a new one to change it',
876
896
  it_h2: 'Integrations',
877
897
  it_sub: 'Messaging bridges — incoming messages go to Beast, replies come back',
878
898
  it_wa_sub: 'Private messages · groups (@mention) · slash commands (/help)',
@@ -2333,47 +2333,49 @@ async function renderEmailPane() {
2333
2333
  const PASS_MASK = '••••••••';
2334
2334
  let savedPass = '';
2335
2335
  let passChanged = false;
2336
+ /* ID'ler mail* önekiyle: empati sekmesindeki em* ID'leriyle ÇAKIŞMASIN —
2337
+ aynı ID iki panelde olursa $() ilkini bulur, empati butonları ölü kalırdı */
2336
2338
  pane.innerHTML =
2337
- '<h2>' + _t('em_h2') + '</h2><div class="sub">' + _t('em_sub') + '</div>' +
2339
+ '<h2>' + _t('mail_h2') + '</h2><div class="sub">' + _t('mail_sub') + '</div>' +
2338
2340
  `<div class="form-grid" style="grid-template-columns:1fr 1fr;align-items:center;margin-top:10px">
2339
- <input id="emHost" class="inp" placeholder="imap.gmail.com" autocomplete="off" />
2340
- <input id="emUser" class="inp" placeholder="adres@gmail.com" autocomplete="off" />
2341
- <input id="emPass" class="inp" type="password" placeholder="Uygulama Şifresi" autocomplete="off" />
2342
- <input id="emSmtpHost" class="inp" placeholder="smtp.gmail.com" autocomplete="off" />
2343
- <input id="emSmtpPort" class="inp" placeholder="465" autocomplete="off" />
2344
- <button id="emSave" class="btn ghost">${_t('em_save')}</button>
2341
+ <input id="mailHost" class="inp" placeholder="imap.gmail.com" autocomplete="off" />
2342
+ <input id="mailUser" class="inp" placeholder="adres@gmail.com" autocomplete="off" />
2343
+ <input id="mailPass" class="inp" type="password" placeholder="Uygulama Şifresi" autocomplete="off" />
2344
+ <input id="mailSmtpHost" class="inp" placeholder="smtp.gmail.com" autocomplete="off" />
2345
+ <input id="mailSmtpPort" class="inp" placeholder="465" autocomplete="off" />
2346
+ <button id="mailSave" class="btn ghost">${_t('mail_save')}</button>
2345
2347
  </div>` +
2346
- '<div class="sub" style="margin-top:8px">' + _t('em_note') + '</div>';
2348
+ '<div class="sub" style="margin-top:8px">' + _t('mail_note') + '</div>';
2347
2349
 
2348
2350
  try {
2349
2351
  const em = await beast.getEmail();
2350
- $('#emHost').value = em.host || 'imap.gmail.com';
2351
- $('#emUser').value = em.user || '';
2352
- $('#emSmtpHost').value = em.smtpHost || 'smtp.gmail.com';
2353
- $('#emSmtpPort').value = em.smtpPort || 465;
2352
+ $('#mailHost').value = em.host || 'imap.gmail.com';
2353
+ $('#mailUser').value = em.user || '';
2354
+ $('#mailSmtpHost').value = em.smtpHost || 'smtp.gmail.com';
2355
+ $('#mailSmtpPort').value = em.smtpPort || 465;
2354
2356
  savedPass = em.pass || '';
2355
2357
  } catch {}
2356
- const emPass = $('#emPass');
2358
+ const mailPass = $('#mailPass');
2357
2359
  if (savedPass) {
2358
2360
  /* kayıtlı şifre maskelenir; sadece kullanıcı değiştirirse yenisi gönderilir */
2359
- emPass.value = PASS_MASK;
2360
- emPass.placeholder = window.I18N ? window.I18N.t('em_pass_masked') : 'Mevcut şifre korunuyor — değiştirmek için yeniden gir';
2361
+ mailPass.value = PASS_MASK;
2362
+ mailPass.placeholder = window.I18N ? window.I18N.t('mail_pass_masked') : 'Mevcut şifre korunuyor — değiştirmek için yeniden gir';
2361
2363
  }
2362
- emPass.addEventListener('focus', () => { if (emPass.value === PASS_MASK) emPass.value = ''; });
2363
- emPass.addEventListener('input', () => { passChanged = true; });
2364
- $('#emSave').addEventListener('click', async () => {
2365
- const entered = emPass.value;
2364
+ mailPass.addEventListener('focus', () => { if (mailPass.value === PASS_MASK) mailPass.value = ''; });
2365
+ mailPass.addEventListener('input', () => { passChanged = true; });
2366
+ $('#mailSave').addEventListener('click', async () => {
2367
+ const entered = mailPass.value;
2366
2368
  const pass = passChanged ? entered : savedPass;
2367
2369
  await beast.setEmail({
2368
- host: $('#emHost').value.trim() || 'imap.gmail.com',
2370
+ host: $('#mailHost').value.trim() || 'imap.gmail.com',
2369
2371
  port: 993,
2370
- user: $('#emUser').value.trim(),
2372
+ user: $('#mailUser').value.trim(),
2371
2373
  pass,
2372
- smtpHost: $('#emSmtpHost').value.trim() || 'smtp.gmail.com',
2373
- smtpPort: Number($('#emSmtpPort').value) || 465,
2374
+ smtpHost: $('#mailSmtpHost').value.trim() || 'smtp.gmail.com',
2375
+ smtpPort: Number($('#mailSmtpPort').value) || 465,
2374
2376
  });
2375
- if (pass && pass !== PASS_MASK) { savedPass = pass; emPass.value = PASS_MASK; passChanged = false; }
2376
- toast(_t('em_saved'));
2377
+ if (pass && pass !== PASS_MASK) { savedPass = pass; mailPass.value = PASS_MASK; passChanged = false; }
2378
+ toast(_t('mail_saved'));
2377
2379
  });
2378
2380
  }
2379
2381
 
@@ -3028,9 +3030,14 @@ async function renderEventsPane() {
3028
3030
  async function renderEmpatiPane() {
3029
3031
  const pane = $('#tab-empati');
3030
3032
  if (!pane) return;
3033
+ /* pane-scope: e-posta sekmesinde eskiden aynı ID'ler vardı; $() global arar
3034
+ ve YANLIŞ butona bağlardı — tüm aramalar bu panelin içinde kalır */
3035
+ const q = (sel) => pane.querySelector(sel);
3031
3036
  const cfg = await beast.empatiGet().catch(() => null);
3032
3037
  let events = [];
3033
3038
  try { events = await beast.empatiEvents(); } catch {}
3039
+ let mem = null;
3040
+ try { mem = await beast.empatiMemory(); } catch {}
3034
3041
  let models = [];
3035
3042
  try { models = (await beast.getState()).models || []; } catch {}
3036
3043
  if (!cfg) {
@@ -3101,6 +3108,7 @@ async function renderEmpatiPane() {
3101
3108
  <div style="margin-top:8px">
3102
3109
  <label class="sub">${_t('em_interests')}</label>
3103
3110
  <textarea id="emInterests" class="inp" rows="2" placeholder="${_t('em_interests_ph')}">${escapeHtml(cfg.interests || '')}</textarea>
3111
+ <div class="sub">${_t('em_interests_auto')}</div>
3104
3112
  </div>
3105
3113
  <label class="lock-row" style="margin-top:8px"><input type="checkbox" id="emNews" ${cfg.newsTopics ? 'checked' : ''}/><span>${_t('em_news')}</span></label>
3106
3114
  <input id="emTopics" class="inp" style="${cfg.newsTopics ? '' : 'opacity:.5'}" placeholder="${_t('em_news_topics_ph')}" value="${escapeHtml(cfg.newsTopics || '')}" spellcheck="false"/>
@@ -3111,46 +3119,85 @@ async function renderEmpatiPane() {
3111
3119
  </div>
3112
3120
  <div class="sub" style="margin-top:6px">${_t('em_note')}</div>
3113
3121
  </div>
3122
+ <h3 style="margin-top:16px;color:var(--muted)">${_t('em_mem_h2')}</h3>
3123
+ <div class="sub">${_t('em_mem_sub')}</div>
3124
+ <div style="margin-top:6px"><label class="sub">${_t('em_mem_learned')}</label>${(() => {
3125
+ const learned = (mem && Array.isArray(mem.learned)) ? mem.learned : [];
3126
+ if (!learned.length) return '<div class="sub" style="margin-top:2px">' + _t('em_mem_no_learned') + '</div>';
3127
+ return '<div style="display:flex;flex-wrap:wrap;gap:4px;margin-top:4px">' +
3128
+ learned.map((x) =>
3129
+ '<span style="font-size:11px;padding:2px 8px;border:1px solid var(--border);border-radius:10px">' +
3130
+ escapeHtml(x.w) + ' <span style="color:var(--muted)">×' + (x.c || 0) + '</span></span>'
3131
+ ).join('') + '</div>';
3132
+ })()}</div>
3133
+ <div style="margin-top:8px"><label class="sub">${_t('em_mem_recent')}</label>${(() => {
3134
+ const chats = (mem && Array.isArray(mem.chats)) ? mem.chats : [];
3135
+ if (!chats.length) return '<div class="sub" style="margin-top:2px">' + _t('em_mem_no_recent') + '</div>';
3136
+ return '<div style="margin-top:4px">' + chats.map((c) =>
3137
+ '<div class="usage-row" style="align-items:flex-start">' +
3138
+ '<div style="min-width:0;flex:1">' +
3139
+ '<div style="font-size:12px">' + escapeHtml(c.t || '') + '</div>' +
3140
+ '<div class="ur-meta">' + escapeHtml(c.ch || '') + ' · ' + when(c.ts) + '</div>' +
3141
+ '</div></div>').join('') + '</div>';
3142
+ })()}
3143
+ <button id="emMemClear" class="btn ghost" style="margin-top:8px;font-size:12px">${_t('em_mem_clear')}</button>
3144
+ </div>
3114
3145
  <h3 style="margin-top:16px;color:var(--muted)">${_t('em_events')}</h3>`;
3115
3146
  const wrap = document.createElement('div');
3116
3147
  if (!rows) wrap.innerHTML = '<p class="sub">' + _t('em_no_events') + '</p>';
3117
3148
  else wrap.innerHTML = rows;
3118
3149
  pane.appendChild(wrap);
3119
3150
 
3120
- const emOn = $('#emOn');
3121
- const emDetail = $('#emDetail');
3122
- const emNews = $('#emNews');
3123
- const emTopics = $('#emTopics');
3124
- emOn.addEventListener('change', () => { emDetail.style.opacity = emOn.checked ? '' : '.5'; });
3125
- emNews.addEventListener('change', () => { emTopics.style.opacity = emNews.checked ? '' : '.5'; });
3126
- $('#emSave').addEventListener('click', async () => {
3151
+ const emOn = q('#emOn');
3152
+ const emDetail = q('#emDetail');
3153
+ const emNews = q('#emNews');
3154
+ const emTopics = q('#emTopics');
3155
+ if (emOn && emDetail) emOn.addEventListener('change', () => { emDetail.style.opacity = emOn.checked ? '' : '.5'; });
3156
+ if (emNews && emTopics) emNews.addEventListener('change', () => { emTopics.style.opacity = emNews.checked ? '' : '.5'; });
3157
+ const emSaveBtn = q('#emSave');
3158
+ if (emSaveBtn) emSaveBtn.addEventListener('click', async () => {
3127
3159
  const patch = {
3128
3160
  enabled: emOn.checked,
3129
- intervalMin: Number($('#emInterval').value) || cfg.intervalMin,
3130
- minNotifyPriority: Number($('#emMin').value),
3131
- cooldownMin: Number($('#emCd').value),
3132
- notifyTarget: $('#emTarget') ? $('#emTarget').value : '',
3133
- filterModel: $('#emModel') ? $('#emModel').value : '',
3134
- interests: $('#emInterests').value.trim(),
3161
+ intervalMin: Number(q('#emInterval').value) || cfg.intervalMin,
3162
+ minNotifyPriority: Number(q('#emMin').value),
3163
+ cooldownMin: Number(q('#emCd').value),
3164
+ notifyTarget: q('#emTarget') ? q('#emTarget').value : '',
3165
+ filterModel: q('#emModel') ? q('#emModel').value : '',
3166
+ interests: q('#emInterests').value.trim(),
3135
3167
  newsTopics: emNews.checked ? emTopics.value.trim() : '',
3136
3168
  };
3137
3169
  const r = await beast.empatiSet(patch).catch(() => null);
3138
3170
  toast(r ? _t('em_saved') : 'Hata');
3139
3171
  if (r) renderEmpatiPane();
3140
3172
  });
3141
- $('#emScan').addEventListener('click', async () => {
3142
- const msg = $('#emMsg');
3143
- msg.textContent = '';
3173
+ const emScanBtn = q('#emScan');
3174
+ if (emScanBtn) emScanBtn.addEventListener('click', async () => {
3175
+ emScanBtn.disabled = true; /* çift tık → 'döngü zaten çalışıyor' kilitlenmesin */
3176
+ const msg = q('#emMsg');
3177
+ if (msg) msg.textContent = '⏳';
3144
3178
  const r = await beast.empatiScan().catch(() => null);
3145
3179
  if (r && r.ok) {
3146
- msg.textContent = _t('em_scan_ok')
3180
+ const txt = _t('em_scan_ok')
3147
3181
  .replace('{raw}', String(r.raw || 0))
3148
3182
  .replace('{queued}', String(r.queued || 0))
3149
3183
  .replace('{stored}', String(r.stored || 0));
3150
- renderEmpatiPane();
3184
+ toast(txt);
3185
+ await renderEmpatiPane(); /* olay + hafıza listeleri tazelensin */
3186
+ const m2 = q('#emMsg');
3187
+ if (m2) m2.textContent = txt; /* yeniden çizim mesajı silmesin */
3151
3188
  } else {
3152
- msg.textContent = (r && r.error) || 'hata';
3189
+ const err = (r && r.error) || 'hata';
3190
+ if (msg) msg.textContent = /zaten çalışıyor/.test(err) ? _t('em_scan_busy') : err;
3191
+ toast('Tarama hatası: ' + err);
3153
3192
  }
3193
+ const b2 = q('#emScan');
3194
+ if (b2) b2.disabled = false;
3195
+ });
3196
+ const emMemClearBtn = q('#emMemClear');
3197
+ if (emMemClearBtn) emMemClearBtn.addEventListener('click', async () => {
3198
+ const r = await beast.empatiMemClear().catch(() => null);
3199
+ toast(r && r.ok ? _t('em_mem_cleared') : 'Hata');
3200
+ if (r && r.ok) renderEmpatiPane();
3154
3201
  });
3155
3202
  }
3156
3203