beast-agent 2.58.1 → 2.59.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.58.1",
4
+ "version": "2.59.0",
5
5
  "description": "Ultra-fast local agent shell for Windows.",
6
6
  "author": "algokodcom (AlgoKod)",
7
7
  "license": "MIT",
@@ -811,9 +811,13 @@ class Engine {
811
811
  payload = squeeze.apply(payload, { cacheKey: String(session.id || '') });
812
812
  }
813
813
  const cands = this._chatCandidates(sel, wasVision);
814
+ /* AYLIK KOTASI DOLAN SAĞLAYICILAR: aynı sağlayıcının diğer modellerini
815
+ denemek boşuna (kota hesap/workspace bazlı) — bu turda atlanır. */
816
+ const quotaOut = new Set();
814
817
  let lastErr = null;
815
818
  for (let i = 0; i < cands.length; i++) {
816
819
  const c = cands[i];
820
+ if (quotaOut.has(String(c.providerId))) continue;
817
821
  let retried413 = false;
818
822
  try {
819
823
  /* provider bazlı girdi limiti: payload aşıyorsa sıkıştırılıp öyle gönderilir.
@@ -903,6 +907,15 @@ class Engine {
903
907
  if (e && (e.name === 'AbortError' || (signal && signal.aborted))) throw e;
904
908
  lastErr = e;
905
909
  const msgStr = String((e && e.message) || '');
910
+ /* AYLIK KOTA: bu sağlayıcı tur boyunca kapalı — kullanıcıya net söyle,
911
+ FALLOUT varsa farklı sağlayıcıya geç */
912
+ if (e && e.usageLimit) {
913
+ quotaOut.add(String(c.providerId));
914
+ emitSafe(this, session.id, {
915
+ type: 'status',
916
+ status: `⛔ ${c.providerName}: aylık kullanım limiti doldu — aynı sağlayıcının diğer modelleri atlanıyor`,
917
+ });
918
+ }
906
919
  /* 413 TPM: sağlayıcı gerçek istek boyutunu söylüyor → kalibre et,
907
920
  aynı modeli yeniden sıkıştırıp BİR KEZ daha dene */
908
921
  const m413 = /Requested\s+([\d,]+)/i.exec(msgStr) || (e && e.status === 413 ? [, 0] : null);
@@ -925,7 +938,11 @@ class Engine {
925
938
  ([400, 404, 422].includes(e.status) ||
926
939
  /\bno endpoints\b/i.test(msgStr) ||
927
940
  /\bimage input\b/i.test(msgStr));
928
- const next = cands[i + 1];
941
+ /* sıradaki aday: kotası dolmuş sağlayıcılar atlanır */
942
+ let next = null;
943
+ for (let j = i + 1; j < cands.length; j++) {
944
+ if (!quotaOut.has(String(cands[j].providerId))) { next = cands[j]; break; }
945
+ }
929
946
  if (!next) break; // zincir bitti — hatayı yukarı fırlat
930
947
  if (imgIssue) {
931
948
  emitSafe(this, session.id, {
@@ -1417,15 +1434,21 @@ class Engine {
1417
1434
  files = fs.readdirSync(this.sessionsDir).filter((f) => f.endsWith('.jsonl'));
1418
1435
  } catch {}
1419
1436
  const out = [];
1437
+ const fin = [];
1420
1438
  for (const f of files) {
1421
1439
  const id = f.replace(/\.jsonl$/, '');
1422
1440
  const v = this._view(this._load(id));
1423
1441
  if (v.isBg) continue;
1424
1442
  if (v.isBotDm) continue; // botlar arası DM — yalnız admin DM Log ekranında
1425
- out.push(v);
1443
+ /* FİNANS GEÇMİŞİ KAYBOLMAZ: normal sohbet listesi 100 ile sınırlı olsa da
1444
+ finance sohbetleri AYRICA listelenir — 100+ oturumu olan kullanıcıda
1445
+ eski finance sohbetleri listeden düşüp "kaybolmuş" görünmez. */
1446
+ (v.finance ? fin : out).push(v);
1426
1447
  }
1427
- out.sort((a, b) => String(b.updatedAt).localeCompare(String(a.updatedAt)));
1428
- return out.slice(0, 100);
1448
+ const byNew = (a, b) => String(b.updatedAt).localeCompare(String(a.updatedAt));
1449
+ out.sort(byNew);
1450
+ fin.sort(byNew);
1451
+ return out.slice(0, 100).concat(fin.slice(0, 60)).sort(byNew);
1429
1452
  }
1430
1453
 
1431
1454
  /* Panel oturumları: belirli bir bgTitle taşıyan gizli oturumlar (Beast Code,
@@ -2009,9 +2032,10 @@ class Engine {
2009
2032
  modeBlock2 + '\n' +
2010
2033
  (roleBlockFull ? roleBlockFull + '\n' : '') +
2011
2034
  (teamLine || '') +
2012
- 'MT5 ARAÇLARI: mt5_status (bağlantı), mt5_account (hesap), mt5_market (canlı fiyat), mt5_positions (açık pozisyonlar), mt5_orders (bekleyen emirler), mt5_history (kapanan işlemler), mt5_alerts (fiyat alarmı: modu SEN seçersin — once/repeat), mt5_ea (BeastFinance grafik panosu: status/ping/chart/note), mt5_trade (TÜM TİPLER: buy/sell market + buy/sell limit/stop), mt5_close (TAM ya da KISMİ kapat: percent ile kısmi TP & kısmi stop — karar ajanın), mt5_modify (SL/TP), mt5_pending (TÜM TİPLER: buy_limit/sell_limit/buy_stop/sell_stop bekleyen + buy_market/sell_market anlık), mt5_cancel (emir iptal).\n' +
2035
+ 'MT5 ARAÇLARI: mt5_status (bağlantı), mt5_account (hesap), mt5_market (canlı fiyat), mt5_positions (açık pozisyonlar), mt5_orders (bekleyen emirler), mt5_history (kapanan işlemler), mt5_alerts (fiyat alarmı: kur/listele/sil — modu SEN seçersin: once/repeat), mt5_ea (BeastFinance grafik panosu: status/ping/chart/note), mt5_trade (TÜM TİPLER: buy/sell market + buy/sell limit/stop), mt5_close (TAM ya da KISMİ kapat: percent ile kısmi TP & kısmi stop — karar ajanın), mt5_modify (SL/TP), mt5_pending (TÜM TİPLER: buy_limit/sell_limit/buy_stop/sell_stop bekleyen + buy_market/sell_market anlık), mt5_cancel (emir iptal).\n' +
2013
2036
  'EMİR TİPLERİ (6 tip — HEPSİ AÇIK): buy_market/sell_market = anlık piyasa emri (fiyat beklenmez), buy_limit/sell_limit = bekleyen limit (price zorunlu), buy_stop/sell_stop = bekleyen stop (price zorunlu). mt5_trade ve mt5_pending İKİSİ DE 6 tipi kabul eder; emir tipini net söyle (ör. type:"sell_limit").\n' +
2014
2037
  'ALARM KARARI SENDE: mt5_alerts ile alarm kurarken mode ZORUNLU — "once" (tek seferlik; ilk tetiklemede kapanır) ya da "repeat" (tekrarlı; alarm açık kalır, koşul sürdükçe cooldownMin dakikada bir tekrar uyarır + seni uyandırır; cooldownMin varsayılan 5). Tekrarlı mı tek seferlik mi olacağını sen seç, varsayılana bırakma; seviyenin önemine göre cooldownMin belirle (ör. 5/15/60).\n' +
2038
+ 'ALARM TEMİZLİĞİ (ZORUNLU DİSİPLİN): alarm bir BEKÇİDİR, süs değil — gereksiz alarm biriktirme. Her turda (ya da en az birkaç turda bir) mt5_alerts action:"list" ile alarmları gör; listede "mine:true" senin kurduklarındır. Tezi/geçerliliği biten alarmı SİL: seviye kırıldı ve artık anlamsız, pozisyon kapandı, plan/hedef değişti, tetiklendi ve işlevi kalmadı. Silme: tekil kayıt action:"remove" {id} · kendi gereksizlerini toplu action:"clear" (varsayılan yalnız SENİN alarmların; symbol ile daralt; tümünü temizlemek için all:true — bunu başka ajanların alarmlarını da sileceği için YALNIZ açıkça gerekliyse kullan). Akan fiyatta işe yaramayan, üst üste aynı bölgeye kurulmuş alarmları temiz tut.\n' +
2015
2039
  'KISMİ KAPATMA YETKİSİ (TÜM finance ajanlarında): açık pozisyonda kısmi kapatma — kısmi TP (kâr al) ve kısmi stop (zarar kes) — TAMAMEN SENİN KARARIN, OTOMATİK DEĞİLDİR; mt5_close {ticket, percent, kind:"partial_tp"|"partial_sl", reason}. Gerek gördüğünde kullan; kalan pozisyon SL/TP ile devam eder. Kısmi TP fırsatı bildirimi gelirse (fiyat seviyeye ulaştı) kapatıp kapatmamak sana bağlı.\n' +
2016
2040
  'KİŞİSEL MT5 ARAÇLARI (VARSAYILAN KURULU — tool__* tüm oturumlarda çağrılabilir): tool__mt5_shot (grafikten GERÇEK PNG + görsel ajana enjekte — grafik işlerinde BİRİNCİL; timeframes:["M1","M15"] ile çoklu periyot, symbols:["GOLD","EURUSD","BTCUSD"] ile çoklu sembol TEK karede), tool__mt5_barlar2 (OHLC mumlar, köprüsüz), tool__mt5_m15_m5 (GOLD M15+M5 hazır paket), tool__mt5_fiyat, tool__mt5_durum (hesap+pozisyon+emir tek çağrı), tool__mt5_gecmis, tool__mt5_pozisyon_gecmis, tool__mt5_sltp, tool__mt5_kapat, tool__mt5_bekleyen, tool__mt5_emir, tool__mt5_emir_iptal.\n' +
2017
2041
  'BEASTFINANCE EA (OTOMATİK GRAFİK UZMANI — ENTEGRASYON KANALI): MT5 terminaline bağlanıldığında BeastFinance uzman danışmanı İLK GRAFİĞE OTOMATİK yüklenir, AutoTrading izni açılır (kurulum sistem tarafından yapılır; elle ekleme gerekmez). Grafik panosu + seviye çizgileri + dosya köprüsü (beast_ea.json / beast_cmd.json / beast_note.json) bu EA üzerinden yürür: entegrasyon işlerinde (status/ping/chart/note) mt5_ea kullan; ekran görüntüsü işlerinde tool__mt5_shot ("shot" komutu — gerçek PNG + ajan görseli) birincildir. Yazdığın not ve çizgiler tool__mt5_shot ve computer_look screenshot\'ında GÖRÜNÜR.\n' +
@@ -200,12 +200,12 @@ const definitions = NAMES.map((name) => {
200
200
  },
201
201
  mt5_alerts: {
202
202
  description:
203
- 'FİYAT ALARMI: mt5_alerts {action:"list"|"set"|"remove"}. set: {symbol, price, direction:"above"|"below", mode:"once"|"repeat", note?, cooldownMin?}. ALARM MODUNU KURAN AJAN (sen) SEÇERSİN — mode zorunlu: "once" tek seferlik (ilk tetiklemede kapanır) ya da "repeat" tekrarlı (alarm açık kalır, koşul sürdükçe cooldownMin dakikada bir tekrar bildirir + sahibi ajanı uyandırır; cooldownMin varsayılan 5). Örn kritik seviye: XAUUSD 3400 üstü → mode:"repeat", cooldownMin:15; tek kere haber: mode:"once".',
203
+ 'FİYAT ALARMI: mt5_alerts {action:"list"|"set"|"remove"|"clear"}. set: {symbol, price, direction:"above"|"below", mode:"once"|"repeat", note?, cooldownMin?}. ALARM MODUNU KURAN AJAN (sen) SEÇERSİN — mode zorunlu: "once" tek seferlik (ilk tetiklemede kapanır) ya da "repeat" tekrarlı (alarm açık kalır, koşul sürdükçe cooldownMin dakikada bir tekrar bildirir + sahibi ajanı uyandırır; cooldownMin varsayılan 5). Örn kritik seviye: XAUUSD 3400 üstü → mode:"repeat", cooldownMin:15; tek kere haber: mode:"once". TEMİZLİK: alarm bir bekçidir, süs değil — gereksiz alarm biriktirme; her turda list ile KENDİ alarmlarını gör, işi bitenleri (tez geçersiz, pozisyon kapandı, seviye anlamsız) clear ile SİL. clear varsayılanı YALNIZ senin kurduğun alarmlar; symbol ile daralt; ids:[...] belirli alarmlar; all:true tüm finance alarmları.',
204
204
  parameters: {
205
205
  type: 'object',
206
206
  properties: {
207
- action: { type: 'string', enum: ['list', 'set', 'remove'] },
208
- symbol: { type: 'string', description: 'set için sembol' },
207
+ action: { type: 'string', enum: ['list', 'set', 'remove', 'clear'] },
208
+ symbol: { type: 'string', description: 'set/clear için sembol (clear: yalnız bu sembole daralt)' },
209
209
  price: { type: 'number', description: 'set için tetik fiyatı' },
210
210
  direction: { type: 'string', enum: ['above', 'below'], description: 'above: fiyat ≥ price; below: fiyat ≤ price' },
211
211
  mode: {
@@ -217,6 +217,8 @@ const definitions = NAMES.map((name) => {
217
217
  cooldownMin: { type: 'number', description: "mode:'repeat' için iki tetik arası en az dakika (varsayılan 5, 0 = her tetiklemede, max 10080)" },
218
218
  once: { type: 'boolean', description: "mode'a alternatif kısa yol (true = tek seferlik)" },
219
219
  id: { type: 'string', description: 'remove için alarm id' },
220
+ ids: { type: 'array', items: { type: 'string' }, description: 'clear için: silinecek alarm id listesi (verilirse sahiplik aranmaz)' },
221
+ all: { type: 'boolean', description: 'clear için: true → sahiplik filtresini kaldır, kalan TÜM finance alarmlarını sil (varsayılan: yalnız kendi kurdukların)' },
220
222
  },
221
223
  required: ['action'],
222
224
  },
@@ -420,7 +422,9 @@ const handlers = {
420
422
  async mt5_alerts(args, ctx) {
421
423
  const action = String(args.action || 'list').toLowerCase();
422
424
  if (action === 'list') {
423
- const list = alertsApi.list();
425
+ /* mine:true → bu oturumun (senin) kurduğu alarm — temizlik kararı için */
426
+ const sid = ctx && ctx.sessionId ? String(ctx.sessionId) : '';
427
+ const list = alertsApi.list().map((a) => ({ ...a, mine: sid ? String(a.sid || '') === sid : !a.sid }));
424
428
  return { ok: true, count: list.length, alerts: list };
425
429
  }
426
430
  if (action === 'remove') {
@@ -429,6 +433,20 @@ const handlers = {
429
433
  const removed = alertsApi.remove(id);
430
434
  return { ok: !!removed, removed: !!removed, id };
431
435
  }
436
+ if (action === 'clear') {
437
+ /* ALARM TEMİZLİĞİ: alarmı kuran ajan kendi gereksiz alarmlarını siler.
438
+ Varsayılan kapsam: bu oturumun kurdukları (ctx.sessionId); symbol ile
439
+ daralt; ids açıkça verilirse sahiplik aranmaz; all:true → tümü. */
440
+ if (typeof alertsApi.clear !== 'function') return { ok: false, error: 'toplu alarm temizliği desteklenmiyor' };
441
+ const ids = Array.isArray(args.ids) ? args.ids.map((x) => String(x || '').trim()).filter(Boolean) : [];
442
+ const all = args.all === true || String(args.all || '').toLowerCase() === 'true' || String(args.scope || '').toLowerCase() === 'all';
443
+ const symbol = String(args.symbol || '').trim().toUpperCase();
444
+ const sid = ctx && ctx.sessionId ? String(ctx.sessionId) : '';
445
+ const res = alertsApi.clear({ ids, sid, all, symbol }) || {};
446
+ const removed = Number(res.removed) || 0;
447
+ if (removed > 0) noteTrade('alert-clear', { removed, ids: res.ids || [], symbol, all }, ctx || {});
448
+ return { ok: true, removed, ids: res.ids || [] };
449
+ }
432
450
  if (action === 'set') {
433
451
  const symbol = String(args.symbol || '').trim().toUpperCase();
434
452
  const price = Number(args.price);
@@ -466,7 +484,7 @@ const handlers = {
466
484
  noteTrade('alert-set', { symbol, price, direction, id: alarm.id, note: alarm.note, cooldownMin: alarm.cooldownMin, once: alarm.once }, ctx || {});
467
485
  return { ok: true, alert: alarm };
468
486
  }
469
- return { ok: false, error: "action: list|set|remove" };
487
+ return { ok: false, error: "action: list|set|remove|clear" };
470
488
  },
471
489
  async mt5_note(args, ctx) {
472
490
  const symbol = String(args.symbol || '').trim().toUpperCase();
@@ -159,6 +159,57 @@ function alertCooldownActive(a, now) {
159
159
  return last > 0 && Number(now) - last < cd;
160
160
  }
161
161
 
162
+ /* ---- alarm temizliği (ajan kendi kurduğu gereksiz alarmı siler) ----
163
+ opts:
164
+ ids → yalnız bu id'ler (açıkça verilenler sahiplikten bağımsız silinir)
165
+ sid → alarmı kuran ajan oturumu; scope varsayılanı "kendi alarmlarım"
166
+ symbol → yalnız bu sembole daralt
167
+ all → sahiplik filtresini kaldır (tüm finance alarmları)
168
+ Dönüş: silinecek alarm id listesi (saf; IO yok). */
169
+ function pickAlarms(alerts, opts) {
170
+ const o = opts || {};
171
+ const ids = Array.isArray(o.ids) ? o.ids.map((x) => String(x || '')).filter(Boolean) : [];
172
+ const sid = String(o.sid || '');
173
+ const symbol = String(o.symbol || '').toUpperCase();
174
+ const all = !!o.all;
175
+ const out = [];
176
+ for (const a of Array.isArray(alerts) ? alerts : []) {
177
+ if (!a || !a.id) continue;
178
+ if (ids.length) {
179
+ if (ids.includes(String(a.id))) out.push(String(a.id));
180
+ continue;
181
+ }
182
+ if (symbol && String(a.symbol || '').toUpperCase() !== symbol) continue;
183
+ if (all) {
184
+ out.push(String(a.id));
185
+ continue;
186
+ }
187
+ /* sid varsa YALNIZ kendi alarmları; sid yoksa (işçi oturumu) sahipsizler */
188
+ const owner = String(a.sid || '');
189
+ if (sid ? owner === sid : !owner) out.push(String(a.id));
190
+ }
191
+ return out;
192
+ }
193
+
194
+ /* ---- trade saatleri (makinenin YEREL saati) ----
195
+ th: { on, start:'HH:MM', end:'HH:MM' }. on değilse 7/24 açık (true).
196
+ Açıkken yalnız [start,end) aralığında true döner; gece aralığı desteklenir
197
+ (ör. 22:00 → 06:00). start === end → 24 saat. Saf; IO yok. */
198
+ function parseHM(v, defMin) {
199
+ const m = /^([01]?\d|2[0-3]):([0-5]\d)$/.exec(String(v || '').trim());
200
+ if (!m) return defMin;
201
+ return Number(m[1]) * 60 + Number(m[2]);
202
+ }
203
+ function tradeHoursOpen(th, now) {
204
+ if (!th || !th.on) return true;
205
+ const s = parseHM(th.start, 9 * 60);
206
+ const e = parseHM(th.end, 22 * 60);
207
+ if (s === e) return true;
208
+ const d = now instanceof Date ? now : new Date(Number(now) || Date.now());
209
+ const cur = d.getHours() * 60 + d.getMinutes();
210
+ return s < e ? cur >= s && cur < e : cur >= s || cur < e;
211
+ }
212
+
162
213
  /* ---- bekleyen emir (pending) → pozisyon eşleştirme ----
163
214
  Watchdog turu emir listesini karşılaştırır: listeden DÜŞEN emir için aynı
164
215
  turda YENİ açılan (ya da netting hesapta hacmi artan) eşleşen bir pozisyon
@@ -228,4 +279,4 @@ function matchPendingDelta(goneOrders, freshPositions) {
228
279
  return out;
229
280
  }
230
281
 
231
- module.exports = { plan, roundTo, stepRound, closeKind, parseOrderType, alertCooldownActive, orderSide, orderTypeLabel, orderVolume, matchPendingDelta };
282
+ module.exports = { plan, roundTo, stepRound, closeKind, parseOrderType, alertCooldownActive, pickAlarms, parseHM, tradeHoursOpen, orderSide, orderTypeLabel, orderVolume, matchPendingDelta };
package/src/agent/llm.js CHANGED
@@ -64,7 +64,27 @@ const HINTS = {
64
64
  504: 'sağlayıcı zaman aşımı',
65
65
  };
66
66
 
67
+ /* AYLIK KOTA / KULLANIM LİMİTİ: 429 gövdesindeki "usage limit / quota" sinyali
68
+ hız limitinden FARKLI — tekrar denemek çözmez, aynı sağlayıcının diğer
69
+ modelleri de kapalıdır. Bu ayrım motorun FALLOUT kararını da besler. */
70
+ function isUsageLimitDetail(detail) {
71
+ return /GoUsageLimit|usage[_ ]?limit|insufficient_quota|monthly limit|kullanım limiti/i.test(
72
+ String(detail || '')
73
+ );
74
+ }
75
+
67
76
  function friendlyError(status, statusText, detail) {
77
+ const d = String(detail || '');
78
+ if (status === 429 && isUsageLimitDetail(d)) {
79
+ let msg = 'HTTP 429 — AYLIK KULLANIM LİMİTİ DOLDU (hız limiti değil)';
80
+ const reset = /Resets?\s+in\s+([^."}]+)/i.exec(d);
81
+ if (reset) msg += ` — sıfırlanma: ${reset[1].trim()} sonra`;
82
+ const link = /(https?:\/\/[^\s"}]+)/.exec(d);
83
+ if (link) msg += `\nDevam için: ${link[1]}`;
84
+ msg +=
85
+ '\n(çözüm: sağlayıcı panelinden bakiyeden kullanımı aç, başka model/sağlayıcı seç ya da sıfırlanmayı bekle)';
86
+ return msg;
87
+ }
68
88
  const hint = HINTS[status];
69
89
  let msg = `HTTP ${status} ${statusText}`;
70
90
  if (hint) msg += ` — ${hint}`;
@@ -72,9 +92,33 @@ function friendlyError(status, statusText, detail) {
72
92
  if ([500, 502, 503, 504].includes(status)) {
73
93
  msg += '\n(geçici olabilir: birkaç saniye sonra tekrar dene ya da model seçiciyi açıp başka bir model dene)';
74
94
  }
95
+ if (isTransientDetail(status, d)) {
96
+ msg += '\n(sağlayıcı geçici hatası — otomatik tekrar denenir; sürerse başka model/sağlayıcı dene)';
97
+ }
75
98
  return msg;
76
99
  }
77
100
 
101
+ /* SAĞLAYICI GEÇİCİ HATASI: bazı sağlayıcılar upstream arızasını HTTP 400
102
+ gövdesinde "server_error" olarak döner — istemci hatası değildir, tekrar
103
+ denenmeli. Bu bayrak olmadan 400 kalıcı sayılıp model boşuna "çökmüş" olur. */
104
+ function isTransientDetail(status, detail) {
105
+ if (status !== 400 && status !== 500) return false;
106
+ return /"(type|code)"\s*:\s*"server_error"|Error from provider|Upstream (request failed|response was not valid JSON)|temporar/i.test(
107
+ String(detail || '')
108
+ );
109
+ }
110
+
111
+ /* HTTP hatasını zengin bayraklarla kur — usageLimit/transient bilgisi retry ve
112
+ FALLOUT katmanlarına taşınır (boşa tekrar/boşa çökme olmaz). */
113
+ function httpError(status, statusText, detail, headers) {
114
+ const err = new Error(friendlyError(status, statusText, detail));
115
+ err.status = status;
116
+ if (isUsageLimitDetail(detail)) err.usageLimit = true;
117
+ if (isTransientDetail(status, detail)) err.transient = true;
118
+ err.retryAfterMs = parseRetryAfter(headers && (headers.get('retry-after-ms') || headers.get('retry-after')));
119
+ return err;
120
+ }
121
+
78
122
  /* opencode cache disiplini (provider/transform.ts:1262-1276 port): destekleyen
79
123
  sağlayıcılara oturum-sabit önbellek anahtarı gönderilir — aynı önek tekrar
80
124
  kullanıldığında girdi tokenleri cache'ten okunur (~%90 ucuz + hızlı TTFT) */
@@ -302,9 +346,15 @@ async function withRetries(fn, { signal, onRetry } = {}) {
302
346
  return await fn();
303
347
  } catch (e) {
304
348
  const aborted = e && (e.name === 'AbortError' || (signal && signal.aborted));
349
+ if (aborted) throw e;
350
+ /* AYLIK KOTA DOLDU: tekrar denemek boşa — hemen fırlat ki motor
351
+ FALLOUT ile sıradaki sağlayıcıya/modele geçebilsin. */
352
+ if (e && e.usageLimit) throw e;
305
353
  const status = e && e.status;
306
- const netErr = !aborted && status === undefined;
307
- const retriable = !aborted && (status === undefined || RETRYABLE_STATUS.has(status));
354
+ const netErr = status === undefined;
355
+ /* 400 gövdesinde "server_error/upstream" diyen sağlayıcı hataları geçicidir
356
+ (transient) — kalıcı 400 gibi davranıp modeli boşuna "çökertme". */
357
+ const retriable = status === undefined || RETRYABLE_STATUS.has(status) || !!(e && e.transient);
308
358
  if (!retriable) throw e;
309
359
  const cap = netErr ? NET_MAX_RETRIES : MAX_RETRIES;
310
360
  if (attempt >= cap) throw e;
@@ -409,12 +459,7 @@ async function streamOnce(sel, body, { signal, onDelta, onRetry } = {}, drops =
409
459
  try {
410
460
  detail = (await r.text()).slice(0, 300);
411
461
  } catch {}
412
- const err = new Error(friendlyError(r.status, r.statusText, detail));
413
- err.status = r.status;
414
- err.retryAfterMs = parseRetryAfter(
415
- r.headers && (r.headers.get('retry-after-ms') || r.headers.get('retry-after'))
416
- );
417
- throw err;
462
+ throw httpError(r.status, r.statusText, detail, r.headers);
418
463
  }
419
464
  return r;
420
465
  },
@@ -590,12 +635,7 @@ async function chatOnce(sel, body, { signal, onRetry, onParamDrop, onEffortDowng
590
635
  try {
591
636
  detail = (await r.text()).slice(0, 300);
592
637
  } catch {}
593
- const err = new Error(friendlyError(r.status, r.statusText, detail));
594
- err.status = r.status;
595
- err.retryAfterMs = parseRetryAfter(
596
- r.headers && (r.headers.get('retry-after-ms') || r.headers.get('retry-after'))
597
- );
598
- throw err;
638
+ throw httpError(r.status, r.statusText, detail, r.headers);
599
639
  }
600
640
  return r;
601
641
  },
package/src/main.js CHANGED
@@ -9400,6 +9400,13 @@ function finCfg() {
9400
9400
  /* GÜNLÜK RUTİN: plan/review saatleri (HH:MM; boş = kapalı) — hafta içi cron */
9401
9401
  if (typeof f.planTime !== 'string') f.planTime = '';
9402
9402
  if (typeof f.reviewTime !== 'string') f.reviewTime = '';
9403
+ /* TRADE SAATLERİ: varsayılan KAPALI (7/24). Açıkken Beast Finance ve tüm
9404
+ ajanları YALNIZ [start,end) aralığında çalışır — makinenin YEREL saati;
9405
+ varsayılan aralık 09:00-22:00; gece aralığı desteklenir (22:00 → 06:00). */
9406
+ if (!f.tradeHours || typeof f.tradeHours !== 'object') f.tradeHours = {};
9407
+ if (typeof f.tradeHours.on !== 'boolean') f.tradeHours.on = false;
9408
+ if (!/^([01]?\d|2[0-3]):([0-5]\d)$/.test(String(f.tradeHours.start || '').trim())) f.tradeHours.start = '09:00';
9409
+ if (!/^([01]?\d|2[0-3]):([0-5]\d)$/.test(String(f.tradeHours.end || '').trim())) f.tradeHours.end = '22:00';
9403
9410
  /* RİSK OTOMASYONU (watchdog): +R'da BE, trailing, kısmi TP — main süreç
9404
9411
  saniyelik döngüyle uygular; ajan turunu BEKLEMEZ. 0 = ilgili kural kapalı. */
9405
9412
  if (typeof f.watchdog !== 'boolean') f.watchdog = true;
@@ -9446,6 +9453,26 @@ function finCfg() {
9446
9453
  return f;
9447
9454
  }
9448
9455
 
9456
+ /* ---- TRADE SAATLERİ KAPISI (makinenin YEREL saati) ----
9457
+ tradeHours.on true iken ajan turları ve olay uyandırmaları yalnız
9458
+ [start,end) aralığında çalışır; gece aralığı desteklenir (ör. 22:00 →
9459
+ 06:00). Kapalıyken (varsayılan) 7/24 serbesttir. Açık pozisyon koruması
9460
+ (watchdog: BE/trailing/kısmi TP) bu kapıdan ETKİLENMEZ — risk güvenliği
9461
+ her saat sürer; kapı yalnız ajan ÜRETİMİNİ (tur/karar/uyandırma) kısar.
9462
+ Karar katmanı saf fonksiyondur: finwatch.tradeHoursOpen (birim testli). */
9463
+ function finTradeHoursOpen(now) {
9464
+ return finwatch.tradeHoursOpen(finCfg().tradeHours, now);
9465
+ }
9466
+ /* Kapı kaynaklı ertelemeler günlüğe SEYREK düşer (10 dk'da en fazla 1 satır) */
9467
+ function finTradeHoursBlockedLog(what) {
9468
+ const now = Date.now();
9469
+ if (now - Number(financeState.tradeHoursLogAt || 0) < 10 * 60 * 1000) return false;
9470
+ financeState.tradeHoursLogAt = now;
9471
+ const th = finCfg().tradeHours || {};
9472
+ financeLog(`[saat] trade saatleri dışı (${th.start || '09:00'}-${th.end || '22:00'} yerel) — ${what} ertelendi`);
9473
+ return true;
9474
+ }
9475
+
9449
9476
  /* ANALİZ EKİBİ: trader'ın yanında koşan uzman ajan rolleri — seçilen her rol
9450
9477
  AYRI bir sürekli finance ajanı açar. Hepsi tüm SKILL'lere + mt5 okuma
9451
9478
  araçlarına erişir (finance oturumu oldukları için), İŞLEM AÇMAZLAR.
@@ -9943,6 +9970,21 @@ function finAlertApi() {
9943
9970
  }
9944
9971
  return false;
9945
9972
  },
9973
+ /* TOPLU TEMİZLİK: alarmı KURAN ajan kendi gereksiz alarmlarını siler.
9974
+ Varsayılan kapsam: yalnız bu oturumun (sid) kurduğu alarmlar; symbol
9975
+ verilirse sembole daralır; ids açıkça verilirse sahiplik aranmaz;
9976
+ all:true → sahiplik filtresi kalkar (tüm finance alarmları). */
9977
+ clear: (opts) => {
9978
+ const o = opts || {};
9979
+ const ids = finwatch.pickAlarms(financeState.alerts, o);
9980
+ if (!ids.length) return { removed: 0, ids: [] };
9981
+ const drop = new Set(ids);
9982
+ financeState.alerts = financeState.alerts.filter((a) => !drop.has(String(a.id)));
9983
+ finAlertsSave();
9984
+ const scope = Array.isArray(o.ids) && o.ids.length ? 'verilen id\'ler' : o.all ? 'tüm alarmlar' : 'ajanın kendi alarmları';
9985
+ financeLog(`[alarm] temizlendi: ${ids.length} adet (${scope}${o.symbol ? ' · ' + String(o.symbol).toUpperCase() : ''})`);
9986
+ return { removed: ids.length, ids };
9987
+ },
9946
9988
  };
9947
9989
  }
9948
9990
 
@@ -10367,6 +10409,12 @@ function finWakeAgentDm(sid, text) {
10367
10409
  const id = String(sid || '');
10368
10410
  const agent = financeState.agents.get(id);
10369
10411
  if (!agent || !engine) return false;
10412
+ /* TRADE SAATLERİ KAPISI: aralık dışında olay ajanı UYANDIRMAZ (panel/kanal
10413
+ bildirimi düşmeye devam eder; tur pencere açılınca kendiliğinden başlar) */
10414
+ if (!finTradeHoursOpen()) {
10415
+ finTradeHoursBlockedLog('olay uyandırması');
10416
+ return false;
10417
+ }
10370
10418
  try {
10371
10419
  const body = agent.role
10372
10420
  ? String(text || '') + "\n(ROL HATIRLATMASI: işlem AÇMA — durumu analiz et; gerekiyorsa agent_dm ile ANA TRADER'a bildir.)"
@@ -10527,11 +10575,12 @@ async function finCheckAlerts() {
10527
10575
  sahip koşmuyor/biliniyorsa koşan tüm finance ajanlarına düşer. */
10528
10576
  const wakeTxt =
10529
10577
  `[FİNANS OLAYI — FİYAT ALARMI${tekrar}, TUR SANİYESİ BEKLENMEDEN İLETİLDİ]\n` +
10530
- `- ${a.symbol} ${a.direction === 'below' ? '≤' : '≥'} ${a.price} tetiklendi (şimdi ${p.bid})${a.note ? ' — ' + a.note : ''}${cdTxt}\n` +
10578
+ `- ${a.symbol} ${a.direction === 'below' ? '≤' : '≥'} ${a.price} tetiklendi (şimdi ${p.bid})${a.note ? ' — ' + a.note : ''}${cdTxt} (id: ${a.id})\n` +
10531
10579
  (a.once
10532
10580
  ? 'Alarm TEK SEFERLİKTİ ve kapandı. '
10533
10581
  : `Alarm TEKRARLI açık kalıyor${a.cooldownMin > 0 ? ` — koşul sürerse en erken ${a.cooldownMin} dk sonra yeniden uyarır` : ''}. `) +
10534
- 'Şimdi yap: alarmı kurma nedenini hatırla; fiyat seviyesini ve planını değerlendir, gerekiyorsa işlem/uyarı üret. Kısa rapor ver.';
10582
+ 'Şimdi yap: alarmı kurma nedenini hatırla; fiyat seviyesini ve planını değerlendir, gerekiyorsa işlem/uyarı üret. ' +
10583
+ 'TEMİZLİK: alarmın görevi bittiyse (tez geçersiz, pozisyon kapandı, seviye anlamsızlaştı) mt5_alerts action:"remove" {id} ya da action:"clear" ile SİL — gereksiz alarm biriktirme. Kısa rapor ver.';
10535
10584
  if (!finWakeAgentDm(a.sid, wakeTxt)) finWakeAgents(wakeTxt, { kind: 'alarm', symbol: a.symbol });
10536
10585
  }
10537
10586
  if (changed) finAlertsSave();
@@ -11453,6 +11502,10 @@ function finTraderBrief(agent) {
11453
11502
  'finance-review' job'ları tetikler. Trader koşmuyorsa sessizce atlanır. */
11454
11503
  function finDailyRoutine(mode) {
11455
11504
  try {
11505
+ if (!finTradeHoursOpen()) {
11506
+ financeLog('[rutin] trade saatleri dışı — günlük ' + mode + ' atlandı');
11507
+ return;
11508
+ }
11456
11509
  let mainSid = '';
11457
11510
  for (const [sid, a] of financeState.agents) {
11458
11511
  if (a && a.main) { mainSid = String(sid); break; }
@@ -11554,6 +11607,15 @@ async function finConsultPlan(f, agent, sid) {
11554
11607
  function finAgentRound(sid) {
11555
11608
  const agent = financeState.agents.get(String(sid));
11556
11609
  if (!agent || !engine) return;
11610
+ /* TRADE SAATLERİ KAPISI: aralık dışında YENİ TUR BAŞLATILMAZ — ajan askıya
11611
+ alınır; pencere açılınca 60 sn içinde kaldığı yerden devam eder */
11612
+ if (!finTradeHoursOpen()) {
11613
+ finTradeHoursBlockedLog(`tur #${(Number(agent.round) || 0) + 1}`);
11614
+ clearTimeout(agent.timer);
11615
+ agent.timer = setTimeout(() => { try { finAgentRound(sid); } catch {} }, 60000);
11616
+ if (agent.main) finPush('trader', { state: 'hours', round: agent.round, nextInSec: 60 });
11617
+ return;
11618
+ }
11557
11619
  if (engine.isBusy(sid)) {
11558
11620
  /* hâlâ çalışıyor — done eventinde tekrar planlanır */
11559
11621
  return;
@@ -11616,6 +11678,12 @@ function finWakeAgent(sid) {
11616
11678
  const id = String(sid || '');
11617
11679
  const agent = financeState.agents.get(id);
11618
11680
  if (!agent || !engine) return;
11681
+ /* TRADE SAATLERİ KAPISI: aralık dışında DM uyandırması tur AÇMAZ; mesaj
11682
+ inbox'ta bekler, pencere açılınca okunur */
11683
+ if (!finTradeHoursOpen()) {
11684
+ finTradeHoursBlockedLog('DM uyandırması');
11685
+ return;
11686
+ }
11619
11687
  if (engine.isBusy(id)) {
11620
11688
  /* tur sürüyor: olay dmInbox'ta bekler — tur bitince finFlushOnDone
11621
11689
  BEKLEMEDEN yeni tur açar (interval beklenmez) */
@@ -11903,6 +11971,20 @@ ipcMain.handle('finance:settings', async (_e, patch) => {
11903
11971
  if (p.shadowMode !== undefined) f.shadowMode = !!p.shadowMode;
11904
11972
  if (p.planTime !== undefined) f.planTime = /^([01]?\d|2[0-3]):([0-5]\d)$/.test(String(p.planTime || '').trim()) ? String(p.planTime).trim() : '';
11905
11973
  if (p.reviewTime !== undefined) f.reviewTime = /^([01]?\d|2[0-3]):([0-5]\d)$/.test(String(p.reviewTime || '').trim()) ? String(p.reviewTime).trim() : '';
11974
+ /* TRADE SAATLERİ: {on, start, end} — yerel saat; aralık dışında ajan turları
11975
+ ve olay uyandırmaları durur (varsayılan kapalı, aralık 09:00-22:00) */
11976
+ if (p.tradeHours !== undefined && p.tradeHours && typeof p.tradeHours === 'object') {
11977
+ const th = f.tradeHours && typeof f.tradeHours === 'object' ? f.tradeHours : {};
11978
+ if (typeof p.tradeHours.on === 'boolean') th.on = p.tradeHours.on;
11979
+ for (const k of ['start', 'end']) {
11980
+ if (p.tradeHours[k] !== undefined) {
11981
+ const v = String(p.tradeHours[k] || '').trim().slice(0, 5);
11982
+ if (/^([01]?\d|2[0-3]):([0-5]\d)$/.test(v)) th[k] = v;
11983
+ }
11984
+ }
11985
+ f.tradeHours = th;
11986
+ if (th.on && !finTradeHoursOpen()) financeLog('[saat] trade saatleri açık — ajanlar ' + th.start + '-' + th.end + ' aralığında çalışacak (yerel saat)');
11987
+ }
11906
11988
  if (p.pythonPath !== undefined) f.pythonPath = String(p.pythonPath || '').trim();
11907
11989
  if (p.terminalPath !== undefined) f.terminalPath = String(p.terminalPath || '').trim();
11908
11990
  if (p.traderSel !== undefined) {
@@ -892,6 +892,20 @@
892
892
  </label>
893
893
  <div class="fin-set-note">RİSK OTOMASYONU gizliyken koruma döngüsü (watchdog) kapatılır; tekrar gösterdiğinde ilgili kutucuktan açabilirsin.</div>
894
894
  <div class="fin-set-sep"></div>
895
+ <label class="fin-set-row" title="Varsayılan KAPALI: kapalıyken ajanlar 7/24 çalışır. Açıkken Beast Finance ve tüm ajanları YALNIZ belirlenen saat aralığında çalışır — aralık dışında yeni tur başlatılmaz ve olaylar ajanı uyandırmaz (makinenin YEREL saati). Açık pozisyon koruması (BE/trailing/kısmi TP) her saat çalışmaya devam eder.">
896
+ <input id="finTradeHoursOn" type="checkbox" />
897
+ <span class="fsr-main"><b>TRADE SAATLERİ</b><i>Kapalıyken 7/24 · açıkken ajanlar yalnız bu aralıkta</i></span>
898
+ </label>
899
+ <div id="finTradeHoursWrap" class="fin-set-sub" hidden>
900
+ <span class="fin-set-lbl">BAŞLANGIÇ / BİTİŞ (YEREL SAAT)</span>
901
+ <div class="fin-th-row">
902
+ <input id="finTradeHoursStart" type="time" step="60" value="09:00" />
903
+ <span class="fin-set-lbl">→</span>
904
+ <input id="finTradeHoursEnd" type="time" step="60" value="22:00" />
905
+ </div>
906
+ <div class="fin-set-note" style="margin:0">Aralık dışında ajan turları askıya alınır, olay alarmları ajanı uyandırmaz; pencere açılıp kapanınca kaldığı yerden devam eder. Gece aralığı da verilebilir (ör. 22:00 → 06:00).</div>
907
+ </div>
908
+ <div class="fin-set-sep"></div>
895
909
  <label class="fin-set-row" title="Açıkken botu eklediğin Telegram grubuna yazılan ilk mesaj o grubu AJAN DM aynası yapar. Her AJAN DM grubu Telegram'da AYRI BAŞLIK (topic) olarak canlı akar; başlığa yazdıkların ilgili AJAN DM grubunda görünür ve o grubun ajanları uyanır. Grup/Konu seçmene gerek yok.">
896
910
  <input id="finDmTgOn" type="checkbox" />
897
911
  <span class="fsr-main"><b>AJAN DM ↔ TELEGRAM</b><i>Her ajan grubu Telegram'da ayrı başlık — canlı</i></span>
@@ -208,6 +208,10 @@ const els = {
208
208
  finDmTgState: $('#finDmTgState'),
209
209
  finDmTgUnbind: $('#finDmTgUnbind'),
210
210
  finDmTgHint: $('#finDmTgHint'),
211
+ finTradeHoursOn: $('#finTradeHoursOn'),
212
+ finTradeHoursWrap: $('#finTradeHoursWrap'),
213
+ finTradeHoursStart: $('#finTradeHoursStart'),
214
+ finTradeHoursEnd: $('#finTradeHoursEnd'),
211
215
  finRiskCard: $('#finRiskCard'),
212
216
  finNotifyCard: $('#finNotifyCard'),
213
217
  finPerfCard: $('#finPerfCard'),
@@ -9997,6 +10001,7 @@ function finRenderAutomation(cfg, watch) {
9997
10001
  if (els.finWatchInfo) {
9998
10002
  els.finWatchInfo.textContent = watch && watch.on ? '· izlenen ' + (watch.managed || 0) : '';
9999
10003
  }
10004
+ finTradeHoursPaint();
10000
10005
  }
10001
10006
 
10002
10007
  function finSparkline(points) {
@@ -10158,6 +10163,7 @@ function finOnEvent(ev) {
10158
10163
  } else if (ev.fn === 'trader') {
10159
10164
  if (ev.state === 'running') finLogLine('[trader] tur #' + (ev.round || 0) + ' başladı');
10160
10165
  else if (ev.state === 'idle') finLogLine('[trader] tur #' + (ev.round || 0) + ' bitti — sıradaki tur ~' + (ev.nextInSec || '?') + ' sn');
10166
+ else if (ev.state === 'hours') finLogLine('[trader] trade saatleri dışı — ajan askıda (kontrol ~' + (ev.nextInSec || 60) + ' sn)');
10161
10167
  else if (ev.state === 'stopped') finLogLine('[trader] durduruldu');
10162
10168
  } else if (ev.fn === 'install') {
10163
10169
  finLogLine('[MT5] paket kurulumu tamamlandı (kod ' + ev.code + ')');
@@ -10319,11 +10325,37 @@ function finDmTgPaint() {
10319
10325
  function finSettingsOpen() {
10320
10326
  if (els.finSettingsOverlay) els.finSettingsOverlay.hidden = false;
10321
10327
  finDmTgPaint();
10328
+ finTradeHoursPaint();
10322
10329
  }
10323
10330
  function finSettingsCloseModal() {
10324
10331
  if (els.finSettingsOverlay) els.finSettingsOverlay.hidden = true;
10325
10332
  }
10326
10333
 
10334
+ /* ---------- TRADE SAATLERİ (görünüm ayarları modalı) ----------
10335
+ Ayarlar sunucuda (settings.finance.tradeHours) yaşar: on + start/end.
10336
+ Kapalıyken ajanlar 7/24; açıkken yalnız [start,end) aralığında çalışır
10337
+ (makinenin yerel saati). Açık pozisyon koruması her saat sürer. */
10338
+ function finTradeHoursPaint() {
10339
+ if (!els.finTradeHoursOn) return;
10340
+ const th = (finCfgCache && finCfgCache.tradeHours) || {};
10341
+ const on = th.on === true;
10342
+ els.finTradeHoursOn.checked = on;
10343
+ const row = els.finTradeHoursOn.closest('.fin-set-row');
10344
+ if (row) row.classList.toggle('on', on);
10345
+ if (els.finTradeHoursWrap) els.finTradeHoursWrap.hidden = !on;
10346
+ const ae = document.activeElement;
10347
+ if (els.finTradeHoursStart && ae !== els.finTradeHoursStart) els.finTradeHoursStart.value = th.start || '09:00';
10348
+ if (els.finTradeHoursEnd && ae !== els.finTradeHoursEnd) els.finTradeHoursEnd.value = th.end || '22:00';
10349
+ }
10350
+ function finTradeHoursSave() {
10351
+ const on = !!(els.finTradeHoursOn && els.finTradeHoursOn.checked);
10352
+ const start = String((els.finTradeHoursStart && els.finTradeHoursStart.value) || '09:00').slice(0, 5) || '09:00';
10353
+ const end = String((els.finTradeHoursEnd && els.finTradeHoursEnd.value) || '22:00').slice(0, 5) || '22:00';
10354
+ if (finCfgCache) finCfgCache.tradeHours = { on, start, end };
10355
+ finSaveCfg({ tradeHours: { on, start, end } });
10356
+ return { on, start, end };
10357
+ }
10358
+
10327
10359
  function finViewToggle(key, on) {
10328
10360
  finView[key] = !!on;
10329
10361
  finViewApply();
@@ -10373,6 +10405,24 @@ if (els.finDmTgUnbind) {
10373
10405
  toast('Telegram grubu koparıldı — yeni grupta /start yazınca bağlanır');
10374
10406
  });
10375
10407
  }
10408
+ /* TRADE SAATLERİ kontrolleri — anında kaydedilir (yerel saat) */
10409
+ if (els.finTradeHoursOn) {
10410
+ els.finTradeHoursOn.addEventListener('change', () => {
10411
+ const { on, start, end } = finTradeHoursSave();
10412
+ finTradeHoursPaint();
10413
+ toast(on
10414
+ ? 'Trade saatleri AÇIK — Beast Finance ve ajanları yalnız ' + start + '–' + end + ' arasında çalışır (yerel saat)'
10415
+ : 'Trade saatleri kapalı — ajanlar 7/24 serbest');
10416
+ });
10417
+ }
10418
+ for (const el of [els.finTradeHoursStart, els.finTradeHoursEnd]) {
10419
+ if (!el) continue;
10420
+ el.addEventListener('change', () => {
10421
+ const { on, start, end } = finTradeHoursSave();
10422
+ if (on) toast('Trade saatleri: ' + start + '–' + end + ' (yerel saat)');
10423
+ });
10424
+ }
10425
+ finTradeHoursPaint();
10376
10426
  finViewApply();
10377
10427
 
10378
10428
  /* ---------- BÖLÜM SIRALAMA (sürükle-bırak) ----------
@@ -5882,6 +5882,8 @@ body.finance-mode #composerWrap { margin-right: var(--finW, 400px); }
5882
5882
  }
5883
5883
  .fin-set-sub[hidden] { display: none; }
5884
5884
  .fin-set-lbl { font-size: 10.5px; color: var(--muted); letter-spacing: 0.5px; }
5885
+ .fin-th-row { display: flex; align-items: center; gap: 8px; }
5886
+ .fin-th-row input[type="time"] { flex: 1; min-width: 0; }
5885
5887
  .fin-dm-tg-state { font-size: 11.5px; color: var(--text); line-height: 1.4; word-break: break-word; }
5886
5888
  .fin-set-btn {
5887
5889
  align-self: flex-start;