beast-agent 2.46.0 → 2.47.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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "beast-agent",
3
3
  "productName": "Beast Agent",
4
- "version": "2.46.0",
4
+ "version": "2.47.0",
5
5
  "description": "Ultra-fast local agent shell for Windows.",
6
6
  "author": "algokodcom (AlgoKod)",
7
7
  "license": "MIT",
@@ -0,0 +1,78 @@
1
+ 'use strict';
2
+
3
+ /* Kanal (WhatsApp/Telegram/Discord) kişi-başına oturum yardımcıları.
4
+ Eski "kanal tek oturumu" düzeninden güvenli göç + oturum anahtarı
5
+ normalizasyonu. Saf fonksiyonlar — test edilebilir.
6
+
7
+ ESKİ DÜZEN: tüm jid/chatId'ler AYNI oturuma bağlıydı → A'nın konuşması
8
+ B'ye bağlam olarak sızıyordu. YENİ DÜZEN: her sohbet kendi oturumunda.
9
+ Göç kuralı: paylaşılan oturum SAHİPTE kalır (sahip jid'i bulunamazsa ilk
10
+ jid'de); diğerleri ayrılır ve o ortak oturuma erişemez — geçmiş
11
+ kayıtlarından da düşürülür ki /open ile açamasınlar. */
12
+
13
+ /* jid içindeki rakamları çıkarır: "905xx:12@s.whatsapp.net" → "905xx" */
14
+ function jidDigits(jid) {
15
+ return String(jid || '')
16
+ .split('@')[0]
17
+ .split(':')[0]
18
+ .replace(/\D/g, '');
19
+ }
20
+
21
+ /* DM oturum anahtarı: LID/cihaz eki yerine GERÇEK numara esas alınır —
22
+ aynı kişi iki ayrı oturum açmasın. pn verilmezse jid'in kendisi döner.
23
+ Gruplar kendi jid'iyle ayrılır. */
24
+ function dmSessionKey(jid, pn) {
25
+ const j = String(jid || '');
26
+ if (!j) return '';
27
+ if (j.endsWith('@g.us')) return j;
28
+ const p = jidDigits(pn);
29
+ if (p) return p + '@s.whatsapp.net';
30
+ const base = j.split('@')[0].split(':')[0];
31
+ if (/^\d+$/.test(base) && j.endsWith('@s.whatsapp.net')) return base + '@s.whatsapp.net';
32
+ return j;
33
+ }
34
+
35
+ /* Paylaşılan (birden çok sohbete bağlı) oturumları ayırır.
36
+ chats: Map<jid, sid>, history: Map<jid, sid[]> (yerinde değiştirilir).
37
+ döner: true → değişiklik oldu (diske yazılmalı). */
38
+ function migrateSharedSessions({ chats, history, ownerDigits, historyCap = 20 }) {
39
+ const bySid = new Map();
40
+ for (const [j, s] of chats) {
41
+ const sid = String(s);
42
+ if (!bySid.has(sid)) bySid.set(sid, []);
43
+ bySid.get(sid).push(String(j));
44
+ }
45
+ const own = String(ownerDigits || '').replace(/\D/g, '');
46
+ let dirty = false;
47
+ for (const [sid, jids] of bySid) {
48
+ if (jids.length < 2) continue;
49
+ /* sahip jid'i ortak oturumda kalır; sahip yoksa kanonik numara jid'i
50
+ (@s.whatsapp.net), o da yoksa ilk jid (mevcut bağlam korunur) */
51
+ const keep =
52
+ (own && jids.find((j) => jidDigits(j).endsWith(own))) ||
53
+ jids.find((j) => j.endsWith('@s.whatsapp.net')) ||
54
+ jids[0];
55
+ for (const j of jids) {
56
+ if (j === keep) continue;
57
+ chats.delete(j);
58
+ const h = (history.get(j) || []).filter((x) => String(x) !== sid);
59
+ if (h.length) history.set(j, h.slice(-historyCap));
60
+ else history.delete(j);
61
+ dirty = true;
62
+ }
63
+ }
64
+ return dirty;
65
+ }
66
+
67
+ /* Birden çok kanal oturumunun sid'lerini tek Set'te toplar: otomatik işler
68
+ (cron/izleyici/fallout) bu oturumlara ASLA enjekte edilmez. */
69
+ function collectSessionIds(...maps) {
70
+ const set = new Set();
71
+ for (const m of maps) {
72
+ if (!m) continue;
73
+ for (const s of m.values()) if (s) set.add(String(s));
74
+ }
75
+ return set;
76
+ }
77
+
78
+ module.exports = { jidDigits, dmSessionKey, migrateSharedSessions, collectSessionIds };
@@ -1522,9 +1522,13 @@ class Engine {
1522
1522
  }
1523
1523
 
1524
1524
  /* "Nerede kaldım?" (#5): en son aktif oturumun özeti + yarım kalan todolar.
1525
- Saf toplama — gönderim/otomatik-devam main'e aittir. */ lastWhereWasI() {
1525
+ Saf toplama — gönderim/otomatik-devam main'e aittir. exclude: bu id'ler
1526
+ (kanal kişi sohbetleri) atlanır — özet yanlış kişiye gitmez. */
1527
+ lastWhereWasI(exclude) {
1528
+ const skip = exclude instanceof Set ? exclude : null;
1526
1529
  let latest = null;
1527
1530
  for (const v of this.listSessions()) {
1531
+ if (skip && skip.has(String(v.id))) continue;
1528
1532
  if (!latest || String(v.updatedAt) > String(latest.updatedAt)) latest = v;
1529
1533
  }
1530
1534
  if (!latest) return null;
package/src/main.js CHANGED
@@ -30,6 +30,7 @@ const piper = require('./agent/piper');
30
30
  const fsguard = require('./agent/fsguard');
31
31
  const log = require('./agent/logger');
32
32
  const squeeze = require('./agent/squeeze');
33
+ const chansessions = require('./agent/channelsessions');
33
34
  const QRCode = require('qrcode'); /* Expo Go QR (bc-expurl) — whatsapp ile aynı paket */
34
35
 
35
36
  /* Renderer'a sır gönderirken kullanılan maske; kaydederken aynen geri gelirse
@@ -124,28 +125,20 @@ function notifyVersionUpdate() {
124
125
  win.webContents.send('agent:event', { sessionId: sid, type: 'done', usage: null });
125
126
  }
126
127
  } catch {}
127
- /* kanallar: SADECE bu oturuma bağlı kanal (kullanıcı oradaydı) */
128
+ /* kanallar: SAHİBE tek satır (kişi sohbetleri kanal oturumudur —
129
+ sürüm duyurusu müşteri/kişi sohbetlerine KARIŞMAZ) */
128
130
  (async () => {
129
131
  try {
130
132
  if (wa && wa.connected) {
131
133
  const own = waOwnerNum();
132
- const jid = own ? own + '@s.whatsapp.net' : '';
133
- if (jid && [...waChats.entries()].some(([j, s]) => j === jid && String(s) === String(sid))) {
134
- await sendWaSafe(jid, waStyleTxt);
135
- }
134
+ if (own) await sendWaSafe(own + '@s.whatsapp.net', waStyleTxt);
136
135
  }
137
136
  } catch {}
138
137
  try {
139
- if (tg && tg.connected) {
140
- const hit = [...tgChats.entries()].find(([, s]) => String(s) === String(sid));
141
- if (hit) await sendTgSafe(hit[0], waStyleTxt);
142
- }
138
+ if (tg && tg.connected) for (const id of tgOwnerIds()) await sendTgSafe(id, waStyleTxt);
143
139
  } catch {}
144
140
  try {
145
- if (dc && dc.connected) {
146
- const hit = [...dcChats.entries()].find(([, s]) => String(s) === String(sid));
147
- if (hit) await sendDcSafe(hit[0], waStyleTxt);
148
- }
141
+ if (dc && dc.connected) for (const id of dcOwnerIds()) await sendDcSafe(id, waStyleTxt);
149
142
  } catch {}
150
143
  })();
151
144
  }
@@ -325,24 +318,26 @@ startHealthServer(); /* splash/boot aşamasından itibaren /health ayakta */
325
318
  try { setSearchChain(settings.searchChain); } catch {}
326
319
  try { setTinyfishKey(settings.tinyfishKey || null); } catch {}
327
320
  let wa = null;
328
- let waChats = new Map(); // jid -> aktif session id (hepsi KANAL TEK OTURUMUNA bağlı)
329
- let waSingleSid = null; // KANAL TEK OTURUMU: WhatsApp için BİR oturum — silinmedikçe hep aynı
321
+ let waChats = new Map(); // jid (numara/grup) -> KENDİ oturumu: her sohbet AYRI (gizlilik)
322
+ let waCronSid = null; // WhatsApp CRON TABAN OTURUMU: otomatik işler; hiçbir kişiye bağlı DEĞİL
330
323
  let waHistory = new Map(); // jid -> [sid,...] bu sohbete ait tüm oturumlar
331
324
  let waBcMode = new Set(); // jid -> BeastCode modu AKTİF (WhatsApp'tan uzaktan kodlama)
332
325
  let waJidPn = new Map(); // jid -> gerçek telefon numarası (LID fallback için)
333
- let waLastActiveJid = ''; // en son mesaj gelen WA sohbeti — kanal tek oturumunda cevap/dosya hedefi
326
+ let waLastActiveJid = ''; // en son mesaj gelen WA sohbeti — cevap/dosya hedefi önceliği
334
327
  let desktopActiveSid = ''; // masaüstü UI'da AÇIK olan oturum — proaktif not buraya işlenir
335
328
  const WA_HISTORY_CAP = 20;
336
329
  let tg = null;
337
- let tgChats = new Map(); // telegram chatId -> aktif session id (hepsi KANAL TEK OTURUMUNA bağlı)
338
- let tgSingleSid = null; // KANAL TEK OTURUMU: Telegram için BİR oturum
330
+ let tgChats = new Map(); // telegram chatId -> KENDİ oturumu: her sohbet AYRI (gizlilik)
331
+ let tgCronSid = null; // Telegram CRON TABAN OTURUMU: otomatik işler; hiçbir kişiye bağlı DEĞİL
339
332
  let tgHistory = new Map(); // chatId -> [sid,...]
340
333
  const TG_HISTORY_CAP = 20;
334
+ let tgLastActiveChatId = ''; // en son mesaj gelen TG sohbeti — cron yansıtma hedefi
341
335
  let dc = null;
342
- let dcChats = new Map(); // discord channelId -> aktif session id (hepsi KANAL TEK OTURUMUNA bağlı)
343
- let dcSingleSid = null; // KANAL TEK OTURUMU: Discord için BİR oturum
336
+ let dcChats = new Map(); // discord channelId -> KENDİ oturumu: her kanal AYRI (gizlilik)
337
+ let dcCronSid = null; // Discord CRON TABAN OTURUMU: otomatik işler; hiçbir kişiye bağlı DEĞİL
344
338
  let dcHistory = new Map(); // channelId -> [sid,...]
345
339
  const DC_HISTORY_CAP = 20;
340
+ let dcLastActiveChannelId = ''; // en son mesaj gelen DC kanalı — cron yansıtma hedefi
346
341
  const DC_CHATS_FILE = path.join(APP_DIR, 'dc-chats.json');
347
342
  let tray = null;
348
343
  app.isQuitting = false;
@@ -379,14 +374,16 @@ try {
379
374
  if (!h.includes(s)) h.push(s);
380
375
  waHistory.set(j, h.slice(-WA_HISTORY_CAP));
381
376
  }
382
- /* kanal tek oturumu: kayıtlı aktif id (geçersizse ensure yeniden seçer) */
383
- if (typeof raw.active === 'string' && raw.active) waSingleSid = raw.active;
377
+ /* CRON TABAN OTURUMU: kişi sohbetlerinden tamamen bağımsız oturum */
378
+ if (typeof raw.cron === 'string' && raw.cron) waCronSid = raw.cron;
384
379
  /* BeastCode modu: hangi sohbetler uzaktan kodlama yapıyor */
385
380
  if (Array.isArray(raw.bcMode)) {
386
381
  for (const j of raw.bcMode) if (typeof j === 'string') waBcMode.add(j);
387
382
  }
388
383
  }
389
384
  } catch {}
385
+ /* GÖÇ: eski "tek oturum" düzeni → kişi başına oturum (sahip bağlamı korunur) */
386
+ try { waMigrateSharedSessions(); } catch {}
390
387
 
391
388
  function saveWaChats() {
392
389
  try {
@@ -396,7 +393,7 @@ function saveWaChats() {
396
393
  chats: Object.fromEntries(waChats),
397
394
  history: Object.fromEntries([...waHistory.entries()].map(([j, a]) => [j, a.slice(-WA_HISTORY_CAP)])),
398
395
  bcMode: [...waBcMode],
399
- active: waSingleSid || '',
396
+ cron: waCronSid || '',
400
397
  })
401
398
  );
402
399
  } catch {}
@@ -409,6 +406,23 @@ function waRememberSession(jid, sid) {
409
406
  waHistory.set(jid, h.slice(-WA_HISTORY_CAP));
410
407
  }
411
408
 
409
+ /* Oturum anahtarı: DM'lerde GERÇEK numara esas alınır — LID/cihaz eki aynı
410
+ kişiyi iki oturuma bölmesin; gruplar kendi jid'iyle ayrı kalır. */
411
+ function waSessionKey(jid) {
412
+ return chansessions.dmSessionKey(jid, waJidPn.get(String(jid || '')));
413
+ }
414
+
415
+ /* GÖÇ (tek oturum → kişi başına oturum): ortak oturum SAHİPTE kalır;
416
+ diğer sohbetler ayrılır, o oturuma erişemez (geçmişten de düşürülür). */
417
+ function waMigrateSharedSessions() {
418
+ return chansessions.migrateSharedSessions({
419
+ chats: waChats,
420
+ history: waHistory,
421
+ ownerDigits: waOwnerNum(),
422
+ historyCap: WA_HISTORY_CAP,
423
+ });
424
+ }
425
+
412
426
  /* ---------- BEASTCODE MODU (WA'dan uzaktan kodlama) ----------
413
427
  /beastcode → sohbet BEAST CODE oturumuna döner: masaüstü IDE paneliyle
414
428
  AYNI motor (engine bcCode + buildBcSystem) — todo planı, edit_file/run_command
@@ -428,15 +442,37 @@ function waBcWorkspace() {
428
442
  canlı akar; panelde yazılan da WhatsApp'a düşer */
429
443
  function waBcSession(jid) {
430
444
  const s = bcGetSession(waBcWorkspace());
431
- if (waChats.get(jid) !== s.id) {
432
- waChats.set(jid, s.id);
433
- waRememberSession(jid, s.id);
445
+ const key = waSessionKey(jid);
446
+ if (waChats.get(key) !== s.id) {
447
+ waChats.set(key, s.id);
448
+ waRememberSession(key, s.id);
434
449
  saveWaChats();
435
450
  if (wa) wa.setWatchJids([...waChats.keys()]);
436
451
  }
437
452
  return s;
438
453
  }
439
454
 
455
+ /* /beastagent: kişinin KENDİ sohbet oturumuna dön (Beast Code oturumundan çık).
456
+ Geçmişteki en güncel yaşayan sohbet oturumu kullanılır; yoksa taze açılır. */
457
+ function waRestoreChatSession(jid) {
458
+ const key = waSessionKey(jid);
459
+ const bcSid = waChats.get(key);
460
+ const hist = [...(waHistory.get(key) || [])].reverse();
461
+ for (const h of hist) {
462
+ if (!h || h === bcSid || !sessionFileAlive(h)) continue;
463
+ try {
464
+ const s = engine.cache.get(h) || engine._load(h);
465
+ if (s && s.bcCode) continue; /* Beast Code oturumlarına dönülmez */
466
+ waChats.set(key, h);
467
+ saveWaChats();
468
+ if (wa) wa.setWatchJids([...waChats.keys()]);
469
+ return h;
470
+ } catch {}
471
+ }
472
+ waChats.delete(key);
473
+ return ensureWaSession(jid);
474
+ }
475
+
440
476
  function settingsLog(line) {
441
477
  try {
442
478
  fs.appendFileSync(
@@ -1416,49 +1452,53 @@ async function tryWaSlash(jid, rawText, senderNum, payload0) {
1416
1452
  if (cmd === 'help') {
1417
1453
  out = waSlashHelp();
1418
1454
  } else if (cmd === 'new') {
1419
- /* /new: kanalın TEK oturumunu kapat, yenisini aç — aynı anda iki
1420
- WhatsApp sohbet oturumu olmaz; eski oturum silinir */
1421
- const oldSid = waSingleSid || waChats.get(jid) || '';
1455
+ /* /new: YALNIZ bu sohbetin oturumu kapatılır, bu sohbete taze oturum
1456
+ açılır — diğer numaraların/grupların oturumlarına DOKUNULMAZ */
1457
+ const key = waSessionKey(jid);
1458
+ const oldSid = waChats.get(key) || '';
1422
1459
  if (oldSid) {
1423
1460
  let olds = null;
1424
1461
  try { olds = engine.cache.get(oldSid) || null; } catch {}
1425
1462
  if (!olds || !olds.bcCode) {
1426
1463
  /* Beast Code panel oturumuysa dokunma — yalnız pointer taşınır */
1427
1464
  try { engine.deleteSession(oldSid); } catch {}
1428
- for (const [j, h] of waHistory.entries()) waHistory.set(j, h.filter((x) => x !== oldSid));
1465
+ const h = (waHistory.get(key) || []).filter((x) => x !== oldSid);
1466
+ if (h.length) waHistory.set(key, h);
1467
+ else waHistory.delete(key);
1429
1468
  }
1430
1469
  }
1431
1470
  const v = engine.createSession();
1432
- waSingleSid = v.id;
1433
- for (const j of [...waChats.keys()]) waChats.set(j, v.id);
1434
- waChats.set(jid, v.id);
1435
- waRememberSession(jid, v.id);
1471
+ waChats.set(key, v.id);
1472
+ waRememberSession(key, v.id);
1436
1473
  saveWaChats();
1437
1474
  wa.setWatchJids([...waChats.keys()]);
1438
- out = `*Yeni oturum* \`${v.code}\` açıldı — eski oturum kapatıldı, artık buradan devam.\nVar olan başka oturuma geçiş: \`/open <kod>\``;
1475
+ out = `*Yeni oturum* \`${v.code}\` açıldı — bu sohbet artık buradan devam eder.\nGeçmiş: \`/sessions\` · Var olan oturuma geçiş: \`/open <kod>\``;
1439
1476
  } else if (cmd === 'open') {
1440
1477
  if (!arg) {
1441
1478
  out = 'Kullanım: `/open <kod>` — kodları görmek için /sessions';
1442
1479
  } else {
1443
1480
  const hit = engine.findByCode(arg);
1444
- if (!hit) {
1445
- out = `\`${arg.toUpperCase()}\` kodlu oturum bulunamadı. Listeyi görmek için /sessions`;
1481
+ const key = waSessionKey(jid);
1482
+ /* GİZLİLİK: yalnız BU sohbetin kendi geçmişindeki oturuma geçilebilir */
1483
+ const own = new Set([...(waHistory.get(key) || []), waChats.get(key) || '']);
1484
+ if (!hit || !own.has(hit.id)) {
1485
+ out = `\`${arg.toUpperCase()}\` bu sohbete ait bir oturum değil. Listeyi görmek için /sessions`;
1446
1486
  } else {
1447
- /* /open: kanal TEK oturumunu bu oturuma çevirir — tüm jid'ler bağlanır */
1448
- waSingleSid = hit.id;
1449
- for (const j of [...waChats.keys()]) waChats.set(j, hit.id);
1450
- waRememberSession(jid, hit.id);
1487
+ waChats.set(key, hit.id);
1488
+ waRememberSession(key, hit.id);
1451
1489
  saveWaChats();
1452
1490
  wa.setWatchJids([...waChats.keys()]);
1453
1491
  out = `*Geçildi:* \`${hit.code}\` — ${escapeWa(hit.title)}`;
1454
1492
  }
1455
1493
  }
1456
1494
  } else if (cmd === 'sessions') {
1457
- const h = [...new Set([...(waHistory.get(jid) || []), ...(waChats.has(jid) ? [waChats.get(jid)] : [])])];
1458
- const activeSid = waChats.get(jid);
1495
+ const key = waSessionKey(jid);
1496
+ const h = [...new Set([...(waHistory.get(key) || []), ...(waChats.has(key) ? [waChats.get(key)] : [])])];
1497
+ const activeSid = waChats.get(key);
1459
1498
  const rows = [];
1460
1499
  for (const sid of h.slice(-8).reverse()) {
1461
1500
  try {
1501
+ if (!sessionFileAlive(sid)) continue;
1462
1502
  const s = engine.openSession(sid);
1463
1503
  rows.push(`${s.id === activeSid ? '\u2726' : '-'} \`${s.code || '?'}\` ${escapeWa(s.title).slice(0, 40)}${s.id === activeSid ? ' *(aktif)*' : ''}`);
1464
1504
  } catch {}
@@ -1591,7 +1631,7 @@ async function tryWaSlash(jid, rawText, senderNum, payload0) {
1591
1631
  } else if (cmd === 'change') {
1592
1632
  out = modelChangeText(arg);
1593
1633
  } else if (cmd === 'notes') {
1594
- const sid = waChats.get(jid);
1634
+ const sid = waChats.get(waSessionKey(jid));
1595
1635
  out = sid ? notesText(sid) : 'Bu sohbetin oturumu yok — önce bir şeyler yaz.';
1596
1636
  } else if (cmd === 'notify') {
1597
1637
  const a = String(arg || '').toLowerCase();
@@ -1602,7 +1642,7 @@ async function tryWaSlash(jid, rawText, senderNum, payload0) {
1602
1642
  }
1603
1643
  out = `Hata mail bildirimi: ${settings.notifyOwnerFail !== false ? 'AÇIK' : 'KAPALI'} (değiştirmek için /notify on|off)`;
1604
1644
  } else if (cmd === 'clear') {
1605
- const sid = waChats.get(jid);
1645
+ const sid = waChats.get(waSessionKey(jid));
1606
1646
  if (sid && engine.clearMessages(sid)) {
1607
1647
  out = '*Bu oturumun geçmişi temizlendi.* Aynı kodla sıfırdan devam edebilirsin.';
1608
1648
  } else {
@@ -1732,7 +1772,7 @@ async function tryWaSlash(jid, rawText, senderNum, payload0) {
1732
1772
  ? `*Tüm oturumların todoları temizlendi* (${n} oturum, ${items} madde).`
1733
1773
  : 'Temizlenecek todo yok.';
1734
1774
  } else {
1735
- const sid = waChats.get(jid) || waSingleSid || '';
1775
+ const sid = waChats.get(waSessionKey(jid)) || '';
1736
1776
  if (!sid) {
1737
1777
  out = 'Bu sohbette açık oturum yok — todo listesi de yok.';
1738
1778
  } else {
@@ -1774,14 +1814,13 @@ async function tryWaSlash(jid, rawText, senderNum, payload0) {
1774
1814
  if (!waBcMode.has(jid)) {
1775
1815
  out = 'Zaten sohbet modundasın — kodlamak için /beastcode yaz.';
1776
1816
  } else {
1777
- const sid = waChats.get(jid);
1817
+ const sid = waChats.get(waSessionKey(jid));
1778
1818
  if (sid && engine.isBusy(sid)) {
1779
1819
  try { engine.interrupt(sid, 'kullanıcı /beastagent ile sohbet moduna döndü'); } catch {}
1780
1820
  }
1781
1821
  waBcMode.delete(jid);
1782
- /* KANAL TEK OTURUMU: yeni oturum AÇILMAZ — sohbet oturumuna dönülür
1783
- (silinmediyse hep aynı; ilk kez ise burada oluşur) */
1784
- const chatSid = ensureWaSession(jid);
1822
+ /* KİŞİ BAŞINA OTURUM: kişinin kendi sohbet oturumuna dönülür */
1823
+ const chatSid = waRestoreChatSession(jid);
1785
1824
  const chatS = engine.cache.get(chatSid);
1786
1825
  saveWaChats();
1787
1826
  if (wa) wa.setWatchJids([...waChats.keys()]);
@@ -1794,7 +1833,7 @@ async function tryWaSlash(jid, rawText, senderNum, payload0) {
1794
1833
  } else if (waBcMode.has(jid) && (cmd === 'plan' || cmd === 'build' || cmd === 'auto')) {
1795
1834
  /* opencode ajan değişimi (WA): /plan → plan ajanı (salt-okur),
1796
1835
  /build · /auto → build ajanı (varsayılan; auto opencode'da yoktur) */
1797
- const sid = waChats.get(jid);
1836
+ const sid = waChats.get(waSessionKey(jid));
1798
1837
  const s = sid ? engine.cache.get(sid) : null;
1799
1838
  if (s && s.bcCode) {
1800
1839
  const agent = engine.setBcAgent(s.id, cmd === 'plan' ? 'plan' : 'build');
@@ -1899,12 +1938,11 @@ function waGroupSenderInfo(payload) {
1899
1938
  return { label, name: (hitP && hitP.name) || '', isOwner: !!(hitP && hitP.owner) };
1900
1939
  }
1901
1940
 
1902
- /* ---------- KANAL TEK OTURUMU (WA/TG/DC) ----------
1903
- Her kanalın BİR aktif sohbet oturumu vardır; eskisi silinmedikçe hep
1904
- AYNI oturumdan devam edilir — Beast kendi kendine oturum kapatıp
1905
- yenisiyle değiştirmez. Yenisi yalnızca: (1) kayıtlı oturum dosyası
1906
- yoksa (kullanıcı UI'dan sildi), (2) kullanıcı WA'da /new yazarsa.
1907
- Masaüstü chat UI'daki "+ Yeni Sohbet" bu kuralın dışındadır. */
1941
+ /* ---------- KİŞİ BAŞINA KANAL OTURUMU (WA/TG/DC) ----------
1942
+ Her numara/grup/sohbet KENDİ oturumunda konuşur — A'nın geçmişi B'ye
1943
+ bağlam olarak sızmaz. Oturum yalnızca o sohbet silinirse yenilenir;
1944
+ cron/otomatik işler ayrı "taban oturumlarında" koşar (bkz. cronBaseSession).
1945
+ Eski "tek oturum" düzeni açılışta güvenli biçimde göç ettirilir. */
1908
1946
  function sessionFileAlive(sid) {
1909
1947
  try {
1910
1948
  return !!sid && fs.existsSync(path.join(engine.sessionsDir, String(sid) + '.jsonl'));
@@ -1912,9 +1950,6 @@ function sessionFileAlive(sid) {
1912
1950
  return false;
1913
1951
  }
1914
1952
  }
1915
-
1916
- /* kanalın yaşayan tek oturumunu döndürür; geçersizse eski bağlamdan devralır,
1917
- o da yoksa (ilk kullanım) yeni açar ve tüm jid'leri ona bağlar */
1918
1953
  /* WA'dan gelen belge orijinallerini oturumun media klasörüne yazar;
1919
1954
  döner: media/<sid>/<dosya> (relatif yol — ajan read_file ile açabilir) */
1920
1955
  function saveSessionMedia(sid, name, buf) {
@@ -1932,54 +1967,40 @@ function saveSessionMedia(sid, name, buf) {
1932
1967
  }
1933
1968
  }
1934
1969
 
1970
+ /* KİŞİ BAŞINA OTURUM: jid yoksa CRON TABAN OTURUMU döner (otomatik işler
1971
+ hiçbir kişinin sohbetine karışmaz); varsa o sohbetin kendi oturumu açılır. */
1935
1972
  function ensureWaSession(jid) {
1936
- if (!waSingleSid || !sessionFileAlive(waSingleSid)) {
1937
- let best = '';
1938
- let bestTs = '';
1939
- for (const c of new Set([...waChats.values(), waSingleSid])) {
1940
- if (!c || !sessionFileAlive(c)) continue;
1941
- let ts = '';
1942
- try {
1943
- const s = engine.cache.get(c) || engine._load(c);
1944
- ts = String((s && s.updatedAt) || '');
1945
- } catch {}
1946
- if (!best || ts > bestTs) {
1947
- best = c;
1948
- bestTs = ts;
1949
- }
1950
- }
1951
- waSingleSid = best || engine.createSession().id;
1952
- saveWaChats();
1953
- }
1954
- let dirty = false;
1955
- for (const [j, s] of waChats.entries()) {
1956
- if (s !== waSingleSid) {
1957
- waChats.set(j, waSingleSid);
1958
- dirty = true;
1959
- }
1960
- }
1961
- if (jid != null && waChats.get(String(jid)) !== waSingleSid) {
1962
- waChats.set(String(jid), waSingleSid);
1963
- dirty = true;
1964
- }
1965
- /* jid'siz çağrı (cron taban oturumu) sohbet geçmişini kirletmez */
1966
- if (jid != null) waRememberSession(String(jid), waSingleSid);
1967
- if (dirty) {
1973
+ if (jid == null || jid === '') return ensureWaCronSession();
1974
+ const key = waSessionKey(jid);
1975
+ const sid = waChats.get(key);
1976
+ if (sid && sessionFileAlive(sid)) return sid;
1977
+ const v = engine.createSession();
1978
+ waChats.set(key, v.id);
1979
+ waRememberSession(key, v.id);
1980
+ saveWaChats();
1981
+ if (wa) wa.setWatchJids([...waChats.keys()]);
1982
+ return v.id;
1983
+ }
1984
+
1985
+ /* Cron/otomatik işlerin WhatsApp taban oturumu: hiçbir jid'e bağlı DEĞİL */
1986
+ function ensureWaCronSession() {
1987
+ if (!waCronSid || !sessionFileAlive(waCronSid)) {
1988
+ waCronSid = engine.createSession().id;
1968
1989
  saveWaChats();
1969
- if (wa) wa.setWatchJids([...waChats.keys()]);
1970
1990
  }
1971
- return waSingleSid;
1991
+ return waCronSid;
1972
1992
  }
1973
1993
 
1974
- /* Kanal tek oturumunda cevap/dosya hedefi: bu oturuma bağlı sohbetler arasında
1975
- EN SON mesaj gelen önceliklidir (kullanıcı o pencerede bekliyor); o bilgi
1976
- yoksa (restart vb.) ilk bağlı sohbet kullanılır. null = bağlı sohbet yok. */
1994
+ /* Cevap/dosya hedefi: oturuma bağlı sohbetler arasında EN SON mesaj gelen
1995
+ önceliklidir; o bilgi yoksa (restart vb.) bağlı sohbet kullanılır. */
1977
1996
  function waReplyJid(sid) {
1997
+ const s = String(sid || '');
1978
1998
  try {
1979
- if (waLastActiveJid && waChats.get(waLastActiveJid) === sid) return waLastActiveJid;
1999
+ const lastKey = waLastActiveJid ? waSessionKey(waLastActiveJid) : '';
2000
+ if (lastKey && String(waChats.get(lastKey) || '') === s) return lastKey;
1980
2001
  } catch {}
1981
- for (const [j, s] of waChats) {
1982
- if (s === sid) return j;
2002
+ for (const [j, v] of waChats) {
2003
+ if (String(v) === s) return j;
1983
2004
  }
1984
2005
  return null;
1985
2006
  }
@@ -2382,6 +2403,9 @@ async function handleWaIncoming(jid, payload, senderNum) {
2382
2403
  if (!engine) return;
2383
2404
  if (typeof payload === 'string') payload = { text: payload };
2384
2405
  const isGroup = !!payload.isGroup || jid.endsWith('@g.us');
2406
+ /* LID → gerçek numara eşlemesi SLASH KOMUTLARINDAN ÖNCE kurulmalı:
2407
+ oturum anahtarı (/new, /sessions, /open) gerçek numaraya göre çalışır */
2408
+ if (!isGroup && senderNum) waJidPn.set(jid, String(senderNum));
2385
2409
  /* İzin listesi kaydı — grup kapıları ve doğal dil komutları bunu kullanır:
2386
2410
  izinli değilse yalnız sohbet; agentic iş yaptıramaz. */
2387
2411
  const senderHit = waFind(senderNum);
@@ -2575,15 +2599,8 @@ async function processWaMessage(jid, payload, senderNum, requeues = 0) {
2575
2599
  waLog(`skip flush: izinli eşleşme yok (sender=+${senderNum || '?'})`);
2576
2600
  return;
2577
2601
  }
2578
- /* OPENCODE STEER: oturum meşgulse mesaj bekleMEZ — engine.send tamponuna
2579
- ekler, koşan tur sonraki istekte görür ve cevaplar (eski cevap bozulmaz).
2580
- Sıra korunur: floş katmanı debounceli ve sıralı işler. */
2581
- let sid = waChats.get(jid) || waSingleSid;
2582
- if (!isGroup && requeues > 0) {
2583
- // retry sonrası izin yeniden kontrol edilir (yukarıda hit zaten alınıyor)
2584
- }
2585
- /* KANAL TEK OTURUMU: eskisi silinmedikçe hep aynı oturumdan devam */
2586
- sid = ensureWaSession(jid);
2602
+ /* KİŞİ BAŞINA OTURUM: her numara/grup kendi oturumunda; oturum yoksa açılır */
2603
+ const sid = ensureWaSession(jid);
2587
2604
  waLastActiveJid = String(jid); // cevap/dosya hedefi: en son yazan sohbet
2588
2605
  // Cevap verilecek — karşı telefonda "yazıyor…" göstergesi (medya işlenene dek sürer)
2589
2606
  wa.setComposing(jid, true);
@@ -2792,16 +2809,31 @@ function ensureWa() {
2792
2809
  if (Array.isArray(arr)) tgHistory.set(c, arr.filter((x) => typeof x === 'string').slice(-TG_HISTORY_CAP));
2793
2810
  }
2794
2811
  }
2795
- /* kanal tek oturumu: kayıtlı aktif id (geçersizse ensure yeniden seçer) */
2796
- if (raw && typeof raw.active === 'string' && raw.active) tgSingleSid = raw.active;
2812
+ /* CRON TABAN OTURUMU: kişi sohbetlerinden tamamen bağımsız oturum */
2813
+ if (raw && typeof raw.cron === 'string' && raw.cron) tgCronSid = raw.cron;
2797
2814
  for (const [c, s] of tgChats.entries()) {
2798
2815
  const h = tgHistory.get(c) || [];
2799
2816
  if (!h.includes(s)) h.push(s);
2800
2817
  tgHistory.set(c, h.slice(-TG_HISTORY_CAP));
2801
2818
  }
2802
2819
  } catch {}
2820
+ /* GÖÇ: eski "tek oturum" düzeni → kişi başına oturum (sahip bağlamı korunur) */
2821
+ try {
2822
+ tgMigrateSharedSessions();
2823
+ } catch {}
2803
2824
  })();
2804
2825
 
2826
+ /* GÖÇ (Telegram): ortak oturum sahipte kalır; diğer sohbetler ayrılır. */
2827
+ function tgMigrateSharedSessions() {
2828
+ const owners = tgOwnerIds();
2829
+ return chansessions.migrateSharedSessions({
2830
+ chats: tgChats,
2831
+ history: tgHistory,
2832
+ ownerDigits: (owners[0] || '').replace(/\D/g, ''),
2833
+ historyCap: TG_HISTORY_CAP,
2834
+ });
2835
+ }
2836
+
2805
2837
  function saveTgChats() {
2806
2838
  try {
2807
2839
  fs.writeFileSync(
@@ -2809,7 +2841,7 @@ function saveTgChats() {
2809
2841
  JSON.stringify({
2810
2842
  chats: Object.fromEntries(tgChats),
2811
2843
  history: Object.fromEntries([...tgHistory.entries()].map(([c, a]) => [c, a.slice(-TG_HISTORY_CAP)])),
2812
- active: tgSingleSid || '',
2844
+ cron: tgCronSid || '',
2813
2845
  })
2814
2846
  );
2815
2847
  } catch {}
@@ -2891,11 +2923,9 @@ async function processTgMessage(chatId, payload, requeues = 0) {
2891
2923
  .catch(() => {});
2892
2924
  return;
2893
2925
  }
2894
- /* OPENCODE STEER: meşgulse bekleme — engine.send tampona ekler, koşan tur
2895
- sonraki istekte görür (WA ile aynı mantık) */
2896
- let sid = tgChats.get(chatId) || tgSingleSid;
2897
- /* KANAL TEK OTURUMU: eskisi silinmedikçe hep aynı oturumdan devam */
2898
- sid = ensureTgSession(chatId);
2926
+ /* KİŞİ BAŞINA OTURUM: her chatId kendi oturumunda; oturum yoksa açılır */
2927
+ const sid = ensureTgSession(chatId);
2928
+ tgLastActiveChatId = String(chatId);
2899
2929
  /* Kişi bazlı granül izin: all/web/read/chat */
2900
2930
  let perm = hit.perm || (hit.lockdown ? 'chat' : 'all');
2901
2931
  engine.setSessionPerm(sid, perm);
@@ -3018,16 +3048,31 @@ function dcFind(senderId, username) {
3018
3048
  if (Array.isArray(arr)) dcHistory.set(c, arr.filter((x) => typeof x === 'string').slice(-DC_HISTORY_CAP));
3019
3049
  }
3020
3050
  }
3021
- /* kanal tek oturumu: kayıtlı aktif id (geçersizse ensure yeniden seçer) */
3022
- if (raw && typeof raw.active === 'string' && raw.active) dcSingleSid = raw.active;
3051
+ /* CRON TABAN OTURUMU: kişi sohbetlerinden tamamen bağımsız oturum */
3052
+ if (raw && typeof raw.cron === 'string' && raw.cron) dcCronSid = raw.cron;
3023
3053
  for (const [c, s] of dcChats.entries()) {
3024
3054
  const h = dcHistory.get(c) || [];
3025
3055
  if (!h.includes(s)) h.push(s);
3026
3056
  dcHistory.set(c, h.slice(-DC_HISTORY_CAP));
3027
3057
  }
3028
3058
  } catch {}
3059
+ /* GÖÇ: eski "tek oturum" düzeni → kişi başına oturum (sahip bağlamı korunur) */
3060
+ try {
3061
+ dcMigrateSharedSessions();
3062
+ } catch {}
3029
3063
  })();
3030
3064
 
3065
+ /* GÖÇ (Discord): ortak oturum sahipte kalır; diğer kanallar ayrılır. */
3066
+ function dcMigrateSharedSessions() {
3067
+ const owners = dcOwnerIds();
3068
+ return chansessions.migrateSharedSessions({
3069
+ chats: dcChats,
3070
+ history: dcHistory,
3071
+ ownerDigits: (owners[0] || '').replace(/\D/g, ''),
3072
+ historyCap: DC_HISTORY_CAP,
3073
+ });
3074
+ }
3075
+
3031
3076
  function saveDcChats() {
3032
3077
  try {
3033
3078
  fs.writeFileSync(
@@ -3035,7 +3080,7 @@ function saveDcChats() {
3035
3080
  JSON.stringify({
3036
3081
  chats: Object.fromEntries(dcChats),
3037
3082
  history: Object.fromEntries([...dcHistory.entries()].map(([c, a]) => [c, a.slice(-DC_HISTORY_CAP)])),
3038
- active: dcSingleSid || '',
3083
+ cron: dcCronSid || '',
3039
3084
  })
3040
3085
  );
3041
3086
  } catch {}
@@ -3111,11 +3156,9 @@ async function processDcMessage(channelId, payload) {
3111
3156
  .catch(() => {});
3112
3157
  return;
3113
3158
  }
3114
- /* OPENCODE STEER: meşgulse bekleme — engine.send tampona ekler, koşan tur
3115
- sonraki istekte görür (WA/TG ile aynı mantık) */
3116
- let sid = dcChats.get(channelId) || dcSingleSid;
3117
- /* KANAL TEK OTURUMU: eskisi silinmedikçe hep aynı oturumdan devam */
3118
- sid = ensureDcSession(channelId);
3159
+ /* KİŞİ BAŞINA OTURUM: her kanal kendi oturumunda; oturum yoksa açılır */
3160
+ const sid = ensureDcSession(channelId);
3161
+ dcLastActiveChannelId = String(channelId);
3119
3162
  /* Kişi bazlı granül izin: all/web/read/chat */
3120
3163
  let perm = hit.perm || (hit.lockdown ? 'chat' : 'all');
3121
3164
  engine.setSessionPerm(sid, perm);
@@ -3430,14 +3473,14 @@ function reloadBackend() {
3430
3473
  } catch {}
3431
3474
  try {
3432
3475
  if (tg && tg.connected) {
3433
- const hit = [...tgChats.entries()].find(([, s]) => s === ev.sessionId);
3434
- if (hit) await sendTgSafe(hit[0], txt);
3476
+ const tgid = tgReplyChat(ev.sessionId);
3477
+ if (tgid) await sendTgSafe(tgid, txt);
3435
3478
  }
3436
3479
  } catch {}
3437
3480
  try {
3438
3481
  if (dc && dc.connected) {
3439
- const hit = [...dcChats.entries()].find(([, s]) => s === ev.sessionId);
3440
- if (hit) await sendDcSafe(hit[0], txt);
3482
+ const dchid = dcReplyChannel(ev.sessionId);
3483
+ if (dchid) await sendDcSafe(dchid, txt);
3441
3484
  }
3442
3485
  } catch {}
3443
3486
  })();
@@ -3445,9 +3488,8 @@ function reloadBackend() {
3445
3488
  }
3446
3489
  // Telegram oturumlarının son cevabını geri gönder (WA ile aynı akış)
3447
3490
  if ((ev.type === 'done' || ev.type === 'error') && !cjob && tg && tg.connected) {
3448
- const hitT = [...tgChats.entries()].find(([, s]) => s === ev.sessionId);
3449
- if (hitT) {
3450
- const tgid = hitT[0];
3491
+ const tgid = tgReplyChat(ev.sessionId);
3492
+ if (tgid) {
3451
3493
  (async () => {
3452
3494
  try {
3453
3495
  if (ev.type === 'error') {
@@ -3466,9 +3508,8 @@ function reloadBackend() {
3466
3508
  }
3467
3509
  // Discord oturumlarının son cevabını geri gönder (TG ile aynı akış)
3468
3510
  if ((ev.type === 'done' || ev.type === 'error') && !cjob && dc && dc.connected) {
3469
- const hitD = [...dcChats.entries()].find(([, s]) => s === ev.sessionId);
3470
- if (hitD) {
3471
- const dchid = hitD[0];
3511
+ const dchid = dcReplyChannel(ev.sessionId);
3512
+ if (dchid) {
3472
3513
  (async () => {
3473
3514
  try {
3474
3515
  if (ev.type === 'error') {
@@ -3486,10 +3527,10 @@ function reloadBackend() {
3486
3527
  }
3487
3528
  }
3488
3529
  /* CRON → TÜM BAĞLI ENTEGRASYONLAR: cron işi bittiğinde cevap (ya da
3489
- hata) WhatsApp (base) + Telegram + Discord'un hepsine yansıtılır.
3490
- Yansıtma yalnız cron oturumunun KENDİ kanalına yapılmaz (orası ham
3491
- cevabı normal akıştan alır). Bayat (TTL aşımı) kayıt Take ile
3492
- düşer, yansıtılmaz. */
3530
+ hata) sahibe WhatsApp + Telegram + Discord'dan yansıtılır. Cron
3531
+ oturumu hiçbir kişi sohbetine bağlı olmadığı için ham cevap normal
3532
+ akıştan GİTMEZ — çift gönderim olmaz. Bayat (TTL aşımı) kayıt Take
3533
+ ile düşer, yansıtılmaz. */
3493
3534
  if (cjob && !ev.aborted) {
3494
3535
  (async () => {
3495
3536
  try {
@@ -3561,7 +3602,9 @@ function falloutResume() {
3561
3602
  Ayarlardan kapatılabilir (settings.whereWasI.enabled). */
3562
3603
  function whereWasISummary() {
3563
3604
  try {
3564
- const last = engine.lastWhereWasI();
3605
+ /* KANAL İZOLASYONU: kişi sohbetleri özet dışıdır — özet masaüstü
3606
+ çalışmasından seçilir ve yanlış kişiye gönderilmez */
3607
+ const last = engine.lastWhereWasI(channelSessionIds());
3565
3608
  if (!last) return null;
3566
3609
  const lines = [];
3567
3610
  lines.push(`Son oturum: \`${last.code}\` — ${last.title}`);
@@ -7412,9 +7455,25 @@ function cronAnswerPendingTake(sid) {
7412
7455
  yapılır (ensureWa/Tg/DcSession ile aynı kural). Ayrıca meşgul oturuma
7413
7456
  send() mesajı DÜŞÜRÜR (false döner, cevap kaybolur) — o yüzden
7414
7457
  meşgul oturumlar da atlanır. */
7458
+ /* Kanal (WA/TG/DC) kişi sohbetleri + kanal cron oturumları: otomatik işler
7459
+ (izleyici/fallout/proaktif) bu oturumlara ASLA enjekte edilmez. */
7460
+ function channelSessionIds() {
7461
+ return chansessions.collectSessionIds(
7462
+ waChats,
7463
+ tgChats,
7464
+ dcChats,
7465
+ new Map([
7466
+ ['wa-cron', waCronSid],
7467
+ ['tg-cron', tgCronSid],
7468
+ ['dc-cron', dcCronSid],
7469
+ ])
7470
+ );
7471
+ }
7472
+
7415
7473
  function reuseOrLatestSession(preferredId) {
7416
7474
  const sid = String(preferredId || '');
7417
- if (sid && sessionFileAlive(sid)) {
7475
+ const chan = channelSessionIds();
7476
+ if (sid && sessionFileAlive(sid) && !chan.has(sid)) {
7418
7477
  try {
7419
7478
  if (engine._load(sid) && !engine.isBusy(sid)) return sid;
7420
7479
  } catch {}
@@ -7422,35 +7481,25 @@ function reuseOrLatestSession(preferredId) {
7422
7481
  try {
7423
7482
  const list = engine.listSessions(); // updatedAt'e göre yeni→eski sıralı
7424
7483
  for (const v of list) {
7484
+ if (chan.has(String(v.id))) continue; /* kişi sohbetine otomatik iş düşmez */
7425
7485
  if (!engine.isBusy(v.id)) return String(v.id);
7426
7486
  }
7427
7487
  } catch {}
7428
7488
  return engine.createSession().id;
7429
7489
  }
7430
7490
 
7431
- /* CRON TABAN OTURUMU: cron işleri hiçbir oturuma/sessionId'ye BAĞLI DEĞİL.
7432
- Her kanalın TEK oturumu vardır — taban sırası: bağlı WhatsApp → bağlı
7433
- Telegram → bağlı Discord → en güncel masaüstü oturumu. WhatsApp
7434
- kullanılmayan kurulumlarda cron altyapısı Telegram/Discord üzerinden
7435
- yaşar; hiçbiri yoksa masaüstü sohbete düşer. */
7491
+ /* CRON TABAN OTURUMU: cron işleri hiçbir KİŞİ sohbetine/oturumuna BAĞLI DEĞİL.
7492
+ Her kanalın kendine ait ayrı cron oturumu vardır — sıra: WA → TG → DC →
7493
+ en güncel masaüstü oturumu. Cevap done olayında sahibe yansıtılır. */
7436
7494
  function cronBaseSession() {
7437
7495
  try {
7438
- if (wa && wa.connected) {
7439
- if (waSingleSid && sessionFileAlive(waSingleSid)) return waSingleSid;
7440
- return ensureWaSession();
7441
- }
7496
+ if (wa && wa.connected) return ensureWaCronSession();
7442
7497
  } catch {}
7443
7498
  try {
7444
- if (tg && tg.connected) {
7445
- if (tgSingleSid && sessionFileAlive(tgSingleSid)) return tgSingleSid;
7446
- return ensureTgSession();
7447
- }
7499
+ if (tg && tg.connected) return ensureTgCronSession();
7448
7500
  } catch {}
7449
7501
  try {
7450
- if (dc && dc.connected) {
7451
- if (dcSingleSid && sessionFileAlive(dcSingleSid)) return dcSingleSid;
7452
- return ensureDcSession();
7453
- }
7502
+ if (dc && dc.connected) return ensureDcCronSession();
7454
7503
  } catch {}
7455
7504
  return reuseOrLatestSession('');
7456
7505
  }
@@ -7472,41 +7521,34 @@ function dcOwnerIds() {
7472
7521
  const owner = objs.find((e) => e.owner) || (objs.length === 1 ? objs[0] : null);
7473
7522
  return owner ? [String(owner.id)] : [];
7474
7523
  }
7524
+ /* Cron cevabının gideceği yerler: kişi sohbetleri DEĞİL, sahip hedeflenir.
7525
+ (Cron oturumu hiçbir jid/chatId'ye bağlı değildir; ham cevap normal akıştan
7526
+ gitmez — yansıtma burada yapılır.) */
7475
7527
  function cronMirrorTargets(cronSid) {
7476
- const sid = String(cronSid || '');
7477
7528
  const out = [];
7478
- /* WHATSAPP (base): normal akış cron oturumuna bağlı sohbete gönderir;
7479
- bağlı sohbet yoksa (normal akış gönderemez) base sohbet/sahip numarası
7480
- hedeflenir — cevap kaybolmaz. */
7529
+ /* WHATSAPP: cron oturumu hiçbir kişiye bağlı değildir — cevap SAHİBE gider.
7530
+ Kişi sohbetlerine cron cevabı KARIŞMAZ. */
7481
7531
  try {
7482
- if (wa && wa.connected && !waReplyJid(sid)) {
7532
+ if (wa && wa.connected) {
7483
7533
  const own = waOwnerNum();
7484
- const jid = waReplyJid(waSingleSid) || (own ? own + '@s.whatsapp.net' : '');
7534
+ const jid = own ? own + '@s.whatsapp.net' : '';
7485
7535
  if (jid) out.push({ kind: 'wa', send: (t) => sendWaSafe(jid, t) });
7486
7536
  }
7487
7537
  } catch {}
7488
- /* TELEGRAM: normal akış cron oturumuna bağlı sohbete gönderir; o yoksa
7489
- kanalın tek oturumuna bağlı sohbet(ler), yoksa sahip kaydı hedeflenir */
7538
+ /* TELEGRAM: sahip kaydı hedeflenir; sahip tanımsızsa en son yazan sohbet */
7490
7539
  try {
7491
- if (tg && tg.connected && ![...tgChats.values()].some((s) => String(s) === sid)) {
7492
- const bound = [...tgChats.entries()]
7493
- .filter(([, s]) => String(s) === String(tgSingleSid))
7494
- .map(([c]) => c);
7495
- for (const id of (bound.length ? bound : tgOwnerIds())) {
7496
- out.push({ kind: 'tg', send: (t) => sendTgSafe(id, t) });
7497
- }
7540
+ if (tg && tg.connected) {
7541
+ const ids = tgOwnerIds();
7542
+ const targets = ids.length ? ids : (tgLastActiveChatId ? [tgLastActiveChatId] : []);
7543
+ for (const id of targets) out.push({ kind: 'tg', send: (t) => sendTgSafe(id, t) });
7498
7544
  }
7499
7545
  } catch {}
7500
- /* DISCORD: normal akış cron oturumuna bağlı kanala gönderir; o yoksa
7501
- kanalın tek oturumuna bağlı kanal(lar), yoksa sahip kaydı hedeflenir */
7546
+ /* DISCORD: sahip kaydı hedeflenir; sahip tanımsızsa en son yazan kanal */
7502
7547
  try {
7503
- if (dc && dc.connected && ![...dcChats.values()].some((s) => String(s) === sid)) {
7504
- const bound = [...dcChats.entries()]
7505
- .filter(([, s]) => String(s) === String(dcSingleSid))
7506
- .map(([c]) => c);
7507
- for (const id of (bound.length ? bound : dcOwnerIds())) {
7508
- out.push({ kind: 'dc', send: (t) => sendDcSafe(id, t) });
7509
- }
7548
+ if (dc && dc.connected) {
7549
+ const ids = dcOwnerIds();
7550
+ const targets = ids.length ? ids : (dcLastActiveChannelId ? [dcLastActiveChannelId] : []);
7551
+ for (const id of targets) out.push({ kind: 'dc', send: (t) => sendDcSafe(id, t) });
7510
7552
  }
7511
7553
  } catch {}
7512
7554
  return out;
@@ -7870,76 +7912,70 @@ function empatiInjectText(ev, out) {
7870
7912
  );
7871
7913
  }
7872
7914
 
7873
- /* kanalın yaşayan tek oturumu (WA kuralının aynısı — Telegram/Discord) */
7915
+ /* KİŞİ BAŞINA OTURUM (Telegram): her chatId kendi oturumunda; chatId yoksa
7916
+ CRON TABAN OTURUMU döner (otomatik işler kişi sohbetlerine karışmaz). */
7874
7917
  function ensureTgSession(chatId) {
7875
- if (!tgSingleSid || !sessionFileAlive(tgSingleSid)) {
7876
- let best = '';
7877
- let bestTs = '';
7878
- for (const c of new Set([...tgChats.values(), tgSingleSid])) {
7879
- if (!c || !sessionFileAlive(c)) continue;
7880
- let ts = '';
7881
- try {
7882
- const s = engine.cache.get(c) || engine._load(c);
7883
- ts = String((s && s.updatedAt) || '');
7884
- } catch {}
7885
- if (!best || ts > bestTs) {
7886
- best = c;
7887
- bestTs = ts;
7888
- }
7889
- }
7890
- tgSingleSid = best || engine.createSession().id;
7918
+ if (chatId == null || chatId === '') return ensureTgCronSession();
7919
+ const key = String(chatId);
7920
+ const sid = tgChats.get(key);
7921
+ if (sid && sessionFileAlive(sid)) return sid;
7922
+ const v = engine.createSession();
7923
+ tgChats.set(key, v.id);
7924
+ tgRememberSession(key, v.id);
7925
+ saveTgChats();
7926
+ return v.id;
7927
+ }
7928
+
7929
+ /* Cron/otomatik işlerin Telegram taban oturumu: hiçbir sohbete bağlı DEĞİL */
7930
+ function ensureTgCronSession() {
7931
+ if (!tgCronSid || !sessionFileAlive(tgCronSid)) {
7932
+ tgCronSid = engine.createSession().id;
7891
7933
  saveTgChats();
7892
7934
  }
7893
- let dirty = false;
7894
- for (const [c, s] of tgChats.entries()) {
7895
- if (s !== tgSingleSid) {
7896
- tgChats.set(c, tgSingleSid);
7897
- dirty = true;
7898
- }
7899
- }
7900
- if (chatId != null && tgChats.get(String(chatId)) !== tgSingleSid) {
7901
- tgChats.set(String(chatId), tgSingleSid);
7902
- dirty = true;
7935
+ return tgCronSid;
7936
+ }
7937
+
7938
+ /* Telegram cevap hedefi: oturuma bağlı sohbet (en son yazan öncelikli) */
7939
+ function tgReplyChat(sid) {
7940
+ const s = String(sid || '');
7941
+ if (tgLastActiveChatId && String(tgChats.get(tgLastActiveChatId) || '') === s) return tgLastActiveChatId;
7942
+ for (const [c, v] of tgChats) {
7943
+ if (String(v) === s) return c;
7903
7944
  }
7904
- /* id'siz çağrı (cron taban oturumu) sohbet geçmişini kirletmez */
7905
- if (chatId != null) tgRememberSession(String(chatId), tgSingleSid);
7906
- if (dirty) saveTgChats();
7907
- return tgSingleSid;
7945
+ return '';
7908
7946
  }
7947
+
7948
+ /* KİŞİ BAŞINA OTURUM (Discord): her kanal kendi oturumunda; channelId yoksa
7949
+ CRON TABAN OTURUMU döner (otomatik işler kişi sohbetlerine karışmaz). */
7909
7950
  function ensureDcSession(channelId) {
7910
- if (!dcSingleSid || !sessionFileAlive(dcSingleSid)) {
7911
- let best = '';
7912
- let bestTs = '';
7913
- for (const c of new Set([...dcChats.values(), dcSingleSid])) {
7914
- if (!c || !sessionFileAlive(c)) continue;
7915
- let ts = '';
7916
- try {
7917
- const s = engine.cache.get(c) || engine._load(c);
7918
- ts = String((s && s.updatedAt) || '');
7919
- } catch {}
7920
- if (!best || ts > bestTs) {
7921
- best = c;
7922
- bestTs = ts;
7923
- }
7924
- }
7925
- dcSingleSid = best || engine.createSession().id;
7951
+ if (channelId == null || channelId === '') return ensureDcCronSession();
7952
+ const key = String(channelId);
7953
+ const sid = dcChats.get(key);
7954
+ if (sid && sessionFileAlive(sid)) return sid;
7955
+ const v = engine.createSession();
7956
+ dcChats.set(key, v.id);
7957
+ dcRememberSession(key, v.id);
7958
+ saveDcChats();
7959
+ return v.id;
7960
+ }
7961
+
7962
+ /* Cron/otomatik işlerin Discord taban oturumu: hiçbir kanala bağlı DEĞİL */
7963
+ function ensureDcCronSession() {
7964
+ if (!dcCronSid || !sessionFileAlive(dcCronSid)) {
7965
+ dcCronSid = engine.createSession().id;
7926
7966
  saveDcChats();
7927
7967
  }
7928
- let dirty = false;
7929
- for (const [c, s] of dcChats.entries()) {
7930
- if (s !== dcSingleSid) {
7931
- dcChats.set(c, dcSingleSid);
7932
- dirty = true;
7933
- }
7934
- }
7935
- if (channelId != null && dcChats.get(String(channelId)) !== dcSingleSid) {
7936
- dcChats.set(String(channelId), dcSingleSid);
7937
- dirty = true;
7968
+ return dcCronSid;
7969
+ }
7970
+
7971
+ /* Discord cevap hedefi: oturuma bağlı kanal (en son yazan öncelikli) */
7972
+ function dcReplyChannel(sid) {
7973
+ const s = String(sid || '');
7974
+ if (dcLastActiveChannelId && String(dcChats.get(dcLastActiveChannelId) || '') === s) return dcLastActiveChannelId;
7975
+ for (const [c, v] of dcChats) {
7976
+ if (String(v) === s) return c;
7938
7977
  }
7939
- /* id'siz çağrı (cron taban oturumu) sohbet geçmişini kirletmez */
7940
- if (channelId != null) dcRememberSession(String(channelId), dcSingleSid);
7941
- if (dirty) saveDcChats();
7942
- return dcSingleSid;
7978
+ return '';
7943
7979
  }
7944
7980
 
7945
7981
  /* Masaüstü hedefi: kullanıcının AÇIK sohbeti önceliklidir — proaktif not
@@ -7947,13 +7983,14 @@ function ensureDcSession(channelId) {
7947
7983
  Açık sohbet bilinmiyorsa en güncel meşgul olmayan sohbet (kanal oturumları
7948
7984
  hariç — onlara kanal yoluyla zaten enjekte edildi); o da yoksa yeni oturum. */
7949
7985
  function empatiDesktopSid(exclude) {
7950
- const skip = exclude instanceof Set ? exclude : null;
7986
+ const skip = exclude instanceof Set ? exclude : new Set();
7951
7987
  try {
7952
7988
  if (desktopActiveSid && sessionFileAlive(desktopActiveSid)) return String(desktopActiveSid);
7953
7989
  } catch {}
7954
7990
  try {
7991
+ const chan = channelSessionIds();
7955
7992
  for (const v of engine.listSessions()) {
7956
- if (skip && skip.has(String(v.id))) continue;
7993
+ if (skip.has(String(v.id)) || chan.has(String(v.id))) continue;
7957
7994
  if (!engine.isBusy(v.id)) return String(v.id);
7958
7995
  }
7959
7996
  } catch {}
@@ -13365,6 +13402,26 @@ ipcMain.handle('wa:queue:get', () => mqueue.stats());
13365
13402
 
13366
13403
  ipcMain.handle('wa:sessions', () => [...waChats.values()]);
13367
13404
 
13405
+ /* Sohbet geçmişi etiketi: soldaki listede oturumun HANGİ NUMARAYA/GRUBA ait
13406
+ olduğu görünür — DM'de "WhatsApp +905xx (Ad)", grupta "WhatsApp 1203… (grup)". */
13407
+ function waChatLabelFor(key) {
13408
+ const k = String(key || '');
13409
+ const digits = chansessions.jidDigits(k);
13410
+ if (k.endsWith('@g.us')) return 'WhatsApp ' + digits + ' (grup)';
13411
+ const hit = waFind(digits);
13412
+ const name = hit && hit.name ? String(hit.name) : '';
13413
+ return 'WhatsApp +' + digits + (name ? ' (' + name + ')' : '');
13414
+ }
13415
+
13416
+ ipcMain.handle('wa:sessions:info', () => {
13417
+ const out = [];
13418
+ for (const [key, sid] of waChats) {
13419
+ if (!sid) continue;
13420
+ out.push({ sid: String(sid), label: waChatLabelFor(key), group: String(key).endsWith('@g.us') });
13421
+ }
13422
+ return out;
13423
+ });
13424
+
13368
13425
  /* TTS sayısal ayar kelepçesi: geçersiz/boş değerde varsayılana döner */
13369
13426
  function clampTtsNum(v, lo, hi, def) {
13370
13427
  const n = Number(v);
@@ -13459,6 +13516,26 @@ ipcMain.handle('tg:allow:set', (_e, list) => {
13459
13516
  });
13460
13517
  ipcMain.handle('tg:sessions', () => [...tgChats.values()]);
13461
13518
 
13519
+ /* Sohbet geçmişi etiketi (Telegram): "Telegram <ad|chatId>" */
13520
+ function tgChatLabelFor(key) {
13521
+ const k = String(key || '');
13522
+ let name = '';
13523
+ try {
13524
+ const hit = tgFind(k, '');
13525
+ name = hit && hit.name ? String(hit.name) : '';
13526
+ } catch {}
13527
+ return 'Telegram ' + (name || k);
13528
+ }
13529
+
13530
+ ipcMain.handle('tg:sessions:info', () => {
13531
+ const out = [];
13532
+ for (const [key, sid] of tgChats) {
13533
+ if (!sid) continue;
13534
+ out.push({ sid: String(sid), label: tgChatLabelFor(key) });
13535
+ }
13536
+ return out;
13537
+ });
13538
+
13462
13539
  /* ---------- Discord IPC ---------- */
13463
13540
  ipcMain.handle('dc:status:get', () => {
13464
13541
  if (!dc) return { configured: !!settings.dcToken, status: 'disconnected', user: null, connected: false };
@@ -13495,6 +13572,26 @@ ipcMain.handle('dc:allow:set', (_e, list) => {
13495
13572
  });
13496
13573
  ipcMain.handle('dc:sessions', () => [...dcChats.values()]);
13497
13574
 
13575
+ /* Sohbet geçmişi etiketi (Discord): "Discord <ad|kanalId>" */
13576
+ function dcChatLabelFor(key) {
13577
+ const k = String(key || '');
13578
+ let name = '';
13579
+ try {
13580
+ const hit = dcFind(k, '');
13581
+ name = hit && hit.name ? String(hit.name) : '';
13582
+ } catch {}
13583
+ return 'Discord ' + (name || k);
13584
+ }
13585
+
13586
+ ipcMain.handle('dc:sessions:info', () => {
13587
+ const out = [];
13588
+ for (const [key, sid] of dcChats) {
13589
+ if (!sid) continue;
13590
+ out.push({ sid: String(sid), label: dcChatLabelFor(key) });
13591
+ }
13592
+ return out;
13593
+ });
13594
+
13498
13595
  /* ---------- e-posta IPC ---------- */
13499
13596
 
13500
13597
  ipcMain.handle('email:get', () => {
package/src/preload.js CHANGED
@@ -246,6 +246,9 @@ contextBridge.exposeInMainWorld('beast', {
246
246
  emailSend: (msg) => ipcRenderer.invoke('email:send', msg),
247
247
  captureScreen: () => ipcRenderer.invoke('screen:capture'),
248
248
  waListSessions: () => ipcRenderer.invoke('wa:sessions'),
249
+ waSessionsInfo: () => ipcRenderer.invoke('wa:sessions:info'),
250
+ tgSessionsInfo: () => ipcRenderer.invoke('tg:sessions:info'),
251
+ dcSessionsInfo: () => ipcRenderer.invoke('dc:sessions:info'),
249
252
  botsList: () => ipcRenderer.invoke('bots:list'),
250
253
  botsAdd: (input) => ipcRenderer.invoke('bots:add', input),
251
254
  botsDmList: () => ipcRenderer.invoke('bots:dm:list'),
@@ -455,6 +455,24 @@
455
455
  </div>
456
456
 
457
457
  <div id="mcpRow">
458
+ <section id="mcpChat">
459
+ <div id="mcpChatHead">
460
+ <span id="mcpChatTitle">MCP SOHBET</span>
461
+ <select id="mcpChatHist" title="MCP sohbet geçmişi — ana sohbetten ayrıdır"></select>
462
+ <span class="file-spacer"></span>
463
+ <button id="mcpChatNew" title="Yeni sohbet (mevcut sohbet geçmişte kalır)">*</button>
464
+ </div>
465
+ <div id="mcpChatOut"></div>
466
+ <div id="mcpChatStatus" hidden></div>
467
+ <div id="mcpChatInputRow">
468
+ <textarea id="mcpChatInput" rows="1" spellcheck="false" autocomplete="off" placeholder="MCP sohbeti — sunucu araçları ajanın emrinde…"></textarea>
469
+ <button id="mcpChatSend" title="Gönder (Enter)"><svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg></button>
470
+ <button id="mcpChatStop" title="Çalışan mesajı durdur" hidden data-i18n-title="bc_stop"><svg viewBox="0 0 24 24" width="13" height="13" fill="currentColor" stroke="none"><rect x="6" y="6" width="12" height="12" rx="2"/></svg></button>
471
+ </div>
472
+ </section>
473
+
474
+ <div id="mcpSplit" title="Sürükleyerek boyutlandır"></div>
475
+
458
476
  <section id="mcpPanel">
459
477
  <div id="mcpHead">
460
478
  <span id="mcpTitle">BEAST MCP</span>
@@ -483,24 +501,6 @@
483
501
  <textarea id="mcpJson" spellcheck="false"></textarea>
484
502
  </div>
485
503
  </section>
486
-
487
- <div id="mcpSplit" title="Sürükleyerek boyutlandır"></div>
488
-
489
- <section id="mcpChat">
490
- <div id="mcpChatHead">
491
- <span id="mcpChatTitle">MCP SOHBET</span>
492
- <select id="mcpChatHist" title="MCP sohbet geçmişi — ana sohbetten ayrıdır"></select>
493
- <span class="file-spacer"></span>
494
- <button id="mcpChatNew" title="Yeni sohbet (mevcut sohbet geçmişte kalır)">*</button>
495
- </div>
496
- <div id="mcpChatOut"></div>
497
- <div id="mcpChatStatus" hidden></div>
498
- <div id="mcpChatInputRow">
499
- <textarea id="mcpChatInput" rows="1" spellcheck="false" autocomplete="off" placeholder="MCP sohbeti — sunucu araçları ajanın emrinde…"></textarea>
500
- <button id="mcpChatSend" title="Gönder (Enter)"><svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg></button>
501
- <button id="mcpChatStop" title="Çalışan mesajı durdur" hidden data-i18n-title="bc_stop"><svg viewBox="0 0 24 24" width="13" height="13" fill="currentColor" stroke="none"><rect x="6" y="6" width="12" height="12" rx="2"/></svg></button>
502
- </div>
503
- </section>
504
504
  </div>
505
505
  </main>
506
506
 
@@ -1020,11 +1020,29 @@ async function renderSessions(list) {
1020
1020
  await loadSessionOrder();
1021
1021
  let waSet = new Set();
1022
1022
  let tgSet = new Set();
1023
+ let dcSet = new Set();
1024
+ /* KANAL ETİKETLERİ: oturumun hangi numaraya/grupa ait olduğu listede görünür */
1025
+ const chanLabel = new Map();
1023
1026
  try {
1024
- waSet = new Set(await beast.waListSessions());
1027
+ for (const it of (await beast.waSessionsInfo()) || []) {
1028
+ if (!it || !it.sid) continue;
1029
+ waSet.add(it.sid);
1030
+ if (it.label) chanLabel.set(String(it.sid), it.label);
1031
+ }
1025
1032
  } catch {}
1026
1033
  try {
1027
- tgSet = new Set(await beast.tgListSessions());
1034
+ for (const it of (await beast.tgSessionsInfo()) || []) {
1035
+ if (!it || !it.sid) continue;
1036
+ tgSet.add(it.sid);
1037
+ if (it.label) chanLabel.set(String(it.sid), it.label);
1038
+ }
1039
+ } catch {}
1040
+ try {
1041
+ for (const it of (await beast.dcSessionsInfo()) || []) {
1042
+ if (!it || !it.sid) continue;
1043
+ dcSet.add(it.sid);
1044
+ if (it.label) chanLabel.set(String(it.sid), it.label);
1045
+ }
1028
1046
  } catch {}
1029
1047
  els.sessList.innerHTML = '';
1030
1048
  /* BEAST FINANCE izolasyonu: finance modunda YALNIZ finance sohbetleri,
@@ -1044,10 +1062,14 @@ async function renderSessions(list) {
1044
1062
  row.dataset.sid = s.id; /* en üstteki sohbeti otomatik aktif etmek için */
1045
1063
  row.title = _t('sess_drag');
1046
1064
  row.draggable = true;
1065
+ const chan = chanLabel.get(String(s.id)) || '';
1047
1066
  row.innerHTML =
1048
1067
  (waSet.has(s.id) ? '<span class="sess-wa" title="WhatsApp">W</span>' : '') +
1049
1068
  (tgSet.has(s.id) ? '<span class="sess-tg" title="Telegram">T</span>' : '') +
1050
- `<span class="sess-title">${escapeHtml(s.title || 'Yeni Sohbet')}</span>` +
1069
+ (dcSet.has(s.id) ? '<span class="sess-dc" title="Discord">D</span>' : '') +
1070
+ (chan
1071
+ ? `<span class="sess-main"><span class="sess-chan">${escapeHtml(chan)}</span><span class="sess-title">${escapeHtml(s.title || 'Yeni Sohbet')}</span></span>`
1072
+ : `<span class="sess-title">${escapeHtml(s.title || 'Yeni Sohbet')}</span>`) +
1051
1073
  `<span class="sess-code" title="Oturum kodu">${escapeHtml(s.code || '')}</span>` +
1052
1074
  `<button class="sess-del" title="Sil">×</button>`;
1053
1075
  row.addEventListener('click', () => openSession(s.id));
@@ -12674,7 +12696,7 @@ if (els.mcpAddBtn) els.mcpAddBtn.addEventListener('click', () => {
12674
12696
  } catch {}
12675
12697
  });
12676
12698
 
12677
- /* ---------- MCP SOHBETİ (sağ panel): ana sohbetten AYRI oturum ----------
12699
+ /* ---------- MCP SOHBETİ (sol panel): ana sohbetten AYRI oturum ----------
12678
12700
  Oturum main'de bgTitle 'Beast MCP' ile yaşar; olaylar sessionId eşleşmesiyle
12679
12701
  yalnız bu panele akar (onEvent yönlendirmesi). Geçmiş seçici eski MCP
12680
12702
  sohbetlerini açar; '*' taze sohbet başlatır (eskisi geçmişte kalır). */
@@ -12906,7 +12928,7 @@ if (els.mcpChatInput) {
12906
12928
  });
12907
12929
  }
12908
12930
 
12909
- /* ---------- MCP sohbet/detay ayırıcı (sandbox deseni, sohbet SAĞDA) ----------
12931
+ /* ---------- MCP sohbet/detay ayırıcı (sandbox deseni, sohbet SOLDA) ----------
12910
12932
  Kaydedilen değer sohbet panelinin mcpRow içindeki payı (0-1). */
12911
12933
  let mcpSplitFrac = 0;
12912
12934
 
@@ -12933,10 +12955,10 @@ if (els.mcpSplit) {
12933
12955
  els.mcpSplit.addEventListener('mousedown', (e) => {
12934
12956
  e.preventDefault();
12935
12957
  els.mcpSplit.classList.add('dragging');
12936
- const rightEdge = els.mcpChat ? els.mcpChat.getBoundingClientRect().right : window.innerWidth;
12958
+ const leftEdge = els.mcpChat ? els.mcpChat.getBoundingClientRect().left : 0;
12937
12959
  const rowW = Math.max(600, els.mcpRow && els.mcpRow.clientWidth ? els.mcpRow.clientWidth : window.innerWidth - 250);
12938
12960
  const move = (ev) => {
12939
- const w = Math.max(300, Math.min(rightEdge - ev.clientX, rowW - 240));
12961
+ const w = Math.max(300, Math.min(ev.clientX - leftEdge, rowW - 240));
12940
12962
  mcpSplitFrac = Math.max(0.2, Math.min(w / rowW, 0.85));
12941
12963
  document.body.style.setProperty('--mcpSplit', Math.round(w) + 'px');
12942
12964
  };
@@ -234,6 +234,30 @@ body:not(.ide-mode):not(.studio-mode):not(.sandbox-mode) #watchPane { display: f
234
234
  align-items: center;
235
235
  justify-content: center;
236
236
  }
237
+ .sess-dc {
238
+ width: 15px;
239
+ height: 15px;
240
+ border-radius: 4px;
241
+ background: #5865f2;
242
+ color: #fff;
243
+ font-size: 10px;
244
+ font-weight: 800;
245
+ display: flex;
246
+ align-items: center;
247
+ justify-content: center;
248
+ }
249
+ /* kanal sohbeti etiketi: hangi numara/grup olduğu + mesaj başlığı alt satırda */
250
+ .sess-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 1px; }
251
+ .sess-chan {
252
+ font-size: 10.5px;
253
+ font-weight: 700;
254
+ color: var(--accent);
255
+ white-space: nowrap;
256
+ overflow: hidden;
257
+ text-overflow: ellipsis;
258
+ }
259
+ .sess-main .sess-title { font-size: 11.5px; color: var(--muted); }
260
+ .sess.active .sess-main .sess-title { color: var(--text); }
237
261
  .sess-title { flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-size: 13px; }
238
262
  .sess-code {
239
263
  color: var(--muted);
@@ -4765,7 +4789,7 @@ body.mcp-mode #mcpRow { display: flex; flex-direction: row; }
4765
4789
  white-space: pre;
4766
4790
  }
4767
4791
 
4768
- /* MCP sağ sohbet paneli: ana sohbetten TAMAMEN AYRI oturum (bgTitle 'Beast MCP') */
4792
+ /* MCP sol sohbet paneli: ana sohbetten TAMAMEN AYRI oturum (bgTitle 'Beast MCP') */
4769
4793
  #mcpChat {
4770
4794
  display: flex;
4771
4795
  flex-direction: column;