beast-agent 2.16.0 → 2.18.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.16.0",
4
+ "version": "2.18.0",
5
5
  "description": "Ultra-fast local agent shell for Windows.",
6
6
  "author": "algokodcom (AlgoKod)",
7
7
  "license": "MIT",
@@ -17,6 +17,7 @@ const research = require('./research');
17
17
  const agentdefs = require('./agentdefs');
18
18
  const memory = require('./memory');
19
19
  const mem0 = require('./mem0');
20
+ const supermemory = require('./supermemory');
20
21
  const nightref = require('./nightref');
21
22
  const skills = require('./skills');
22
23
  const mcp = require('./mcp');
@@ -275,6 +276,14 @@ class Engine {
275
276
  /* mem0-native hafıza katmanı: semantik arama + LLM konsolidasyonu (bot başına izole store).
276
277
  Ayarlardan kapatılabilir (mem0:false) — kapalıysa klasik keyword hafızası çalışır. */
277
278
  this.mem0Enabled = opts.mem0 !== false;
279
+ /* Supermemory (lokal) bellek katmanı — DEFAULT AÇIK: sunucu ayaktaysa
280
+ (npx supermemory local → localhost:6767) hibrit arama + kullanıcı profili
281
+ öncelikli gelir; ayakta değilse klasik hafıza sorunsuz devralır.
282
+ Sağlık önbelleği: probe 60 sn'de bir tazelenir (spam probe yok). */
283
+ this.supermemory = opts.supermemory || null; // { enabled, baseUrl, apiKey, containerTag }
284
+ this._smUp = null; // null = henüz bilinmiyor
285
+ this._smCheckedAt = 0;
286
+ this._smWarned = false; // erişilemez iletisi bir kez basılır
278
287
  /* ANA KOD KİLİDİ: agent kendi kaynak koduna dokunamaz (yazma/silme yok, okuma serbest) */
279
288
  this.protectedDirs = (opts.protectedDirs || [])
280
289
  .map((d) => String(d).replace(/[\\/]+$/, '').toLowerCase().replace(/\//g, '\\'))
@@ -1588,6 +1597,74 @@ class Engine {
1588
1597
  }
1589
1598
  }
1590
1599
 
1600
+ /* ---------- Supermemory (lokal) bellek köprüsü ---------- */
1601
+
1602
+ _smConfig() {
1603
+ const sm = this.supermemory;
1604
+ if (!sm || sm.enabled === false || !sm.baseUrl) return null;
1605
+ return sm;
1606
+ }
1607
+
1608
+ /* Ayaktayken true — 60 sn cooldown'lu probe (aynı tura 10 kez probe vurmaz) */
1609
+ async _smReady() {
1610
+ const sm = this._smConfig();
1611
+ if (!sm) return false;
1612
+ const now = Date.now();
1613
+ if (this._smUp === true && now - this._smCheckedAt < supermemory.HEALTH_COOLDOWN_MS) return true;
1614
+ if (this._smUp === false && now - this._smCheckedAt < supermemory.HEALTH_COOLDOWN_MS) return false;
1615
+ this._smUp = await supermemory.probe(sm.baseUrl, sm.apiKey);
1616
+ this._smCheckedAt = now;
1617
+ if (!this._smUp && !this._smWarned) {
1618
+ this._smWarned = true;
1619
+ emitSafe(this, null, {
1620
+ type: 'status',
1621
+ status: '🧠 supermemory erişilemedi — klasik hafıza kullanılıyor (açmak için: npx supermemory local)',
1622
+ });
1623
+ }
1624
+ return this._smUp;
1625
+ }
1626
+
1627
+ /* Container tag: bot oturumları kendi izole havuzuna yazar (mem0 deseni) */
1628
+ _smContainer(sessionId) {
1629
+ const sm = this._smConfig();
1630
+ const bctx = this._sessionBotCtx(sessionId ? this.cache.get(String(sessionId)) : null);
1631
+ return (bctx && bctx.id ? 'bot:' + bctx.id : '') || (sm && sm.containerTag) || 'beast';
1632
+ }
1633
+
1634
+ /* YAZ: yerel hafıza HER ZAMEN yazılır (risksiz yedek); supermemory ASENKRON
1635
+ push edilir — turu bloklamaz, hata yutulur (mem0 deseninin aynısı) */
1636
+ _smPush(sessionId, text) {
1637
+ const sm = this._smConfig();
1638
+ if (!sm) return;
1639
+ supermemory
1640
+ .add(sm.baseUrl, sm.apiKey, { content: text, containerTag: this._smContainer(sessionId), metadata: { type: 'conversation' } })
1641
+ .catch(() => {});
1642
+ }
1643
+
1644
+ /* ARA: supermemory hibrit arama + profil → yerel sonuçlarla BİRLEŞTİR.
1645
+ Sunucu yoksa/hata yerse eski akış aynen çalışır (fallback). */
1646
+ async _smSearch(sessionId, q, limit) {
1647
+ const sm = this._smConfig();
1648
+ if (!sm || !(await this._smReady())) return null;
1649
+ const container = this._smContainer(sessionId);
1650
+ try {
1651
+ const [hits, prof] = await Promise.all([
1652
+ supermemory.search(sm.baseUrl, sm.apiKey, { q, containerTag: container, limit, mode: 'hybrid' }),
1653
+ supermemory.profile(sm.baseUrl, sm.apiKey, { containerTag: container }).catch(() => null),
1654
+ ]);
1655
+ const out = {
1656
+ engine: 'supermemory',
1657
+ results: hits.map((h) => ({ text: h.text, score: h.similarity, source: 'supermemory' })),
1658
+ };
1659
+ if (prof && (prof.static.length || prof.dynamic.length)) out.profile = prof;
1660
+ return out;
1661
+ } catch {
1662
+ this._smUp = false;
1663
+ this._smCheckedAt = Date.now();
1664
+ return null;
1665
+ }
1666
+ }
1667
+
1591
1668
  /* opencode request.ts prepare — system birleşimi (birebir sıra) */
1592
1669
  buildOcSystem(session, agentInfo) {
1593
1670
  const sid = String(session && session.id);
@@ -1753,6 +1830,11 @@ class Engine {
1753
1830
  : 'ROL: GÖZLEMCİ ANALİST 🔍 — tur tur piyasa tararsın ama OTOMATİK İŞLEM KAPALI: mt5_trade/mt5_pending KULLANMA. Fırsatları net öneri olarak yaz (sembol, yön, giriş, SL, TP, sebep); sahibi onaylarsa açılır.'
1754
1831
  : 'ROL: TRADING CO-PİLOT 💬 — sahibinle sohbet ediyorsun. Veri isterse mt5_* araçlarıyla çek, analiz et; işlem istenirse limitler içinde uygula, istenmezse sadece öneri ver.';
1755
1832
  const proj = this._projectInstructions(session);
1833
+ /* Skills kataloğu: finance oturumlarında da skill'ler çağrılabilir
1834
+ (skill aracı _chatTurn'de finance oturumlarına enjekte edilir). */
1835
+ const skList = (() => {
1836
+ try { return skills.scan(); } catch { return []; }
1837
+ })();
1756
1838
  return (
1757
1839
  'Sen BEAST FİNANS\u2019sın — bilgisayardaki MetaTrader 5 (MT5) terminaline köprüyle bağlı trading ajanı.\n' +
1758
1840
  'ORTAM: Beast Finance uygulamasındasın — solda sohbet geçmişi + izleyiciler, ortada bu sohbet, SAĞ panelde MT5 işlem panosu (hesap, pozisyonlar, piyasa, AKIŞ) var.\n' +
@@ -1769,6 +1851,10 @@ class Engine {
1769
1851
  '- Emin olmadığında İŞLEM YOK — "BEKLE: <sebep>" yaz. Sık işlem > iyi işlem.\n' +
1770
1852
  '- Hesap kaldıracı ve serbest marja göre pozisyon boyutunu düşük tut; tek işlemde serbest marjın büyük kısmını riske atma.\n' +
1771
1853
  (session && session.financeStrategy ? `SAHİBİNİN STRATEJİ NOTU (önceliklidir):\n${session.financeStrategy}\n` : '') +
1854
+ (skList.length
1855
+ ? '# SKILLS\nKurulu skill kataloğu: görev bir skill\u2019in tanımına uyarsa skill aracıyla gövdesini (SKILL.md) yükleyip uygula — tekerleği yeniden icat etme. Analysis/rapor/PDF gibi işlerde skill\u2019in prosedürüne uy.\n' +
1856
+ skList.map((s) => `- ${s.name}: ${s.description} [${s.path}]`).join('\n') + '\n'
1857
+ : '') +
1772
1858
  'RAPOR DİSİPLİNİ: kısa ve sayısal — sembol, yön, lot, giriş, SL/TP, sebep tek satırda. Uzun fal açma; tablo/liste kullanabilirsin.\n' +
1773
1859
  `Yerel zaman: ${localDate} ${localTime} — piyasa saatlerine dikkat et (borsa/metal seansları kapalıysa spread genişler; hafta sonu FX kapalıdır).\n` +
1774
1860
  (proj ? '# PROJE TALİMATLARI\n' + proj + '\n' : '') +
@@ -3325,6 +3411,16 @@ class Engine {
3325
3411
  toolsList = await mcp.mergeTools(toolsList);
3326
3412
  /* Beast Apps: kurulu app'lerin araçları (app__<id>__<tool>) modele açılır */
3327
3413
  toolsList = apps.mergeTools(toolsList);
3414
+ /* Beast Finance: skill aracı da açık — kataloğun SKILL.md gövdesi
3415
+ modele açılır (skill handler'ı zaten tüm oturumlar için çalışır,
3416
+ definition yalnız BC'de vardı; finance chat copilot + trader ikisi de)
3417
+ Not: BC oturumları zaten opencode registry'sinden skill'i görür. */
3418
+ if (session && session.finance) {
3419
+ const skillDef = (opencode.toolmap.definitions() || []).find(
3420
+ (d) => d && d.function && d.function.name === 'skill'
3421
+ );
3422
+ if (skillDef) toolsList = [...toolsList, skillDef];
3423
+ }
3328
3424
  /* panel_run yalnız SANDBOX oturumlarında görünsün — diğer panellerde
3329
3425
  modelin alet çantasında olmasın (hook olmadan çalışmaz) */
3330
3426
  if (!(session && session.sbSandbox)) {
@@ -4576,6 +4672,26 @@ const skills = require('./skills');
4576
4672
  return null;
4577
4673
  }
4578
4674
 
4675
+ /* opencode skill aracı gövdesi: katalogdan eşleşen SKILL.md'yi okur.
4676
+ Salt-okur — tüm oturum türlerinde (BC/finance/chat/trader) aynı yol. */
4677
+ _skillBody(args) {
4678
+ const wanted = String((args && args.name) || '').trim();
4679
+ let list = [];
4680
+ try { list = skills.scan(); } catch {}
4681
+ const hit = list.find((s) => s && s.name === wanted) || null;
4682
+ if (!hit) {
4683
+ return {
4684
+ ok: false,
4685
+ error:
4686
+ 'skill bulunamadı: ' + (wanted || '(boş)') +
4687
+ (list.length ? ' — mevcut: ' + list.map((s) => s.name).join(', ') : ''),
4688
+ };
4689
+ }
4690
+ let body = '';
4691
+ try { body = fs.readFileSync(hit.path, 'utf8'); } catch {}
4692
+ return { ok: true, name: hit.name, path: hit.path, content: body.slice(0, 24000) };
4693
+ }
4694
+
4579
4695
  async _execTool(name, args, signal, sessionId) {
4580
4696
  try {
4581
4697
  /* opencode mantığı (BC): model opencode builtin dilinde çağırır —
@@ -4592,6 +4708,12 @@ const skills = require('./skills');
4592
4708
  /* ANA KOD KİLİDİ: korumalı bölgeye yazma/silme girişimi daha kapıdan geçmez */
4593
4709
  const blocked = this._guardTool(name, args);
4594
4710
  if (blocked) return blocked;
4711
+ /* opencode skill aracı: katalogdaki SKILL.md gövdesini modele açar.
4712
+ Salt-okur ve güvenli — BC dahil TÜM oturum türlerinde çalışır
4713
+ (Beast Finance chat copilot + trader buradan kullanır). */
4714
+ if (name === 'skill') {
4715
+ return JSON.stringify(this._skillBody(args));
4716
+ }
4595
4717
  if (isBc) {
4596
4718
  /* opencode permission akışı (tool/external-directory.ts + her aracın
4597
4719
  ctx.ask'ı): önce workspace DIŞI erişim izni, sonra araç izni.
@@ -4618,24 +4740,6 @@ const skills = require('./skills');
4618
4740
  } catch (permErr) {
4619
4741
  return JSON.stringify({ ok: false, error: AskService.errorText(permErr) });
4620
4742
  }
4621
- /* opencode skill aracı: katalogdaki SKILL.md gövdesini modele açar */
4622
- if (name === 'skill') {
4623
- const wanted = String((args && args.name) || '').trim();
4624
- let list = [];
4625
- try { list = skills.scan(); } catch {}
4626
- const hit = list.find((s) => s && s.name === wanted) || null;
4627
- if (!hit) {
4628
- return JSON.stringify({
4629
- ok: false,
4630
- error:
4631
- 'skill bulunamadı: ' + (wanted || '(boş)') +
4632
- (list.length ? ' — mevcut: ' + list.map((s) => s.name).join(', ') : ''),
4633
- });
4634
- }
4635
- let body = '';
4636
- try { body = fs.readFileSync(hit.path, 'utf8'); } catch {}
4637
- return JSON.stringify({ ok: true, name: hit.name, path: hit.path, content: body.slice(0, 24000) });
4638
- }
4639
4743
  } else {
4640
4744
  /* onay kapısı: riskli araçta dış onay bekle; reddedilirse araç çalışmaz.
4641
4745
  "always" onaylı araçlar doğrudan geçer. (BC dışı oturumlar — opencode
@@ -4682,6 +4786,9 @@ const skills = require('./skills');
4682
4786
  return JSON.stringify(await mcp.call(name, args, signal));
4683
4787
  }
4684
4788
  if (name === 'memory_write') {
4789
+ /* Supermemory (lokal) DEFAULT: her yazım asenkron push edilir —
4790
+ sunucu ayaktaysa graf motoru özet/çelişki/unutma işlerini yürütür */
4791
+ this._smPush(sessionId, args.text);
4685
4792
  /* bot oturumu → botun KENDİ hafıza store'una yaz (global Beast hafızasına değil).
4686
4793
  mem0 açıkken: hash+semantic dedup'lı store'a gider, MEMORY.md aynası güncellenir. */
4687
4794
  const bctx = this._sessionBotCtx(sessionId ? this.cache.get(String(sessionId)) : null);
@@ -4690,7 +4797,7 @@ const skills = require('./skills');
4690
4797
  const r = await mem0.add('bot:' + bctx.id, [args.text]).catch(() => null);
4691
4798
  if (r && r.ok) {
4692
4799
  mem0.syncMirror('bot:' + bctx.id);
4693
- return JSON.stringify({ ok: true, duplicate: r.skipped > 0, event: r.events[0] && r.events[0].event });
4800
+ return JSON.stringify({ ok: true, duplicate: r.skipped > 0, event: r.events[0] && r.events[0].event, supermemory: !!this._smConfig() });
4694
4801
  }
4695
4802
  }
4696
4803
  return JSON.stringify(this.botMemory.append(bctx.id, args.text));
@@ -4699,7 +4806,7 @@ const skills = require('./skills');
4699
4806
  const r = await mem0.add('main', [args.text]).catch(() => null);
4700
4807
  if (r && r.ok) {
4701
4808
  mem0.syncMirror('main');
4702
- return JSON.stringify({ ok: true, duplicate: r.skipped > 0, event: r.events[0] && r.events[0].event });
4809
+ return JSON.stringify({ ok: true, duplicate: r.skipped > 0, event: r.events[0] && r.events[0].event, supermemory: !!this._smConfig() });
4703
4810
  }
4704
4811
  }
4705
4812
  return JSON.stringify(memory.append(args.text));
@@ -4717,6 +4824,12 @@ const skills = require('./skills');
4717
4824
  const bctx = this._sessionBotCtx(sessionId ? this.cache.get(String(sessionId)) : null);
4718
4825
  const q = String(args.query || '');
4719
4826
  const limit = clamp(Number(args.limit) || 5, 1, 15);
4827
+ /* Supermemory DEFAULT öncelik: hibrit arama + kullanıcı profili.
4828
+ Erişilemediyse/hata yerse yerel yollar aynen çalışır (fallback). */
4829
+ const smRes = await this._smSearch(sessionId, q, limit);
4830
+ if (smRes && smRes.results.length) {
4831
+ return JSON.stringify({ ok: true, query: q, ...smRes });
4832
+ }
4720
4833
  if (bctx && this.botMemory) {
4721
4834
  const rows = this.mem0Enabled
4722
4835
  ? await mem0.search('bot:' + bctx.id, q, { limit }).catch(() => [])
@@ -5883,6 +5996,25 @@ Engine.prototype.undoLastTodo = function (sid) {
5883
5996
  return this.undoTodo(sid, j[j.length - 1].todoId);
5884
5997
  };
5885
5998
 
5999
+ /* /deltodo: oturumun todo listesini tamamen temizler. Dosyaya BOŞ t:todo
6000
+ kaydı append edilir — yüklemede son kayıt kazanır, restart sonrası da
6001
+ boş kalır (eski t:todo satırları yeniden canlanamaz). */
6002
+ Engine.prototype.clearTodos = function (sid) {
6003
+ const key = String(sid || '');
6004
+ try {
6005
+ if (!key || !fs.existsSync(this._file(key))) return { ok: false, error: 'oturum bulunamadı' };
6006
+ } catch {
6007
+ return { ok: false, error: 'oturum bulunamadı' };
6008
+ }
6009
+ const prev = this.todos.get(key) || [];
6010
+ this.todos.set(key, []);
6011
+ try {
6012
+ fs.appendFileSync(this._file(key), JSON.stringify({ t: 'todo', items: [] }) + '\n');
6013
+ } catch {}
6014
+ emitSafe(this, key, { type: 'todos', sessionId: key, todos: [] });
6015
+ return { ok: true, count: prev.length };
6016
+ };
6017
+
5886
6018
  module.exports = Engine;
5887
6019
  module.exports.Engine = Engine;
5888
6020
  module.exports.sanitizeTodoItems = sanitizeTodoItems;
@@ -0,0 +1,132 @@
1
+ 'use strict';
2
+
3
+ /* ---------- Supermemory (lokal) adaptörü ----------
4
+ Beast'in bellek katmanına supermemory (git.new/memory) köprüsü — mem0.js
5
+ deseniyle TAKILABİLİR katman: sunucu (npx supermemory local → localhost:6767)
6
+ ayaktayken daha akıllı bellek (graf motoru, otomatik unutma, çelişki
7
+ çözümü, kullanıcı profili); ayakta değilken engine mevcut hafızaya sorunsuz
8
+ düşer. Zero npm bağımlılık — Node 18+ global fetch.
9
+
10
+ Uçlar (supermemory docs birebir):
11
+ POST /v3/documents { content, containerTag, customId?, metadata? }
12
+ POST /v4/search { q, containerTag, searchMode, limit, include? }
13
+ POST /v4/profile { containerTag, q? }
14
+ Auth: Authorization: Bearer <apiKey> (local key ilk boot'ta basılır) */
15
+
16
+ const BASE_TIMEOUT_MS = 8000;
17
+ const PROBE_TIMEOUT_MS = 2500;
18
+ const HEALTH_COOLDOWN_MS = 60000; // kapalıya aynı dakikada 200 kez vurma
19
+
20
+ function normalizeBase(url) {
21
+ return String(url || '').trim().replace(/\/+$/, '');
22
+ }
23
+
24
+ async function call(baseUrl, apiKey, path, body, timeoutMs) {
25
+ const base = normalizeBase(baseUrl);
26
+ if (!base) throw new Error('supermemory baseUrl boş');
27
+ const ctrl = new AbortController();
28
+ const timer = setTimeout(() => ctrl.abort(), timeoutMs || BASE_TIMEOUT_MS);
29
+ try {
30
+ const res = await fetch(base + path, {
31
+ method: 'POST',
32
+ headers: {
33
+ 'content-type': 'application/json',
34
+ ...(apiKey ? { authorization: 'Bearer ' + apiKey } : {}),
35
+ },
36
+ body: JSON.stringify(body || {}),
37
+ signal: ctrl.signal,
38
+ });
39
+ if (!res.ok) {
40
+ const txt = await res.text().catch(() => '');
41
+ throw new Error('HTTP ' + res.status + (txt ? ': ' + txt.slice(0, 180) : ''));
42
+ }
43
+ return await res.json();
44
+ } finally {
45
+ clearTimeout(timer);
46
+ }
47
+ }
48
+
49
+ /* Sunucu ayakta mı? GET / ile ucuz sağlık kontrolü */
50
+ async function probe(baseUrl, apiKey) {
51
+ const base = normalizeBase(baseUrl);
52
+ if (!base) return false;
53
+ const ctrl = new AbortController();
54
+ const timer = setTimeout(() => ctrl.abort(), PROBE_TIMEOUT_MS);
55
+ try {
56
+ const res = await fetch(base, {
57
+ method: 'GET',
58
+ headers: apiKey ? { authorization: 'Bearer ' + apiKey } : {},
59
+ signal: ctrl.signal,
60
+ });
61
+ return res.status < 500;
62
+ } catch {
63
+ return false;
64
+ } finally {
65
+ clearTimeout(timer);
66
+ }
67
+ }
68
+
69
+ /* Konuşma/belgi yut — arka planda çağrılır (fire-and-forget desenine uygun) */
70
+ async function add(baseUrl, apiKey, { content, containerTag, customId, metadata }) {
71
+ return call(baseUrl, apiKey, '/v3/documents', {
72
+ content: String(content || ''),
73
+ containerTag: String(containerTag || 'beast'),
74
+ ...(customId ? { customId: String(customId) } : {}),
75
+ ...(metadata && typeof metadata === 'object' ? { metadata } : {}),
76
+ });
77
+ }
78
+
79
+ /* Hibrit arama → normalize: [{text, similarity}] — memories varsayılan;
80
+ 'hybrid' RAG + bellek tek sorguda */
81
+ async function search(baseUrl, apiKey, { q, containerTag, limit, mode }) {
82
+ const r = await call(
83
+ baseUrl,
84
+ apiKey,
85
+ '/v4/search',
86
+ {
87
+ q: String(q || ''),
88
+ containerTag: String(containerTag || 'beast'),
89
+ searchMode: mode === 'documents' || mode === 'hybrid' ? mode : 'memories',
90
+ limit: Math.min(20, Math.max(1, Number(limit) || 5)),
91
+ },
92
+ 12000
93
+ );
94
+ const out = [];
95
+ for (const hit of (r && Array.isArray(r.results) ? r.results : []).slice(0, 10)) {
96
+ if (hit && typeof hit.memory === 'string' && hit.memory.trim()) {
97
+ out.push({ text: hit.memory.trim().slice(0, 600), similarity: Number(hit.similarity) || 0 });
98
+ }
99
+ /* hybrid mod: chunk'lar da gelir */
100
+ for (const c of (hit && Array.isArray(hit.chunks) ? hit.chunks : []).slice(0, 3)) {
101
+ const t = c && (c.content || c.text);
102
+ if (typeof t === 'string' && t.trim()) {
103
+ out.push({ text: t.trim().slice(0, 600), similarity: Number(c && c.similarity) || 0 });
104
+ }
105
+ }
106
+ }
107
+ return out.slice(0, Math.max(1, Number(limit) || 5));
108
+ }
109
+
110
+ /* Kullanıcı profili: { static: [], dynamic: [] } — her turda ucuz bağlam */
111
+ async function profile(baseUrl, apiKey, { containerTag, q }) {
112
+ const r = await call(
113
+ baseUrl,
114
+ apiKey,
115
+ '/v4/profile',
116
+ {
117
+ containerTag: String(containerTag || 'beast'),
118
+ ...(q ? { q: String(q) } : {}),
119
+ },
120
+ 12000
121
+ );
122
+ const p = (r && r.profile) || {};
123
+ const arr = (x) =>
124
+ Array.isArray(x)
125
+ ? x.map((s) => String(s || '').trim()).filter(Boolean).slice(0, 12)
126
+ : typeof x === 'string' && x.trim()
127
+ ? [x.trim().slice(0, 300)]
128
+ : [];
129
+ return { static: arr(p.static), dynamic: arr(p.dynamic) };
130
+ }
131
+
132
+ module.exports = { probe, add, search, profile, HEALTH_COOLDOWN_MS };
@@ -6,7 +6,8 @@
6
6
  kind=logs → Beast log dosyası taranır; son windowMin dakikadaki error/warn sayısı
7
7
  değer olur ("10 dk içinde 3+ hata olursa bağır")
8
8
  Koşul sağlanınca ilgili oturuma mesaj düşer (WA köprüsüne de otomatik akar).
9
- Depo: %APPDATA%\beast\watchers.json */
9
+ Depo: %APPDATA%\beast\watchers.json
10
+ Loglar: %APPDATA%\beast\watcher-logs.json (ana depodan AYRI; izleyici başına denetim kaydı) */
10
11
 
11
12
  const fs = require('fs');
12
13
  const path = require('path');
@@ -18,7 +19,89 @@ function file() {
18
19
  return path.join(beastRoot(), 'watchers.json');
19
20
  }
20
21
 
22
+ /* İzleyici logları ANA depodan AYRI tutulur: watcher-logs.json
23
+ Yapı: { logs: { [watcherId]: [{ ts, ev: ok|trigger|error, value, error }] } }
24
+ İzleyici başına LOG_CAP kayıt tutulur (yenisi eklenince en eskisi düşer). */
25
+ const LOG_CAP = 200;
26
+ const LOG_FILE = 'watcher-logs.json';
27
+
28
+ function logFile() {
29
+ return path.join(beastRoot(), LOG_FILE);
30
+ }
31
+
21
32
  let items = [];
33
+ let logs = {}; // watcherId -> [{ ts, ev, value, error }]
34
+ let logsDirty = false;
35
+
36
+ function loadLogs() {
37
+ try {
38
+ const raw = JSON.parse(fs.readFileSync(logFile(), 'utf8'));
39
+ logs = raw && typeof raw.logs === 'object' && raw.logs !== null ? raw.logs : {};
40
+ } catch {
41
+ logs = {};
42
+ }
43
+ }
44
+
45
+ function saveLogs(force) {
46
+ if (!logsDirty && !force) return;
47
+ try {
48
+ fs.mkdirSync(beastRoot(), { recursive: true });
49
+ fs.writeFileSync(logFile(), JSON.stringify({ logs }, null, 2));
50
+ logsDirty = false;
51
+ } catch {}
52
+ }
53
+
54
+ /* Tek izleyiciye log kaydı düş — değer/seri hale getirilemezse güvenli temsil */
55
+ function pushLog(id, ev, value, error) {
56
+ let v = null;
57
+ if (value !== undefined && value !== null) {
58
+ try {
59
+ v = typeof value === 'object' ? JSON.stringify(value).slice(0, 200) : value;
60
+ } catch {
61
+ v = String(value).slice(0, 200);
62
+ }
63
+ }
64
+ const arr = Array.isArray(logs[id]) ? logs[id] : (logs[id] = []);
65
+ arr.push({
66
+ ts: new Date().toISOString(),
67
+ ev, // 'ok' | 'trigger' | 'error'
68
+ value: v,
69
+ error: error ? String(error).slice(0, 300) : '',
70
+ });
71
+ if (arr.length > LOG_CAP) arr.splice(0, arr.length - LOG_CAP);
72
+ logsDirty = true;
73
+ }
74
+
75
+ /* Bir izleyicinin logları — en yeni en başta */
76
+ function logEntries(id) {
77
+ const arr = Array.isArray(logs[String(id)]) ? logs[String(id)] : [];
78
+ return arr.slice().reverse();
79
+ }
80
+
81
+ /* Tüm izleyicilerin logları — { [watcherId]: [...] }, en yeni en başta */
82
+ function logsAll() {
83
+ const out = {};
84
+ for (const k of Object.keys(logs)) out[k] = logEntries(k);
85
+ return out;
86
+ }
87
+
88
+ /* Logları temizle — id verilirse yalnız o izleyici, yoksa hepsi */
89
+ function logClear(id) {
90
+ if (id) delete logs[String(id)];
91
+ else logs = {};
92
+ saveLogs(true);
93
+ return { ok: true };
94
+ }
95
+
96
+ /* Artık var olmayan izleyicilerin loglarını temizle */
97
+ function pruneLogs() {
98
+ const ids = new Set(items.map((w) => w.id));
99
+ let changed = false;
100
+ for (const k of Object.keys(logs)) {
101
+ if (!ids.has(k)) { delete logs[k]; changed = true; }
102
+ }
103
+ if (changed) saveLogs(true);
104
+ }
22
105
 
23
106
  function load() {
24
107
  try {
@@ -27,6 +110,8 @@ function load() {
27
110
  } catch {
28
111
  items = [];
29
112
  }
113
+ loadLogs();
114
+ pruneLogs();
30
115
  }
31
116
 
32
117
  function save() {
@@ -189,6 +274,7 @@ function remove(id) {
189
274
  const before = items.length;
190
275
  items = items.filter((w) => w.id !== id);
191
276
  save();
277
+ if (items.length < before) logClear(id); /* izleyici gitti → logları da gitsin */
192
278
  return { ok: items.length < before };
193
279
  }
194
280
 
@@ -283,6 +369,7 @@ async function tickOnce(deps = {}) {
283
369
  const r = applyCheck(w, value, now);
284
370
  Object.assign(w, r.patch);
285
371
  w.lastError = '';
372
+ pushLog(w.id, r.triggered ? 'trigger' : 'ok', value);
286
373
  if (r.triggered) {
287
374
  if (typeof hooks.onTrigger === 'function') hooks.onTrigger({ ...w }, value);
288
375
  events.push({ id: w.id, name: w.name, value });
@@ -290,9 +377,11 @@ async function tickOnce(deps = {}) {
290
377
  } catch (e) {
291
378
  w.lastCheckAt = new Date(now).toISOString();
292
379
  w.lastError = String((e && e.message) || e).slice(0, 300);
380
+ pushLog(w.id, 'error', undefined, w.lastError);
293
381
  }
294
382
  }
295
383
  save();
384
+ saveLogs();
296
385
  return events;
297
386
  }
298
387
 
@@ -330,4 +419,7 @@ module.exports = {
330
419
  applyCheck,
331
420
  isDue,
332
421
  cooldownActive,
422
+ logEntries,
423
+ logsAll,
424
+ logClear,
333
425
  };
package/src/cron.js CHANGED
@@ -136,12 +136,13 @@ function list() {
136
136
  return jobs.map((j) => ({ ...j }));
137
137
  }
138
138
 
139
- function add({ name, schedule, prompt, once, sessionId }) {
139
+ function add({ name, schedule, prompt, once, sessionId, kind }) {
140
140
  const n = String(name || '').trim().slice(0, 80) || 'Görev';
141
141
  const s = String(schedule || '').trim();
142
142
  const p = String(prompt || '').trim().slice(0, 4000);
143
143
  if (!isValidSchedule(s)) return { ok: false, error: 'geçersiz cron ifadesi (örn: */15 * * * *)' };
144
144
  if (!p) return { ok: false, error: 'görev metni boş olamaz' };
145
+ const k = String(kind || '').trim().slice(0, 24);
145
146
  const job = {
146
147
  id: uid(),
147
148
  name: n,
@@ -150,6 +151,7 @@ function add({ name, schedule, prompt, once, sessionId }) {
150
151
  sessionId: sessionId || null,
151
152
  once: !!once,
152
153
  enabled: true,
154
+ ...(k ? { kind: k } : {}),
153
155
  createdAt: new Date().toISOString(),
154
156
  lastRunAt: null,
155
157
  nextRunAt: nextRunFrom(s, new Date()),
@@ -186,6 +188,17 @@ function remove(id) {
186
188
  return { ok: jobs.length < before };
187
189
  }
188
190
 
191
+ /* Koşula uyan TÜM görevleri sil (/autodel, "hatırlatmaları sil"). */
192
+ function removeIf(pred) {
193
+ const before = jobs.length;
194
+ jobs = jobs.filter((j) => {
195
+ try { return !pred(j); } catch { return true; }
196
+ });
197
+ const count = before - jobs.length;
198
+ if (count) save();
199
+ return { ok: true, count };
200
+ }
201
+
189
202
  /* Tüm görevleri bellekten ve diskten sil; zamanlayıcıyı durdur.
190
203
  "hepsini sil" / "/cron clear" için kullanılır. */
191
204
  function clearAll() {
@@ -233,17 +246,43 @@ function tick() {
233
246
  }
234
247
  }
235
248
 
249
+ /* Uygulama kapalıyken saati geçen tek-seferlik hatırlatma için teslim penceresi:
250
+ bu kadar süre geçmediyse uyarı GECİKMELİ de olsa teslim edilir. */
251
+ const ONCE_GRACE_MS = 60 * 60 * 1000;
252
+
253
+ /* Açılış uzlaşması (saf): saati geçmiş tek-seferlik hatırlatmalar eski
254
+ davranışta nextRunFrom ile GELECEK YILIN aynı gününe yeniden kuruluyordu —
255
+ bayat kayıt ölümsüzleşip tekrar tekrar önüne düşüyordu. Artık: grace
256
+ içindeyse "fire" listesine (gecikmeli teslim), bayatsa listeden düşer.
257
+ Tekrarlıların nextRunAt'ı her zamanki gibi yeniden hesaplanır. */
258
+ function reconcile(list, now, graceMs) {
259
+ const keep = [];
260
+ const fire = [];
261
+ for (const job of list) {
262
+ if (!job.enabled) { keep.push(job); continue; }
263
+ const nr = job.nextRunAt ? new Date(job.nextRunAt).getTime() : 0;
264
+ if (job.once && nr && nr <= now) {
265
+ if (now - nr <= graceMs) fire.push(job);
266
+ continue; // bayat olan burada düşer
267
+ }
268
+ if (!nr || nr <= now) {
269
+ keep.push({ ...job, nextRunAt: nextRunFrom(job.schedule, new Date(now)) });
270
+ continue;
271
+ }
272
+ keep.push(job);
273
+ }
274
+ return { jobs: keep, fire };
275
+ }
276
+
236
277
  function init(hooks) {
237
278
  onFire = (hooks && hooks.onFire) || null;
238
279
  load();
239
- for (const job of jobs) {
240
- if (job.enabled && (!job.nextRunAt || new Date(job.nextRunAt).getTime() <= Date.now())) {
241
- job.nextRunAt = nextRunFrom(job.schedule, new Date());
242
- }
243
- }
280
+ const r = reconcile(jobs, Date.now(), ONCE_GRACE_MS);
281
+ jobs = r.jobs;
244
282
  save();
245
283
  if (timer) clearInterval(timer);
246
284
  timer = setInterval(tick, 20 * 1000);
285
+ for (const job of r.fire) fire(job); // grace içindeki bayat hatırlatmayı gecikmeli teslim et
247
286
  }
248
287
 
249
288
  /* /stop: zamanlayıcıyı durdur (init ile geri başlar) */
@@ -252,4 +291,4 @@ function stop() {
252
291
  timer = null;
253
292
  }
254
293
 
255
- module.exports = { init, stop, list, add, update, remove, clearAll, toggle, runNow, parseCron, nextRunFrom, isValidSchedule, reminderSchedule };
294
+ module.exports = { init, stop, list, add, update, remove, removeIf, clearAll, toggle, runNow, parseCron, nextRunFrom, isValidSchedule, reminderSchedule, reconcile, ONCE_GRACE_MS };
package/src/main.js CHANGED
@@ -426,6 +426,11 @@ function parseSettingsText(text) {
426
426
  const parsed = JSON.parse(text);
427
427
  if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) throw new Error('bozuk yapı');
428
428
  if (!parsed.roleModels) parsed.roleModels = {};
429
+ /* Supermemory (lokal) DEFAULT AÇIK — sunucu ayaktayken daha akıllı bellek;
430
+ ayakta değilken klasik hafıza sorunsuz çalışır (engine fallback) */
431
+ if (!parsed.supermemory || typeof parsed.supermemory !== 'object') {
432
+ parsed.supermemory = { enabled: true, baseUrl: 'http://localhost:6767', apiKey: '', containerTag: 'beast' };
433
+ }
429
434
  return parsed;
430
435
  }
431
436
 
@@ -1101,6 +1106,8 @@ function waSlashHelp() {
1101
1106
  '• */usage* – bugünkü kullanım',
1102
1107
  '• */backup* – tüm veriyi ŞİFRELİ yedekle (Beast Kodu imzalı, Masaüstü\\Beast-Backups)',
1103
1108
  '• */status* – bağlantı ve servis durumu',
1109
+ '• */autodel* – tüm otomatik hatırlatmaları (bayat/eski dahil) tek komutla sil · */autodel all*: cron görevleri dahil hepsi',
1110
+ '• */deltodo* – bu oturumun todo listesini temizle · */deltodo all*: tüm oturumların todoları',
1104
1111
  '',
1105
1112
  'Gruplarda beni @mention ile çağır; ardışık mesajlarını tek cevapta birleştiririm.',
1106
1113
  ].join('\n');
@@ -1641,6 +1648,58 @@ async function tryWaSlash(jid, rawText, senderNum, payload0) {
1641
1648
  } else {
1642
1649
  out = 'Kullanım: `/cron list` · `/cron clear` (hepsini sil) · `/cron del <id>`';
1643
1650
  }
1651
+ } else if (cmd === 'autodel') {
1652
+ /* /autodel [all] — TÜM otomatik hatırlatmaları tek komutla sil.
1653
+ Bayat/eski kayıtlar dahil: kind=reminder + eski isim/prompt önekleri eşleşir.
1654
+ /autodel all → cron görevleri dahil her şey (=/cron clear) */
1655
+ const sub = String(arg || '').toLowerCase().trim();
1656
+ if (sub === 'all' || sub === 'hepsi' || sub === 'hepsini') {
1657
+ const r = cron.clearAll();
1658
+ cronEmit();
1659
+ out = r.ok
1660
+ ? `*Tüm zamanlanmış görevler silindi* (${r.count} adet).`
1661
+ : 'Temizlenemedi.';
1662
+ } else {
1663
+ const r = cron.removeIf(isReminderJob);
1664
+ cronEmit();
1665
+ const left = cron.list().filter((j) => j.enabled).length;
1666
+ out = r.count
1667
+ ? `*${r.count} otomatik hatırlatma silindi.* Kalan aktif görev: ${left}`
1668
+ : 'Silinecek otomatik hatırlatma yok — zaten temiz.';
1669
+ }
1670
+ } else if (cmd === 'deltodo') {
1671
+ /* /deltodo [all] — todo listelerini temizler: arg=all → TÜM oturumlar;
1672
+ yoksa yalnız bu sohbetin oturumu. Panelden de düşsün diye 'todos'
1673
+ olayı emit edilir (engine.clearTodos). */
1674
+ const sub = String(arg || '').toLowerCase().trim();
1675
+ if (sub === 'all' || sub === 'hepsi' || sub === 'hepsini') {
1676
+ let n = 0;
1677
+ let items = 0;
1678
+ try {
1679
+ for (const v of engine.listSessions()) {
1680
+ const r = engine.clearTodos(v.id);
1681
+ if (r && r.ok) {
1682
+ n++;
1683
+ items += r.count || 0;
1684
+ }
1685
+ }
1686
+ } catch {}
1687
+ out = n
1688
+ ? `*Tüm oturumların todoları temizlendi* (${n} oturum, ${items} madde).`
1689
+ : 'Temizlenecek todo yok.';
1690
+ } else {
1691
+ const sid = waChats.get(jid) || waSingleSid || '';
1692
+ if (!sid) {
1693
+ out = 'Bu sohbette açık oturum yok — todo listesi de yok.';
1694
+ } else {
1695
+ const r = engine.clearTodos(sid);
1696
+ out = r.ok
1697
+ ? (r.count
1698
+ ? `*${r.count} todo temizlendi.* (tüm oturumlar için: /deltodo all)`
1699
+ : 'Todo listesi zaten boş. (tüm oturumlar için: /deltodo all)')
1700
+ : 'Oturum bulunamadı — todo listesi yok.';
1701
+ }
1702
+ }
1644
1703
  } else if (cmd === 'beastcode') {
1645
1704
  /* /beastcode [görev] — WhatsApp'tan UZAKTAN KODLAMA modu:
1646
1705
  masaüstünde GERÇEK Beast Code paneli açılır (IDE ekranı), sohbet
@@ -2079,6 +2138,22 @@ async function documentToTextAsync(media) {
2079
2138
 
2080
2139
  /* ---------- WA hatırlatıcı ---------- */
2081
2140
 
2141
+ /* Hatırlatma job'ı mı? Yeni kayıtlarda kind='reminder' var; eski (bayat)
2142
+ kayıtlar isim/prompt önekinden tanınır — /autodel ve doğal dil silme
2143
+ bunu kullanır. */
2144
+ function isReminderJob(j) {
2145
+ if (!j || typeof j !== 'object') return false;
2146
+ if (j.kind === 'reminder') return true;
2147
+ const n = String(j.name || '');
2148
+ const p = String(j.prompt || '');
2149
+ return (
2150
+ /^Hatırlatma:/.test(n) ||
2151
+ /^Tekrarlı hatırlatma:/.test(n) ||
2152
+ p.startsWith('[HATIRLATMA ZAMANI]') ||
2153
+ p.startsWith('[TEKRARLI HATIRLATMA]')
2154
+ );
2155
+ }
2156
+
2082
2157
  function scheduleReminder({ when, message, sessionId, repeat }) {
2083
2158
  try {
2084
2159
  const msg = String(message || '').trim();
@@ -2092,6 +2167,7 @@ function scheduleReminder({ when, message, sessionId, repeat }) {
2092
2167
  name: 'Tekrarlı hatırlatma: ' + msg.slice(0, 40),
2093
2168
  schedule: s.schedule,
2094
2169
  prompt: `[TEKRARLI HATIRLATMA] Kullanıcıya şunu hatırlat: "${msg}". Kısaca ve nazikçe bildir.`,
2170
+ kind: 'reminder',
2095
2171
  sessionId: sessionId || undefined,
2096
2172
  });
2097
2173
  if (!r.ok) return r;
@@ -2106,6 +2182,7 @@ function scheduleReminder({ when, message, sessionId, repeat }) {
2106
2182
  name: 'Hatırlatma: ' + msg.slice(0, 40),
2107
2183
  schedule: cronExpr,
2108
2184
  prompt: `[HATIRLATMA ZAMANI] Kullanıcıya şunu hatırlat: "${msg}". Kısaca ve nazikçe bildir.`,
2185
+ kind: 'reminder',
2109
2186
  once: true,
2110
2187
  sessionId: sessionId || undefined,
2111
2188
  });
@@ -2286,6 +2363,23 @@ async function handleWaIncoming(jid, payload, senderNum) {
2286
2363
  return;
2287
2364
  }
2288
2365
 
2366
+ /* "hatırlatmaları sil" / "alarm temizle" / "reminders kaldır" → yalnızca
2367
+ otomatik hatırlatmalar silinir (cron görevlerine dokunulmaz). */
2368
+ if (
2369
+ /hat[iı]rlat|alarm|remind/.test(lc) &&
2370
+ /(sil|sıl|temizle|kaldır|iptal|sıfırla|kapat)/.test(lc)
2371
+ ) {
2372
+ const r = cron.removeIf(isReminderJob);
2373
+ cronEmit();
2374
+ await sendWaSafe(
2375
+ jid,
2376
+ r.count
2377
+ ? `*${r.count} otomatik hatırlatma silindi.* Zamanlanmış kalan görev: ${cron.list().filter((j) => j.enabled).length}`
2378
+ : 'Silinecek otomatik hatırlatma yok — zaten temiz.'
2379
+ ).catch(() => {});
2380
+ return;
2381
+ }
2382
+
2289
2383
  if (isGroup) {
2290
2384
  const g = settings.waGroups || {};
2291
2385
  if (!g.enabled) return;
@@ -3075,6 +3169,10 @@ function reloadBackend() {
3075
3169
  nightReflectAt: settings.nightReflectAt || null,
3076
3170
  approvals: settings.security && settings.security.approvals ? approvalsBridge : null,
3077
3171
  alwaysAllowTools: (settings.security && settings.security.alwaysAllow) || [],
3172
+ /* Supermemory (lokal) bellek katmanı — default açıktır (parseSettingsText);
3173
+ sunucu ayakta değilse engine klasik hafızaya düşer. Kapamak:
3174
+ settings.json → supermemory.enabled = false */
3175
+ supermemory: settings.supermemory || { enabled: true, baseUrl: 'http://localhost:6767', apiKey: '', containerTag: 'beast' },
3078
3176
  crashFile: FALLOUT_CRASH_FILE,
3079
3177
  notifyOwnerFail: settings.notifyOwnerFail !== false,
3080
3178
  fileSend: deliverFile,
@@ -3298,11 +3396,11 @@ function reloadBackend() {
3298
3396
  }
3299
3397
  /* CRON → TÜM AKTİF ENTEGRASYONLAR: cron işi bittiğinde cevap (ya da
3300
3398
  hata) sahibin bağlı olduğu her kanala yansıtılır — WA + Telegram +
3301
- Discord. Aynı kanal hem cron oturumuna bağlıysa TEK cevap alır. */
3302
- if ((ev.type === 'done' || ev.type === 'error') && cronAnswerPending.has(String(ev.sessionId))) {
3303
- const cjob = cronAnswerPending.get(String(ev.sessionId));
3304
- cronAnswerPending.delete(String(ev.sessionId));
3305
- if (!ev.aborted) {
3399
+ Discord. Aynı kanal hem cron oturumuna bağlıysa TEK cevap alır.
3400
+ Bayat (TTL aşımı) bekleme kaydı Take ile düşer, yansıtılmaz. */
3401
+ if (ev.type === 'done' || ev.type === 'error') {
3402
+ const cjob = cronAnswerPendingTake(ev.sessionId);
3403
+ if (cjob && !ev.aborted) {
3306
3404
  (async () => {
3307
3405
  try {
3308
3406
  let txt = '';
@@ -6030,6 +6128,39 @@ ipcMain.handle('sec:set', (_e, cfg) => {
6030
6128
  log.info('sec', `güvenlik: onay kapısı ${settings.security.approvals ? 'AÇIK' : 'KAPALI (her şey serbest)'}`);
6031
6129
  return { approvals: settings.security.approvals, alwaysAllow: settings.security.alwaysAllow };
6032
6130
  });
6131
+
6132
+ /* Supermemory (lokal) ayarları: engine'e canlı yansıt + sağlık önbelleğini tazele */
6133
+ ipcMain.handle('supermemory:set', (_e, cfg) => {
6134
+ try {
6135
+ const base = String((cfg && cfg.baseUrl) || '').trim() || 'http://localhost:6767';
6136
+ settings.supermemory = {
6137
+ enabled: !!(cfg && cfg.enabled),
6138
+ baseUrl: base.replace(/\/+$/, ''),
6139
+ apiKey: String((cfg && cfg.apiKey) || '').trim(),
6140
+ containerTag: String((cfg && cfg.containerTag) || 'beast').trim() || 'beast',
6141
+ };
6142
+ saveSettings();
6143
+ if (engine) {
6144
+ engine.supermemory = settings.supermemory;
6145
+ engine._smUp = null;
6146
+ engine._smCheckedAt = 0;
6147
+ engine._smWarned = false;
6148
+ }
6149
+ return { ok: true, supermemory: settings.supermemory };
6150
+ } catch (e) {
6151
+ return { ok: false, error: String((e && e.message) || e) };
6152
+ }
6153
+ });
6154
+ ipcMain.handle('supermemory:get', () => {
6155
+ const sm = settings.supermemory || {};
6156
+ return {
6157
+ enabled: sm.enabled !== false,
6158
+ baseUrl: sm.baseUrl || 'http://localhost:6767',
6159
+ apiKey: sm.apiKey || '',
6160
+ containerTag: sm.containerTag || 'beast',
6161
+ up: engine ? !!engine._smUp : null,
6162
+ };
6163
+ });
6033
6164
  ipcMain.handle('approval:respond', (_e, { id, ok, always }) => resolveApproval(id, ok, always));
6034
6165
  /* opencode permission reply (BC): UI kartı üçlü cevap verir —
6035
6166
  'once' | 'always' | 'reject' (+ opsiyonel geri bildirim) */
@@ -6477,8 +6608,40 @@ function cronEmit() {
6477
6608
 
6478
6609
  /* Cron cevabı bekleme haritası: sid -> job. İş 'done' olunca cevap
6479
6610
  SAHİBİN bağlı olduğu TÜM AKTİF entegrasyonlara yansıtılır (kullanıcı
6480
- hangi kanaldan ajanla iletişimde belli değil). */
6611
+ hangi kanaldan ajanla iletişimde belli değil). Kayıtlar TTL'li —
6612
+ done/error hiç gelmezse bayat bekleme sonraki done'a yansıtılmaz. */
6481
6613
  const cronAnswerPending = new Map();
6614
+ const cronAnswerPendingAt = new Map();
6615
+ const CRON_ANSWER_TTL_MS = 30 * 60 * 1000;
6616
+
6617
+ function cronAnswerPendingSet(sid, job) {
6618
+ cronAnswerPendingSweep();
6619
+ cronAnswerPending.set(String(sid), job);
6620
+ cronAnswerPendingAt.set(String(sid), Date.now());
6621
+ }
6622
+
6623
+ function cronAnswerPendingDrop(sid) {
6624
+ cronAnswerPending.delete(String(sid));
6625
+ cronAnswerPendingAt.delete(String(sid));
6626
+ }
6627
+
6628
+ /* Bayat kayıt (TTL aşımı) haritalardan temizlenir */
6629
+ function cronAnswerPendingSweep() {
6630
+ const now = Date.now();
6631
+ for (const [k, at] of cronAnswerPendingAt) {
6632
+ if (now - at > CRON_ANSWER_TTL_MS) cronAnswerPendingDrop(k);
6633
+ }
6634
+ }
6635
+
6636
+ /* Kaydı okuyup düşürür; bayat kayıt ise null döner (yansıtma yok) */
6637
+ function cronAnswerPendingTake(sid) {
6638
+ const k = String(sid);
6639
+ const job = cronAnswerPending.get(k);
6640
+ const at = cronAnswerPendingAt.get(k) || 0;
6641
+ cronAnswerPendingDrop(k);
6642
+ if (!job || Date.now() - at > CRON_ANSWER_TTL_MS) return null;
6643
+ return job;
6644
+ }
6482
6645
 
6483
6646
  /* OTOMATİK YENİ SOHBET YOK: cron/izleyici/fallout tetiklendiğinde oturum
6484
6647
  seçimi — kayıtlı id'nin DOSYASI hâlâ duruyorsa O, değilse EN GÜNCEL
@@ -6558,11 +6721,11 @@ function cronFire(job) {
6558
6721
  if (sid !== String(job.sessionId || '')) {
6559
6722
  cron.update(job.id, { sessionId: sid });
6560
6723
  }
6561
- cronAnswerPending.set(String(sid), job);
6724
+ cronAnswerPendingSet(sid, job);
6562
6725
  const sent = engine.send(sid, {
6563
6726
  text: `[cron: ${job.name}]\n${job.prompt}`,
6564
6727
  });
6565
- if (!sent) cronAnswerPending.delete(String(sid)); // gönderilemedi — bayat bekleme bırakma
6728
+ if (!sent) cronAnswerPendingDrop(sid); // gönderilemedi — bayat bekleme bırakma
6566
6729
  } catch {}
6567
6730
  cronEmit();
6568
6731
  }
@@ -7082,6 +7245,9 @@ ipcMain.handle('watchers:toggle', (_e, id) => {
7082
7245
  watchers.patch(String(id), { enabled: !w.enabled });
7083
7246
  return { ok: true, watcher: watchers.get(String(id)) };
7084
7247
  });
7248
+ /* izleyici logları — ana depodan ayrı watcher-logs.json üzerinden */
7249
+ ipcMain.handle('watchers:logs', () => watchers.logsAll());
7250
+ ipcMain.handle('watchers:logsClear', (_e, id) => watchers.logClear(id ? String(id) : null));
7085
7251
  ipcMain.handle('cron:add', (_e, job) => {
7086
7252
  const r = cron.add(job || {});
7087
7253
  cronEmit();
package/src/preload.js CHANGED
@@ -89,6 +89,9 @@ contextBridge.exposeInMainWorld('beast', {
89
89
  bcDeleteAll: () => ipcRenderer.invoke('bc:deleteAll'),
90
90
  /* opencode permission (BC): UI kartı cevabı — 'once' | 'always' | 'reject' */
91
91
  permissionReply: (requestId, action, message) => ipcRenderer.invoke('permission:reply', { requestId, action, message }),
92
+ /* Supermemory (lokal) bellek katmanı ayarları */
93
+ supermemoryGet: () => ipcRenderer.invoke('supermemory:get'),
94
+ supermemorySet: (cfg) => ipcRenderer.invoke('supermemory:set', cfg),
92
95
  studioSend: (msg, attachments) => ipcRenderer.invoke('studio:send', { msg, attachments }),
93
96
  studioStop: () => ipcRenderer.invoke('studio:stop'),
94
97
  studioNew: () => ipcRenderer.invoke('studio:new'),
@@ -143,6 +146,10 @@ contextBridge.exposeInMainWorld('beast', {
143
146
 
144
147
  watchersToggle: (id) => ipcRenderer.invoke('watchers:toggle', id),
145
148
 
149
+ watchersLogs: () => ipcRenderer.invoke('watchers:logs'),
150
+
151
+ watchersLogsClear: (id) => ipcRenderer.invoke('watchers:logsClear', id),
152
+
146
153
  providerKeys: () => ipcRenderer.invoke('providers:keys'),
147
154
 
148
155
  openPath: (p) => ipcRenderer.invoke('shell:openPath', p),
@@ -542,6 +542,14 @@
542
542
  bot_no_watchers: 'Bu bota ait izleyici yok.',
543
543
  w_pane: 'İZLEYİCİLER',
544
544
  w_empty: 'Kayıtlı izleyici yok.',
545
+ tipWatchLog: 'İzleyici loglarını gör',
546
+ w_log_title: 'İzleyici Logları',
547
+ w_log_clear: 'Seçili izleyicinin loglarını temizle',
548
+ w_log_cleared: 'Loglar temizlendi',
549
+ w_log_empty: 'Bu izleyici için henüz log kaydı yok.',
550
+ w_log_ev_ok: 'kontrol',
551
+ w_log_ev_trigger: 'tetik',
552
+ w_log_ev_error: 'hata',
545
553
  bot_stats_sub: 'Bu botun kullanım özeti.',
546
554
  bot_stat_numbers: 'Numara',
547
555
  bot_stat_sessions: 'Sohbet',
@@ -1038,6 +1046,14 @@
1038
1046
  cr_toggle_pause: 'Pause',
1039
1047
  w_empty: 'No saved watchers — add from the form above.',
1040
1048
  w_pane: 'WATCHERS',
1049
+ tipWatchLog: 'View watcher logs',
1050
+ w_log_title: 'Watcher Logs',
1051
+ w_log_clear: 'Clear selected watcher logs',
1052
+ w_log_cleared: 'Logs cleared',
1053
+ w_log_empty: 'No log entries for this watcher yet.',
1054
+ w_log_ev_ok: 'check',
1055
+ w_log_ev_trigger: 'trigger',
1056
+ w_log_ev_error: 'error',
1041
1057
  wa_vazgec: 'Cancel',
1042
1058
  wa_kaydet: 'save',
1043
1059
  wa_updated: 'Updated',
@@ -17,7 +17,8 @@
17
17
  <div id="watchPaneHead">
18
18
  <span id="watchPaneTitle" data-i18n="w_pane">İZLEYİCİLER</span>
19
19
  <span class="file-spacer"></span>
20
- <button id="watchPaneOpen" title="İzleyicileri yönet">+</button>
20
+ <button id="watchPaneLog" title="İzleyici loglarını gör" data-i18n-title="tipWatchLog"><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M10 9H8"/><path d="M16 13H8"/><path d="M16 17H8"/></svg></button>
21
+ <button id="watchPaneOpen" title="İzleyicileri yönet" data-i18n-title="tipWatch">+</button>
21
22
  </div>
22
23
  <div id="watchPaneList"></div>
23
24
  </div>
@@ -528,6 +529,22 @@
528
529
  </div>
529
530
  </div>
530
531
 
532
+ <div id="watchLogOverlay" class="mini-overlay" hidden>
533
+ <div class="mini-dialog">
534
+ <div class="mini-head">
535
+ <span class="mini-title" data-i18n="w_log_title">İzleyici Logları</span>
536
+ <button class="mini-close" id="watchLogClose" title="Kapat" data-i18n-title="btn_close">&#x00D7;</button>
537
+ </div>
538
+ <div class="mini-body">
539
+ <div id="watchLogBar">
540
+ <select id="watchLogSel" class="mini-select"></select>
541
+ <button id="watchLogClear" class="mr-btn" title="Seçili izleyicinin loglarını temizle" data-i18n-title="w_log_clear"><svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v2"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/></svg></button>
542
+ </div>
543
+ <div id="watchLogList" class="mini-list"></div>
544
+ </div>
545
+ </div>
546
+ </div>
547
+
531
548
  <div id="finSymOverlay" class="mini-overlay" hidden>
532
549
  <div class="mini-dialog fin-sym-dialog">
533
550
  <div class="mini-head">
@@ -45,6 +45,7 @@ const els = {
45
45
  watchBtn: $('#watchBtn'),
46
46
  watchPaneList: $('#watchPaneList'),
47
47
  watchPaneOpen: $('#watchPaneOpen'),
48
+ watchPaneLog: $('#watchPaneLog'),
48
49
  cronBtn: $('#cronBtn'),
49
50
  watchOverlay: $('#watchOverlay'),
50
51
  cronOverlay: $('#cronOverlay'),
@@ -52,6 +53,11 @@ const els = {
52
53
  cronClose: $('#cronClose'),
53
54
  watchList: $('#watchList'),
54
55
  cronModalList: $('#cronModalList'),
56
+ watchLogOverlay: $('#watchLogOverlay'),
57
+ watchLogClose: $('#watchLogClose'),
58
+ watchLogSel: $('#watchLogSel'),
59
+ watchLogClear: $('#watchLogClear'),
60
+ watchLogList: $('#watchLogList'),
55
61
  browserBar: $('#browserBar'),
56
62
  bbBack: $('#bbBack'),
57
63
  bbFwd: $('#bbFwd'),
@@ -5611,6 +5617,11 @@ async function renderWatchersPane(force) {
5611
5617
  `</div></div>`;
5612
5618
  const btns = document.createElement('div');
5613
5619
  btns.className = 'wp-btns';
5620
+ const lg = document.createElement('button');
5621
+ lg.className = 'wp-btn';
5622
+ lg.innerHTML = '<svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M10 9H8"/><path d="M16 13H8"/><path d="M16 17H8"/></svg>';
5623
+ lg.title = _t('w_log_title');
5624
+ lg.addEventListener('click', (e) => { e.stopPropagation(); openWatchLogModal(w.id); });
5614
5625
  const t = document.createElement('button');
5615
5626
  t.className = 'wp-btn';
5616
5627
  t.textContent = w.enabled ? '❚❚' : '▶';
@@ -5632,7 +5643,7 @@ async function renderWatchersPane(force) {
5632
5643
  renderWatchersPane(true);
5633
5644
  renderWatchersModal(true);
5634
5645
  });
5635
- btns.append(t, d);
5646
+ btns.append(lg, t, d);
5636
5647
  row.appendChild(btns);
5637
5648
  row.addEventListener('click', () => openWatchModal());
5638
5649
  list.appendChild(row);
@@ -5669,6 +5680,11 @@ async function renderWatchersModal(force) {
5669
5680
  `</div></div>`;
5670
5681
  const btns = document.createElement('div');
5671
5682
  btns.style.cssText = 'display:flex;gap:6px;flex:none';
5683
+ const lg = document.createElement('button');
5684
+ lg.className = 'mr-btn';
5685
+ lg.innerHTML = '<svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M10 9H8"/><path d="M16 13H8"/><path d="M16 17H8"/></svg>';
5686
+ lg.title = _t('w_log_title');
5687
+ lg.addEventListener('click', () => openWatchLogModal(w.id));
5672
5688
  const t = document.createElement('button');
5673
5689
  t.className = 'mr-btn';
5674
5690
  t.textContent = w.enabled ? '❚❚' : '▶';
@@ -5679,12 +5695,85 @@ async function renderWatchersModal(force) {
5679
5695
  d.textContent = '×';
5680
5696
  d.title = _t('cr_del');
5681
5697
  d.addEventListener('click', async () => { await beast.watchersRemove(w.id); renderWatchersModal(); });
5682
- btns.append(t, d);
5698
+ btns.append(lg, t, d);
5683
5699
  row.appendChild(btns);
5684
5700
  els.watchList.appendChild(row);
5685
5701
  }
5686
5702
  }
5687
5703
 
5704
+ /* ---------- İZLEYİCİ LOG MODALI ----------
5705
+ Her izleyicinin logu AYRI tutulur (watcher-logs.json) ve AYRI gösterilir:
5706
+ üstteki seçiciyle izleyici seç → yalnız onun denetim kayıtları listelenir. */
5707
+ let watchLogSelId = '';
5708
+ let lastWatchLogKey = '';
5709
+ let lastWatchLogSelKey = '';
5710
+
5711
+ async function renderWatchLogModal(force) {
5712
+ if (!els.watchLogOverlay) return;
5713
+ let rows = []; try { rows = (await beast.watchersList()) || []; } catch {}
5714
+ let logsMap = {}; try { logsMap = (await beast.watchersLogs()) || {}; } catch {}
5715
+ /* seçim: yok olan seçilmişse ilk izleyiciye düş */
5716
+ if (!rows.some((w) => w.id === watchLogSelId)) watchLogSelId = rows.length ? rows[0].id : '';
5717
+ /* seçici — izleyici listesi değişmediyse DOM'a dokunma (odak korunur) */
5718
+ const selKey = JSON.stringify(rows.map((w) => [w.id, w.name, w.enabled]));
5719
+ if (selKey !== lastWatchLogSelKey) {
5720
+ lastWatchLogSelKey = selKey;
5721
+ els.watchLogSel.innerHTML = '';
5722
+ if (!rows.length) {
5723
+ const o = document.createElement('option');
5724
+ o.textContent = _t('w_empty');
5725
+ o.value = '';
5726
+ els.watchLogSel.appendChild(o);
5727
+ } else {
5728
+ for (const w of rows) {
5729
+ const o = document.createElement('option');
5730
+ o.value = w.id;
5731
+ o.textContent = (w.enabled ? '' : '⏸ ') + w.name;
5732
+ els.watchLogSel.appendChild(o);
5733
+ }
5734
+ }
5735
+ els.watchLogSel.value = watchLogSelId;
5736
+ }
5737
+ /* log satırları — yalnız seçili izleyicinin kayıtları */
5738
+ const entries = watchLogSelId ? (logsMap[watchLogSelId] || []) : [];
5739
+ const key = JSON.stringify([watchLogSelId, selKey, entries]);
5740
+ if (!force && key === lastWatchLogKey) return;
5741
+ lastWatchLogKey = key;
5742
+ els.watchLogList.innerHTML = '';
5743
+ if (!watchLogSelId || !entries.length) {
5744
+ els.watchLogList.innerHTML = '<div class="mini-empty">' + _t('w_log_empty') + '</div>';
5745
+ return;
5746
+ }
5747
+ const evBadge = { ok: ['ok', _t('w_log_ev_ok')], trigger: ['trigger', _t('w_log_ev_trigger')], error: ['error', _t('w_log_ev_error')] };
5748
+ for (const en of entries) {
5749
+ const [cls, label] = evBadge[en.ev] || ['ok', en.ev];
5750
+ const row = document.createElement('div');
5751
+ row.className = 'wl-row';
5752
+ row.innerHTML =
5753
+ `<span class="wl-time">${escapeHtml(fmtWhen(en.ts))}</span>` +
5754
+ `<span class="wl-badge ${cls}">${escapeHtml(label)}</span>` +
5755
+ `<span class="wl-main">` +
5756
+ (en.ev === 'error'
5757
+ ? `<span class="wl-err">${escapeHtml(String(en.error || '').slice(0, 120))}</span>`
5758
+ : (en.value !== null && en.value !== undefined ? escapeHtml(String(en.value).slice(0, 120)) : '—')) +
5759
+ `</span>`;
5760
+ els.watchLogList.appendChild(row);
5761
+ }
5762
+ }
5763
+
5764
+ function closeWatchLogModal() {
5765
+ toggleMini(els.watchLogOverlay, true);
5766
+ stopMiniRefresh();
5767
+ }
5768
+
5769
+ async function openWatchLogModal(id) {
5770
+ if (id) watchLogSelId = String(id);
5771
+ lastWatchLogSelKey = ''; /* seçici her açılışta TAZE kurulsun */
5772
+ toggleMini(els.watchLogOverlay, false);
5773
+ await renderWatchLogModal(true);
5774
+ startMiniRefresh();
5775
+ }
5776
+
5688
5777
  let lastCronKey = '';
5689
5778
  async function renderCronModal(force) {
5690
5779
  let jobs = [];
@@ -5746,6 +5835,7 @@ function startMiniRefresh() {
5746
5835
  stopMiniRefresh();
5747
5836
  miniRefreshTimer = setInterval(async () => {
5748
5837
  if (els.watchOverlay && !els.watchOverlay.hidden) await renderWatchersModal();
5838
+ else if (els.watchLogOverlay && !els.watchLogOverlay.hidden) await renderWatchLogModal();
5749
5839
  else if (els.cronOverlay && !els.cronOverlay.hidden) await renderCronModal();
5750
5840
  else stopMiniRefresh();
5751
5841
  }, 4000);
@@ -6487,12 +6577,29 @@ async function init() {
6487
6577
  /* cron modülü — olay bağlama */
6488
6578
  if (els.watchBtn) els.watchBtn.addEventListener('click', openWatchModal);
6489
6579
  if (els.watchPaneOpen) els.watchPaneOpen.addEventListener('click', openWatchModal);
6580
+ if (els.watchPaneLog) els.watchPaneLog.addEventListener('click', () => openWatchLogModal());
6490
6581
  if (els.cronBtn) els.cronBtn.addEventListener('click', openCronModal);
6491
6582
  if (els.watchClose) els.watchClose.addEventListener('click', closeWatchModal);
6492
6583
  if (els.cronClose) els.cronClose.addEventListener('click', closeCronModal);
6584
+ if (els.watchLogClose) els.watchLogClose.addEventListener('click', closeWatchLogModal);
6585
+ if (els.watchLogSel) els.watchLogSel.addEventListener('change', () => {
6586
+ watchLogSelId = els.watchLogSel.value;
6587
+ lastWatchLogKey = '';
6588
+ renderWatchLogModal(true);
6589
+ });
6590
+ if (els.watchLogClear) els.watchLogClear.addEventListener('click', async () => {
6591
+ if (!watchLogSelId) return;
6592
+ await beast.watchersLogsClear(watchLogSelId);
6593
+ lastWatchLogKey = '';
6594
+ await renderWatchLogModal(true);
6595
+ toast(_t('w_log_cleared'));
6596
+ });
6493
6597
  if (els.watchOverlay) els.watchOverlay.addEventListener('click', (e) => {
6494
6598
  if (e.target === els.watchOverlay) closeWatchModal();
6495
6599
  });
6600
+ if (els.watchLogOverlay) els.watchLogOverlay.addEventListener('click', (e) => {
6601
+ if (e.target === els.watchLogOverlay) closeWatchLogModal();
6602
+ });
6496
6603
  if (els.cronOverlay) els.cronOverlay.addEventListener('click', (e) => {
6497
6604
  if (e.target === els.cronOverlay) closeCronModal();
6498
6605
  });
@@ -6579,6 +6686,7 @@ async function init() {
6579
6686
  document.addEventListener('keydown', (e) => {
6580
6687
  if (e.key === 'Escape') {
6581
6688
  if ($('#botOverlay') && !$('#botOverlay').hidden) { botOverlaySetOpen(false); return; }
6689
+ if (els.watchLogOverlay && !els.watchLogOverlay.hidden) { closeWatchLogModal(); return; }
6582
6690
  if (els.watchOverlay && !els.watchOverlay.hidden) { closeWatchModal(); return; }
6583
6691
  if (els.cronOverlay && !els.cronOverlay.hidden) { closeCronModal(); return; }
6584
6692
  if (!els.settingsOverlay.hidden) closeSettings();
@@ -133,7 +133,6 @@ body:not(.ide-mode):not(.studio-mode):not(.sandbox-mode) #watchPane { display: f
133
133
  }
134
134
  #watchPaneTitle { font-size: 10.5px; letter-spacing: 2px; color: var(--muted); font-weight: 800; }
135
135
  #watchPaneHead button {
136
- margin-left: auto;
137
136
  background: none;
138
137
  border: 1px solid var(--border);
139
138
  border-radius: 7px;
@@ -142,7 +141,13 @@ body:not(.ide-mode):not(.studio-mode):not(.sandbox-mode) #watchPane { display: f
142
141
  font-size: 12px;
143
142
  line-height: 1.2;
144
143
  padding: 1px 7px;
144
+ height: 22px;
145
+ display: inline-flex;
146
+ align-items: center;
147
+ justify-content: center;
145
148
  }
149
+ /* LOG ve + butonları sağda YAN YANA BİTİŞİK dursun: yalnız ilkinde itici boşluk */
150
+ #watchPaneHead button:first-of-type { margin-left: auto; }
146
151
  #watchPaneHead button:hover { color: var(--text); background: var(--panel2); }
147
152
  #watchPaneList { flex: 1; overflow-y: auto; padding: 4px; }
148
153
  .wp-row {
@@ -1464,6 +1469,35 @@ body.browser-open .mini-overlay { right: var(--bw, 480px); }
1464
1469
  .mr-btn:hover { color: var(--accent); }
1465
1470
  .mr-btn.del:hover { color: var(--err); }
1466
1471
 
1472
+ /* ---------- İzleyici log modalı (her izleyicinin logu ayrı) ---------- */
1473
+ #watchLogBar { display: flex; gap: 6px; align-items: center; margin-bottom: 10px; }
1474
+ #watchLogSel { flex: 1; min-width: 0; }
1475
+ .mini-select {
1476
+ background: var(--panel2);
1477
+ border: 1px solid var(--border);
1478
+ border-radius: 7px;
1479
+ color: var(--text);
1480
+ font-size: 12px;
1481
+ padding: 4px 6px;
1482
+ }
1483
+ .wl-row {
1484
+ display: flex; align-items: baseline; gap: 7px;
1485
+ background: var(--panel); border: 1px solid var(--border);
1486
+ border-radius: 8px; padding: 5px 8px;
1487
+ }
1488
+ .wl-row:hover { background: var(--panel2); }
1489
+ .wl-time { flex: none; color: var(--muted); font-size: 10px; min-width: 56px; }
1490
+ .wl-badge {
1491
+ flex: none; font-size: 9px; font-weight: 800; letter-spacing: 0.5px;
1492
+ padding: 1px 6px; border-radius: 5px; border: 1px solid var(--border);
1493
+ text-transform: uppercase;
1494
+ }
1495
+ .wl-badge.ok { color: var(--muted); }
1496
+ .wl-badge.trigger { color: var(--accent); border-color: var(--accent); }
1497
+ .wl-badge.error { color: var(--err); border-color: var(--err); }
1498
+ .wl-main { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 11.5px; }
1499
+ .wl-err { color: var(--err); font-size: 11px; }
1500
+
1467
1501
  /* #26 ajan dosya kartı */
1468
1502
  .file-card {
1469
1503
  display: flex;
@@ -1916,6 +1950,7 @@ body.browser-open #settingsDialog { width: min(70vw, calc(100vw - var(--bw, 480p
1916
1950
  flex: none;
1917
1951
  width: 250px;
1918
1952
  min-width: 250px;
1953
+ min-height: 0;
1919
1954
  background: var(--panel);
1920
1955
  border-left: 1px solid var(--border);
1921
1956
  display: flex;
@@ -1957,9 +1992,19 @@ body.browser-open #settingsDialog { width: min(70vw, calc(100vw - var(--bw, 480p
1957
1992
  }
1958
1993
  #railClear:hover { background: rgba(0, 0, 0, 0.08); }
1959
1994
  #railClear svg { display: block; }
1960
- }
1961
1995
 
1962
- #railList { flex: 1; overflow-y: auto; overflow-x: hidden; padding: 4px 8px; }
1996
+ /* rail listesi: kartlar ekranı aştığında KAYDIRILABİLİR olmalı —
1997
+ flex:1 + min-height:0 + overflow-y:auto üçlüsü zorunlu. overscroll-behavior
1998
+ sayfa geri kaydırma animasyonunu da keser. */
1999
+ #railList {
2000
+ flex: 1 1 auto;
2001
+ min-height: 0;
2002
+ overflow-y: auto;
2003
+ overflow-x: hidden;
2004
+ overscroll-behavior: contain;
2005
+ scrollbar-gutter: stable;
2006
+ padding: 4px 8px;
2007
+ }
1963
2008
 
1964
2009
  .rail-empty {
1965
2010
  color: var(--muted);