beast-agent 0.26.4 → 0.27.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": "0.26.4",
4
+ "version": "0.27.0",
5
5
  "description": "Ultra-fast local agent shell for Windows.",
6
6
  "author": "algokodcom (AlgoKod)",
7
7
  "license": "MIT",
@@ -19,6 +19,9 @@ const SUB_MAX_TURNS = 8;
19
19
  const HISTORY_TOKEN_BUDGET = 18000;
20
20
  const TOOL_OUT_KEEP = 1200;
21
21
  const USER_MAX = 8000;
22
+ /* observe() ile enjekte edilen bağlam mesajlarının öneki — ardışık bağlam
23
+ mesajlarının tek mesajda birleşmesi bu işaretle anlaşılır */
24
+ const OBSERVE_MARK = '[BAĞLAM';
22
25
 
23
26
  /* Oturum notları (geçici hafıza): eski mesajlar bütçeden düşülmeden önce
24
27
  önemli noktalar çıkarılır, sistem promptuna küçük blok olarak girer. */
@@ -1611,6 +1614,38 @@ class Engine {
1611
1614
  return true;
1612
1615
  }
1613
1616
 
1617
+ /* SESSİZ BAĞLAM ENJEKSİYONU: metin oturum geçmişine düşer ama _run()
1618
+ TETİKLENMEZ — ajan "görür", cevap ÜRETMEZ. WhatsApp gruplarında
1619
+ @mention moduyla kullanılır: grubun tüm konuşması bağlam olarak
1620
+ birikir, bot yalnız mention gelince konuşur. Ardışık bağlam
1621
+ mesajları işaret önekli tek user mesajında birleşir (bazı sağlayıcılar
1622
+ art arda user mesajını reddettiği için). */
1623
+ observe(sessionId, text) {
1624
+ const s = this._load(String(sessionId));
1625
+ if (!s) return false;
1626
+ const body = String(text || '').slice(0, USER_MAX);
1627
+ if (!body.trim()) return false;
1628
+ const lastMsg = s.messages[s.messages.length - 1];
1629
+ if (
1630
+ lastMsg && lastMsg.role === 'user' &&
1631
+ typeof lastMsg.content === 'string' &&
1632
+ lastMsg.content.startsWith(OBSERVE_MARK)
1633
+ ) {
1634
+ /* önceki bağlam mesajının devamı — tek mesajda birleştir */
1635
+ lastMsg.content = (lastMsg.content + '\n' + body).slice(0, USER_MAX);
1636
+ this._rewriteLastMsg(s.id, lastMsg.content);
1637
+ } else {
1638
+ const msg = { role: 'user', content: body };
1639
+ s.messages.push(msg);
1640
+ try {
1641
+ this._append(s, msg);
1642
+ } catch {}
1643
+ }
1644
+ this.emit({ type: 'observe', sessionId: s.id });
1645
+ this.emit({ type: 'sessions' });
1646
+ return true;
1647
+ }
1648
+
1614
1649
  /* #14 paralel ajanlar: ANA oturumu hiç bloklamayan arka plan çalıştırıcı.
1615
1650
  Ayrı gizli oturum açar, işi orada koşturur; bitince özet bildirim
1616
1651
  istenen sohbete düşer. Birden fazla iş aynı anda paralel koşabilir —
@@ -3850,3 +3885,4 @@ module.exports.BG_HIDDEN_TOOLS = BG_HIDDEN_TOOLS;
3850
3885
  module.exports.PERM_TOOL_SETS = PERM_TOOL_SETS;
3851
3886
  module.exports.PERM_LEVELS = PERM_LEVELS;
3852
3887
  module.exports.normalizePerms = normalizePerms;
3888
+ module.exports.OBSERVE_MARK = OBSERVE_MARK;
@@ -337,8 +337,20 @@ class WhatsAppBridge {
337
337
  async _processIncoming(msg) {
338
338
  try {
339
339
  const jid = msg.key && msg.key.remoteJid;
340
- if (!jid || msg.key.fromMe) return;
340
+ if (!jid) return;
341
341
  if (jid === 'status@broadcast' || jid.endsWith('@newsletter')) return;
342
+ /* botun kendi kimliği: telefon base'i + LID base'i (LID dönemi) */
343
+ const meBase = String(this._userIdRaw || '').split('@')[0].split(':')[0];
344
+ const lidBase = String((this.sock && this.sock.user && this.sock.user.lid) || '').split('@')[0].split(':')[0];
345
+ /* KENDİ KENDİNE SOHBET (Saved Messages): kullanıcı kendi numarasını izinliye
346
+ ekleyip kendi sohbetinden bota yazabilsin. Bu sohbetteki HER mesaj fromMe
347
+ gelir; botun KENDİ gönderdiği mesajlar (_tracked) filtrelenir → döngü yok.
348
+ Diğer sohbetlerdeki elle yazılan fromMe mesajları bot etkilenmesin diye yutulur. */
349
+ const selfChat = !!(meBase && (jid === meBase + '@s.whatsapp.net' || (lidBase && jid === lidBase + '@lid')));
350
+ if (msg.key.fromMe) {
351
+ if (this._tracked.has(String(msg.key.id))) return; // botun kendi cevabı — echo
352
+ if (!selfChat) return;
353
+ }
342
354
  const isGroup = jid.endsWith('@g.us');
343
355
  const participantJid = isGroup && msg.key.participant ? String(msg.key.participant) : '';
344
356
  const mm = msg.message || {};
@@ -351,20 +363,35 @@ class WhatsAppBridge {
351
363
  const t = String(text || '').trim();
352
364
 
353
365
  // gruplarda bot'un kendisi @mention edilmiş mi
366
+ /* LİD DÖNEMİ UYARISI: mentionedJid artık çoğu zaman botun @lid numarasıyla
367
+ gelir (telefon JID'i DEĞİL) — bu yüzden yalnız meBase ile karşılaştırma
368
+ mention'ı ıskalıyordu. Şimdi: botun telefon base'i + LID base'i ikisiyle
369
+ de eşitlik aranır; contextInfo görsel/video/belge caption'larından da alınır. */
354
370
  let mentioned = false;
355
371
  if (isGroup) {
356
- const ci = mm.extendedTextMessage && mm.extendedTextMessage.contextInfo;
372
+ const ci =
373
+ (mm.extendedTextMessage && mm.extendedTextMessage.contextInfo) ||
374
+ (mm.imageMessage && mm.imageMessage.contextInfo) ||
375
+ (mm.videoMessage && mm.videoMessage.contextInfo) ||
376
+ (mm.documentMessage && mm.documentMessage.contextInfo) ||
377
+ null;
357
378
  const men = (ci && ci.mentionedJid) || [];
358
- const meBase = String(this._userIdRaw || '').split('@')[0].split(':')[0];
379
+ const baseOf = (j) => String(j || '').split('?')[0].split(':')[0].split('@')[0];
359
380
  for (const m of men) {
360
- if (meBase && String(m).split('?')[0].split(':')[0].includes(meBase)) {
381
+ const base = baseOf(m);
382
+ if (base && ((meBase && base === meBase) || (lidBase && base === lidBase))) {
361
383
  mentioned = true;
362
384
  break;
363
385
  }
364
386
  }
365
- if (!mentioned && ci && ci.participant && meBase && String(ci.participant).includes(meBase)) {
366
- mentioned = true; // mesajımıza reply verilmiş
387
+ if (!mentioned && ci && ci.participant) {
388
+ const pBase = baseOf(ci.participant);
389
+ if ((meBase && pBase === meBase) || (lidBase && pBase === lidBase)) {
390
+ mentioned = true; // mesajımıza reply verilmiş
391
+ }
367
392
  }
393
+ /* metin yedeği: @<bot numarası> elle yazılmışsa da mention sayılır */
394
+ if (!mentioned && meBase && t.includes('@' + meBase)) mentioned = true;
368
395
  }
369
396
 
370
397
  // medya çıkarımı (varsa)
@@ -406,6 +433,9 @@ class WhatsAppBridge {
406
433
  senderNum = (participantJid || jid).split('@')[0].split(':')[0];
407
434
  if (!/^\d+$/.test(senderNum)) senderNum = '';
408
435
  }
436
+ /* kendi kendine sohbette gönderen = botun telefon numarası — izin listesi
437
+ telefon numarasıyla eşleşsin (LID self-chat'te jid @lid olur) */
438
+ if (selfChat && meBase) senderNum = meBase;
409
439
  this.onIncoming(jid, {
410
440
  text: t,
411
441
  media,
package/src/main.js CHANGED
@@ -7,7 +7,7 @@ const http = require('http');
7
7
  const dns = require('dns');
8
8
  const crypto = require('crypto');
9
9
  const { spawn } = require('child_process');
10
- const Engine = require('./agent/engine');
10
+ const { Engine, OBSERVE_MARK } = require('./agent/engine');
11
11
  const { loadBeastConfig, beastDir } = require('./agent/config');
12
12
  const bots = require('./agent/bots');
13
13
  const mqueue = require('./agent/mqueue');
@@ -1199,6 +1199,75 @@ async function waFlush(jid) {
1199
1199
  }
1200
1200
  }
1201
1201
 
1202
+ /* Grup göndereninin kimlik etiketi + SAHİP olup olmadığı.
1203
+ Sıra: izin listesindeki isim → @kullanıcı adı → gerçek PN → LID base.
1204
+ hitP.owner → bu kişi izin listesinde SAHİP olarak işaretli. */
1205
+ function waGroupSenderInfo(payload) {
1206
+ const pnDigits = String(payload.participantPn || '').split('@')[0].split(':')[0];
1207
+ const hitP = /^\d+$/.test(pnDigits) ? waFind(pnDigits) : null;
1208
+ const uname = String(payload.participantUsername || '').trim();
1209
+ let label;
1210
+ if (hitP && hitP.name) label = `${hitP.name} (+${pnDigits})`;
1211
+ else if (uname) label = `@${uname}${/^\d+$/.test(pnDigits) ? ' (+' + pnDigits + ')' : ''}`;
1212
+ else if (/^\d+$/.test(pnDigits)) label = '+' + pnDigits;
1213
+ else label = payload.participant ? '+' + String(payload.participant).split('@')[0].split(':')[0] : '';
1214
+ return { label, name: (hitP && hitP.name) || '', isOwner: !!(hitP && hitP.owner) };
1215
+ }
1216
+
1217
+ /* Oturum yoksa oluştur (processWaMessage ile aynı kalıp) */
1218
+ function ensureWaSession(jid) {
1219
+ let sid = waChats.get(jid);
1220
+ if (!sid) {
1221
+ const v = engine.createSession();
1222
+ sid = v.id;
1223
+ waChats.set(jid, sid);
1224
+ saveWaChats();
1225
+ if (wa) wa.setWatchJids([...waChats.keys()]);
1226
+ } else {
1227
+ waRememberSession(jid, sid);
1228
+ }
1229
+ return sid;
1230
+ }
1231
+
1232
+ /* ---------- GRUP BAĞLAM AKIŞI (mentionOnly + seeAll) ----------
1233
+ Bot grubun tüm konuşmasını okur ama CEVAP ÜRETMEZ; mesajlar sessizce
1234
+ oturum geçmişine bağlam olarak düşer. @mention gelince bot tüm bu
1235
+ bağlamı görerek konuşur. Anti-spam: aynı birleştirme penceresi. */
1236
+ const waCtxQueue = new Map(); // jid -> { timer, payloads[] }
1237
+
1238
+ function waGroupObserve(jid, payload, senderNum) {
1239
+ let q = waCtxQueue.get(jid);
1240
+ if (!q) {
1241
+ q = { payloads: [] };
1242
+ waCtxQueue.set(jid, q);
1243
+ }
1244
+ q.payloads.push({ payload, senderNum });
1245
+ clearTimeout(q.timer);
1246
+ waLog(`grup bağlam: kuyruğa girdi jid=${waPrettyJid(jid)} toplam=${q.payloads.length}`);
1247
+ q.timer = setTimeout(() => {
1248
+ waCtxFlush(jid).catch((e) => waLog(`ctx flush KRASİ: ${String((e && e.stack) || e)}`));
1249
+ }, WA_DEBOUNCE_MS);
1250
+ }
1251
+
1252
+ async function waCtxFlush(jid) {
1253
+ const q = waCtxQueue.get(jid);
1254
+ if (!q) return;
1255
+ waCtxQueue.delete(jid);
1256
+ const sid = ensureWaSession(jid);
1257
+ /* her satır gönderen etiketli olur — ajan kimin ne yazdığını izleyebilsin */
1258
+ const lines = [];
1259
+ for (const { payload } of q.payloads) {
1260
+ if (!payload || !payload.text) continue;
1261
+ const gi = waGroupSenderInfo(payload);
1262
+ const who = gi.label || '?';
1263
+ lines.push(`${who}: ${String(payload.text).slice(0, 1500)}`);
1264
+ }
1265
+ if (!lines.length) return;
1266
+ const text = `${OBSERVE_MARK} — WhatsApp grup konuşması (cevap verme, sadece bilgi olarak sakla)]\n` + lines.join('\n').slice(0, 6000);
1267
+ engine.observe(sid, text);
1268
+ waLog(`grup bağlam: oturuma işlendi sid=${sid} satır=${lines.length}`);
1269
+ }
1270
+
1202
1271
  /* ---------- WA'dan skill kurulumu ----------
1203
1272
  SKILL.md (veya *.skill.md) belgesi atılırsa skills altına kurulur. */
1204
1273
 
@@ -1549,7 +1618,15 @@ async function handleWaIncoming(jid, payload, senderNum) {
1549
1618
  if (isGroup) {
1550
1619
  const g = settings.waGroups || {};
1551
1620
  if (!g.enabled) return;
1552
- if (g.mentionOnly !== false && !payload.mentioned) return;
1621
+ const mentionMode = g.mentionOnly !== false;
1622
+ /* MENTION MODU + seeAll: bot grubun TÜM konuşmasını BAĞLAM olarak görür
1623
+ ama yalnız @mention'da cevap üretir. seeAll VARSAYILAN KAPALI —
1624
+ mention'sız mesajlar tamamen yutulur (gizlilik). */
1625
+ if (mentionMode && !payload.mentioned && g.seeAll) {
1626
+ waGroupObserve(jid, payload, senderNum);
1627
+ return;
1628
+ }
1629
+ if (mentionMode && !payload.mentioned) return;
1553
1630
  waLog(`grup mesajı jid=${waPrettyJid(jid)} participant=+${senderNum || '?'} mention=${!!payload.mentioned}`);
1554
1631
  } else {
1555
1632
  const hit = waFind(senderNum);
@@ -1725,22 +1802,21 @@ async function processWaMessage(jid, payload, senderNum, requeues = 0) {
1725
1802
  Kimlik çözümleme sırası: izin listesindeki isim → @kullanıcı adı → gerçek PN → LID.
1726
1803
  Ajan böylece grubun içinde mesajın KİMDEN geldiğini görür. */
1727
1804
  let groupSender = '';
1805
+ let groupSenderOwner = false;
1728
1806
  if (isGroup) {
1729
- const pnDigits = String(payload.participantPn || '').split('@')[0].split(':')[0];
1730
- const hitP = /^\d+$/.test(pnDigits) ? waFind(pnDigits) : null;
1731
- const uname = String(payload.participantUsername || '').trim();
1732
- if (hitP && hitP.name) groupSender = `${hitP.name} (+${pnDigits})`;
1733
- else if (uname) groupSender = `@${uname}${/^\d+$/.test(pnDigits) ? ' (+' + pnDigits + ')' : ''}`;
1734
- else if (/^\d+$/.test(pnDigits)) groupSender = '+' + pnDigits;
1735
- else groupSender = participantName || '';
1807
+ const gi = waGroupSenderInfo(payload);
1808
+ groupSender = gi.label || participantName;
1809
+ groupSenderOwner = gi.isOwner;
1736
1810
  }
1737
- /* #v13.1 rol: SAHİP vs MİSAFİR — ajan kime konuştuğunu net bilsin */
1811
+ /* #v13.1 rol: SAHİP vs MİSAFİR — ajan kime konuştuğunu net bilsin.
1812
+ GRUPLARDA DA: gönderen izin listesinde SAHİP olarak işaretliyse ajan
1813
+ bunu görür — sahibinin grup içi talepleri misafir sözünden önceliklidir. */
1738
1814
  const isOwner = !isGroup && !!hit.owner;
1739
- const roleTag = isOwner
1815
+ const roleTag = isOwner || groupSenderOwner
1740
1816
  ? 'SAHİBİN (talepleri önceliklidir)'
1741
1817
  : 'MİSAFİR (izinli ama sahibin sözü önceliklidir)';
1742
1818
  const label = isGroup
1743
- ? `Grup ${jid.split('@')[0]}${groupSender ? ' — gönderen: ' + groupSender : ''}`
1819
+ ? `Grup ${jid.split('@')[0]}${groupSender ? ` — gönderen: ${groupSender} — ${groupSenderOwner ? 'SAHİBİN' : 'MİSAFİR (grup üyesi)'}` : ''}`
1744
1820
  : hit.name
1745
1821
  ? `${hit.name} (+${senderNum || '?'}) — ${roleTag}`
1746
1822
  : `+${senderNum || '?'} — ${roleTag}`;
@@ -1748,7 +1824,9 @@ async function processWaMessage(jid, payload, senderNum, requeues = 0) {
1748
1824
  if (!isGroup && !isOwner) {
1749
1825
  text += `\n[NOT: Bu kişi SAHİP DEĞİL, misafirdir. Sahibin ayarlarını/verilerini değiştirme; kalıcı hafızaya misafire özel bilgi yazma.]`;
1750
1826
  } else if (isGroup) {
1751
- text += `\n[NOT: Grup mesajı — gönderen grubun üyesidir, izin listendeki kişi olmayabilir. Grup üyelerine karşı temkinli konuş.]`;
1827
+ text += groupSenderOwner
1828
+ ? `\n[NOT: Grup mesajı ama gönderen SAHİBİN — talepleri önceliklidir, misafir gibi temkinli konuşmana gerek yok.]`
1829
+ : `\n[NOT: Grup mesajı — gönderen grubun üyesidir, izin listendeki kişi olmayabilir. Grup üyelerine karşı temkinli konuş.]`;
1752
1830
  }
1753
1831
  const attachments = [];
1754
1832
 
@@ -4551,12 +4629,16 @@ ipcMain.handle('wherewasi:set', (_e, cfg) => {
4551
4629
  return settings.whereWasI;
4552
4630
  });
4553
4631
 
4554
- /* WhatsApp grup ayarı: { enabled, mentionOnly } */
4555
- ipcMain.handle('wa:groups:get', () => settings.waGroups || { enabled: false, mentionOnly: true });
4632
+ /* WhatsApp grup ayarı: { enabled, mentionOnly, seeAll }
4633
+ seeAll: yalnız mentionOnly modunda anlamlı bot grubun TÜM konuşmasını
4634
+ bağlam olarak okur ama yine SADECE @mention'da cevap verir.
4635
+ VARSAYILAN KAPALI: herkesin konuşmasını görmesi istenmeyebilir. */
4636
+ ipcMain.handle('wa:groups:get', () => settings.waGroups || { enabled: false, mentionOnly: true, seeAll: false });
4556
4637
  ipcMain.handle('wa:groups:set', (_e, cfg) => {
4557
4638
  settings.waGroups = {
4558
4639
  enabled: !!(cfg && cfg.enabled),
4559
4640
  mentionOnly: !(cfg && cfg.mentionOnly === false),
4641
+ seeAll: !!(cfg && cfg.seeAll),
4560
4642
  };
4561
4643
  saveSettings();
4562
4644
  return settings.waGroups;
@@ -195,8 +195,10 @@
195
195
  it_connect: 'QR ile Bağlan',
196
196
  it_disconnect: 'Kes',
197
197
  it_reset_pair: 'Eşlemeyi Sıfırla',
198
- it_groups_on: 'WhatsApp gruplarında çalış — bot @mention edilince cevap verir',
199
- it_groups_all: 'Mention şart değil: grup içindeki her mesaja karış',
198
+ it_groups_on: 'WhatsApp gruplarında çalış',
199
+ it_groups_mention: 'Sadece @mention edilince cevap ver (grupta botu etiketle)',
200
+ it_groups_seeall: 'Mention modunda grubun TÜM konuşmalarını oku (bağlam biriktirir, yine sadece @mention\'a cevap verir)',
201
+ it_groups_all: 'Grubun her mesajına karış',
200
202
  it_allow_label: 'İzinli numaralar',
201
203
  it_name_ph: 'İsim (zorunlu)',
202
204
  it_num_ph: 'Numara 905xxxxxxxxx',
@@ -723,8 +725,10 @@
723
725
  it_connect: 'Connect via QR',
724
726
  it_disconnect: 'Disconnect',
725
727
  it_reset_pair: 'Reset pairing',
726
- it_groups_on: 'Work in WhatsApp groups — bot replies when @mentioned',
727
- it_groups_all: 'Mention not required: respond to every group message',
728
+ it_groups_on: 'Work in WhatsApp groups',
729
+ it_groups_mention: 'Reply only when @mentioned (tag the bot in the group)',
730
+ it_groups_seeall: 'In mention mode, read ALL group messages as context (still replies only when @mentioned)',
731
+ it_groups_all: 'Respond to every group message',
728
732
  it_allow_label: 'Allowed numbers',
729
733
  it_name_ph: 'Name (required)',
730
734
  it_num_ph: 'Number 905xxxxxxxxx',
@@ -1473,7 +1473,7 @@ const WA_STATUS_TEXT = {
1473
1473
 
1474
1474
  async function renderIntegrationsPane() {
1475
1475
  const pane = $('#tab-integrations');
1476
- const g = await beast.waGetGroups().catch(() => ({ enabled: false, mentionOnly: true }));
1476
+ const g = await beast.waGetGroups().catch(() => ({ enabled: false, mentionOnly: true, seeAll: false }));
1477
1477
  pane.innerHTML =
1478
1478
  '<h2>' + _t('it_h2') + '</h2><div class="sub">' + _t('it_sub') + '</div>' +
1479
1479
  `<div class="wa-card">
@@ -1498,7 +1498,9 @@ async function renderIntegrationsPane() {
1498
1498
  </div>
1499
1499
  <div style="height:14px"></div>
1500
1500
  <label class="lock-row"><input type="checkbox" id="waGroupsOn" ${g.enabled ? 'checked' : ''}/><span>${_t('it_groups_on')}</span></label>
1501
- <label class="lock-row" style="${g.enabled ? '' : 'opacity:.45'}"><input type="checkbox" id="waGroupsAll" ${g.mentionOnly === false ? 'checked' : ''}/><span>${_t('it_groups_all')}</span></label>
1501
+ <label class="lock-row" style="${g.enabled ? '' : 'opacity:.45'}"><input type="radio" name="waGroupMode" id="waGroupsMention" ${g.mentionOnly !== false ? 'checked' : ''}/><span>${_t('it_groups_mention')}</span></label>
1502
+ <label class="lock-row" style="${g.enabled ? '' : 'opacity:.45'}"><input type="radio" name="waGroupMode" id="waGroupsAll" ${g.mentionOnly === false ? 'checked' : ''}/><span>${_t('it_groups_all')}</span></label>
1503
+ <label class="lock-row" id="waSeeAllRow" style="${g.enabled && g.mentionOnly !== false ? '' : 'opacity:.45'}"><input type="checkbox" id="waGroupsSeeAll" ${g.seeAll ? 'checked' : ''}/><span>${_t('it_groups_seeall')}</span></label>
1502
1504
  <div class="divider"></div>
1503
1505
  <label class="mem-label" style="margin-top:0">${_t('it_allow_label')}</label>
1504
1506
  <div id="waAllowChips" class="chips-inline"></div>
@@ -1571,16 +1573,44 @@ async function renderIntegrationsPane() {
1571
1573
  </div>`;
1572
1574
 
1573
1575
  const waGroupsOn = $('#waGroupsOn');
1576
+ const waGroupsMention = $('#waGroupsMention');
1574
1577
  const waGroupsAll = $('#waGroupsAll');
1578
+ const waGroupsSeeAll = $('#waGroupsSeeAll');
1579
+ const waSeeAllRow = $('#waSeeAllRow');
1580
+ /* iki mod BİRBİRİNE HARIÇTİR: ya @mention ya her mesaja karışma (radio)
1581
+ seeAll yalnız mention modunda anlamlı — o mod seçiliyken aktifleşir */
1582
+ const syncGroupMode = () => {
1583
+ const op = waGroupsOn.checked ? '' : '.45';
1584
+ if (waGroupsMention) waGroupsMention.parentElement.style.opacity = op;
1585
+ if (waGroupsAll) waGroupsAll.parentElement.style.opacity = op;
1586
+ if (waSeeAllRow) waSeeAllRow.style.opacity = waGroupsOn.checked && waGroupsMention && waGroupsMention.checked ? '' : '.45';
1587
+ };
1575
1588
  const saveGroups = async () => {
1576
- const r = await beast.waSetGroups({ enabled: waGroupsOn.checked, mentionOnly: !waGroupsAll.checked });
1577
- toast(r.enabled ? (r.mentionOnly ? 'Gruplar açık — @mention bekler' : 'Gruplar açık — tüm mesajlara karışır') : 'Gruplar kapalı');
1589
+ const r = await beast.waSetGroups({ enabled: waGroupsOn.checked, mentionOnly: !waGroupsAll.checked, seeAll: !!(waGroupsSeeAll && waGroupsSeeAll.checked) });
1590
+ toast(
1591
+ r.enabled
1592
+ ? r.mentionOnly
1593
+ ? r.seeAll
1594
+ ? 'Gruplar açık — tüm konuşmalar bağlam olarak okunuyor, sadece @mention\'a cevap verir'
1595
+ : 'Gruplar açık — @mention bekler'
1596
+ : 'Gruplar açık — tüm mesajlara karışır'
1597
+ : 'Gruplar kapalı'
1598
+ );
1578
1599
  };
1579
1600
  waGroupsOn.addEventListener('change', async () => {
1580
- waGroupsAll.parentElement.style.opacity = waGroupsOn.checked ? '' : '.45';
1601
+ syncGroupMode();
1581
1602
  await saveGroups();
1582
1603
  });
1583
- waGroupsAll.addEventListener('change', saveGroups);
1604
+ waGroupsMention.addEventListener('change', () => {
1605
+ syncGroupMode();
1606
+ saveGroups();
1607
+ });
1608
+ waGroupsAll.addEventListener('change', () => {
1609
+ syncGroupMode();
1610
+ saveGroups();
1611
+ });
1612
+ if (waGroupsSeeAll) waGroupsSeeAll.addEventListener('change', saveGroups);
1613
+ syncGroupMode();
1584
1614
 
1585
1615
  $('#waStartBtn').addEventListener('click', async () => {
1586
1616
  toast('Bağlanıyor…');
@@ -113,28 +113,91 @@ test('eski görseller metne indirilir, son mesajın görseli korunur', () => {
113
113
  assert.ok(Array.isArray(lastUser.content));
114
114
  });
115
115
 
116
- /* ---------- oturum notlar� (ge�ici haf�za) ---------- */
116
+ /* ---------- oturum notlar� (ge�ici haf�za) ---------- */
117
117
 
118
- test('oturum notlar�: dosyadan y�klenir, mesaj listesine kar��maz', () => {
118
+ test('oturum notlar�: dosyadan y�klenir, mesaj listesine kar��maz', () => {
119
119
  const eng = makeEngine();
120
120
  const id = eng.createSession().id;
121
121
  for (let i = 0; i < 5; i++) {
122
122
  fs.appendFileSync(eng._file(id), JSON.stringify({ t: 'msg', role: 'user', content: 'mesaj ' + i }) + '\n');
123
123
  }
124
- fs.appendFileSync(eng._file(id), JSON.stringify({ t: 'notes', text: 'hedef: v7 �al�smas�', at: 3 }) + '\n');
125
- eng.cache.delete(id); // diskten taze y�kle
124
+ fs.appendFileSync(eng._file(id), JSON.stringify({ t: 'notes', text: 'hedef: v7 �al�smas�', at: 3 }) + '\n');
125
+ eng.cache.delete(id); // diskten taze y�kle
126
126
  const s = eng._load(id);
127
- assert.equal(s.notes, 'hedef: v7 �al�smas�');
127
+ assert.equal(s.notes, 'hedef: v7 �al�smas�');
128
128
  assert.equal(s.notesAt, 3);
129
129
  assert.equal(s.messages.length, 5);
130
130
  });
131
131
 
132
- test('oturum notlar� varken payload s�k�la��r', () => {
133
- const eng = makeEngine({ historyTokenBudget: 100000 });
134
- const msgs = [];
135
- for (let i = 0; i < 30; i++) msgs.push({ role: 'user', content: 'kisa mesaj #' + i });
136
- const withoutNotes = eng._buildPayload('sys', msgs, null);
137
- const withNotes = eng._buildPayload('sys', msgs, '�zet: test');
138
- assert.ok(withNotes.length < withoutNotes.length);
139
- assert.equal(withNotes[withNotes.length - 1].content, 'kisa mesaj #29'); // son mesaj hep kal�r
132
+ test('oturum notlar� varken payload s�k�la��r', () => {
133
+ const eng = makeEngine({ historyTokenBudget: 100000 });
134
+ const msgs = [];
135
+ for (let i = 0; i < 30; i++) msgs.push({ role: 'user', content: 'kisa mesaj #' + i });
136
+ const withoutNotes = eng._buildPayload('sys', msgs, null);
137
+ const withNotes = eng._buildPayload('sys', msgs, '�zet: test');
138
+ assert.ok(withNotes.length < withoutNotes.length);
139
+ assert.equal(withNotes[withNotes.length - 1].content, 'kisa mesaj #29'); // son mesaj hep kal�r
140
+ });
141
+
142
+ /* ---------- observe(): sessiz bağlam enjeksiyonu ---------- */
143
+
144
+ test('observe: mesaj geçmişe düşer ama run tetiklenmez', async () => {
145
+ const events = [];
146
+ const eng = makeEngine({ emit: (ev) => events.push(ev) });
147
+ const id = eng.createSession().id;
148
+ const ok = eng.observe(id, '[BAĞLAM — grup] ali: selam');
149
+ assert.ok(ok);
150
+ const s = eng._load(id);
151
+ assert.equal(s.messages.length, 1);
152
+ assert.equal(s.messages[0].role, 'user');
153
+ assert.ok(s.messages[0].content.startsWith('[BAĞLAM'));
154
+ // _run tetiklenmedi: LLM hatası/done olayı yok
155
+ await new Promise((r) => setTimeout(r, 60));
156
+ assert.ok(!events.some((e) => e.type === 'error' || e.type === 'done'));
157
+ // observe olayı yayıldı
158
+ assert.ok(events.some((e) => e.type === 'observe' && e.sessionId === id));
159
+ // boş metin false
160
+ assert.equal(eng.observe(id, ' '), false);
161
+ });
162
+
163
+ test('observe: ardışık bağlam mesajları tek mesajda birleşir', () => {
164
+ const eng = makeEngine();
165
+ const id = eng.createSession().id;
166
+ eng.observe(id, '[BAĞLAM — grup] ali: selam');
167
+ eng.observe(id, '[BAĞLAM — grup] ayşe: nasılsın');
168
+ const s = eng._load(id);
169
+ assert.equal(s.messages.length, 1);
170
+ assert.ok(s.messages[0].content.includes('ali: selam'));
171
+ assert.ok(s.messages[0].content.includes('ayşe: nasılsın'));
172
+ // dosyaya da tek satır yazıldı
173
+ const lines = fs.readFileSync(eng._file(id), 'utf8').split('\n').filter((l) => l.trim());
174
+ const msgLines = lines.filter((l) => { try { return JSON.parse(l).t === 'msg'; } catch { return false; } });
175
+ assert.equal(msgLines.length, 1);
140
176
  });
177
+
178
+ test('observe: normal user mesajıyla birleşmez', () => {
179
+ const eng = makeEngine();
180
+ const id = eng.createSession().id;
181
+ const s = eng._load(id);
182
+ s.messages.push({ role: 'user', content: 'gerçek soru' });
183
+ fs.appendFileSync(eng._file(id), JSON.stringify({ t: 'msg', role: 'user', content: 'gerçek soru' }) + '\n');
184
+ eng.observe(id, '[BAĞLAM — grup] ali: selam');
185
+ assert.equal(s.messages.length, 2);
186
+ assert.equal(s.messages[0].content, 'gerçek soru');
187
+ assert.ok(s.messages[1].content.startsWith('[BAĞLAM'));
188
+ });
189
+
190
+ test('send: mention mesajı bekleyen bağlam mesajıyla birleşir ve run başlar', async () => {
191
+ const events = [];
192
+ const eng = makeEngine({ emit: (ev) => events.push(ev) });
193
+ const id = eng.createSession().id;
194
+ eng.observe(id, '[BAĞLAM — grup] ali: selam\nayşe: nasılsın');
195
+ eng.send(id, { text: '@beast bana bugün hava nasıl?' });
196
+ const s = eng._load(id);
197
+ assert.equal(s.messages.length, 1);
198
+ assert.ok(s.messages[0].content.includes('ayşe: nasılsın'));
199
+ assert.ok(s.messages[0].content.includes('@beast bana bugün hava nasıl?'));
200
+ // run başladı → sağlayıcı yoksa hata olayı düşer (tetiklenme kanıtı)
201
+ await new Promise((r) => setTimeout(r, 120));
202
+ assert.ok(events.some((e) => e.type === 'error' || e.type === 'done'));
203
+ });