beast-agent 0.26.5 → 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.5",
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;
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;
@@ -197,6 +197,7 @@
197
197
  it_reset_pair: 'Eşlemeyi Sıfırla',
198
198
  it_groups_on: 'WhatsApp gruplarında çalış',
199
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)',
200
201
  it_groups_all: 'Grubun her mesajına karış',
201
202
  it_allow_label: 'İzinli numaralar',
202
203
  it_name_ph: 'İsim (zorunlu)',
@@ -726,6 +727,7 @@
726
727
  it_reset_pair: 'Reset pairing',
727
728
  it_groups_on: 'Work in WhatsApp groups',
728
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)',
729
731
  it_groups_all: 'Respond to every group message',
730
732
  it_allow_label: 'Allowed numbers',
731
733
  it_name_ph: 'Name (required)',
@@ -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">
@@ -1500,6 +1500,7 @@ async function renderIntegrationsPane() {
1500
1500
  <label class="lock-row"><input type="checkbox" id="waGroupsOn" ${g.enabled ? 'checked' : ''}/><span>${_t('it_groups_on')}</span></label>
1501
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
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>
1503
1504
  <div class="divider"></div>
1504
1505
  <label class="mem-label" style="margin-top:0">${_t('it_allow_label')}</label>
1505
1506
  <div id="waAllowChips" class="chips-inline"></div>
@@ -1574,22 +1575,41 @@ async function renderIntegrationsPane() {
1574
1575
  const waGroupsOn = $('#waGroupsOn');
1575
1576
  const waGroupsMention = $('#waGroupsMention');
1576
1577
  const waGroupsAll = $('#waGroupsAll');
1577
- /* iki mod BİRBİRİNE HARIÇTİR: ya @mention ya her mesaja karışma (radio) */
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 */
1578
1582
  const syncGroupMode = () => {
1579
1583
  const op = waGroupsOn.checked ? '' : '.45';
1580
1584
  if (waGroupsMention) waGroupsMention.parentElement.style.opacity = op;
1581
1585
  if (waGroupsAll) waGroupsAll.parentElement.style.opacity = op;
1586
+ if (waSeeAllRow) waSeeAllRow.style.opacity = waGroupsOn.checked && waGroupsMention && waGroupsMention.checked ? '' : '.45';
1582
1587
  };
1583
1588
  const saveGroups = async () => {
1584
- const r = await beast.waSetGroups({ enabled: waGroupsOn.checked, mentionOnly: !waGroupsAll.checked });
1585
- 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
+ );
1586
1599
  };
1587
1600
  waGroupsOn.addEventListener('change', async () => {
1588
1601
  syncGroupMode();
1589
1602
  await saveGroups();
1590
1603
  });
1591
- waGroupsMention.addEventListener('change', saveGroups);
1592
- 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);
1593
1613
  syncGroupMode();
1594
1614
 
1595
1615
  $('#waStartBtn').addEventListener('click', async () => {
@@ -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
+ });