beast-agent 0.20.0 → 0.22.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.
@@ -83,6 +83,16 @@ const PERM_TOOL_SETS = {
83
83
  };
84
84
  const PERM_LEVELS = ['all', 'web', 'read', 'chat'];
85
85
 
86
+ /* İzin değerini normalize eder: 'all' → ['all'], 'web' → ['web'],
87
+ 'web,read' / ['web','read'] → ['web','read'] (sıra PERM_LEVELS'e göre dizilir).
88
+ Geçersiz değerler atılır; hiçbiri kalmazsa boş dizi döner. */
89
+ function normalizePerms(p) {
90
+ const arr = Array.isArray(p) ? p.map(String) : String(p == null ? '' : p).split(',');
91
+ const picked = arr.map((s) => s.trim()).filter((s) => PERM_LEVELS.includes(s));
92
+ if (picked.includes('all')) return ['all'];
93
+ return PERM_LEVELS.filter((k) => picked.includes(k));
94
+ }
95
+
86
96
  /* CEO modu: ana (konuşma) oturumunun KULLANAMAYACAĞI uygulayıcı araçlar.
87
97
  Bunların hepsi run_background ile paralel ajana devredilir — CEO sadece
88
98
  konuşur, planlar, emir verir ve takip eder. */
@@ -190,7 +200,7 @@ class Engine {
190
200
  this.sel = this._resolve(opts.modelOverride) || this.cfg.defaultSelection || null;
191
201
  this.roleModels = opts.roleModels || {}; // { vision?, terminal?, coding?, subagent? } // providerId::model string
192
202
  this.lockdown = !!opts.lockdown; // varsayılan kısıt (oturum bazlı override edilmezse)
193
- this.sessionPerm = new Map(); // sessionId -> 'web'|'read'|'chat' (kişi bazlı izin)
203
+ this.sessionPerm = new Map(); // sessionId -> ['web'] | ['web','read'] | ['chat'] (kişi/bot bazlı izin)
194
204
  this.sessionTools = new Map(); // sessionId -> Set(araç adları) — bot skill kısıtı
195
205
  this.resolveBot = opts.resolveBot || null; // botId -> bot bilgisi (main enjekte eder)
196
206
  /* bot oturumu hafıza köprüsü: botun kendi SOUL/USER/MEMORY dosyaları */
@@ -285,12 +295,13 @@ class Engine {
285
295
  this.lockdown = !!v;
286
296
  }
287
297
 
288
- /* Kişi bazlı granül izin — WhatsApp oturumları için. 'all' kaydı siler. */
298
+ /* Kişi/bot bazlı granül izin — WhatsApp oturumları için.
299
+ Tek seviye ('web') ya da çoklu (['web','read']) verilebilir; 'all' kaydı siler. */
289
300
  setSessionPerm(sessionId, perm) {
290
301
  const id = String(sessionId || '');
291
302
  if (!id) return;
292
- const p = PERM_LEVELS.includes(perm) ? perm : null;
293
- if (p && p !== 'all') this.sessionPerm.set(id, p);
303
+ const arr = normalizePerms(perm);
304
+ if (arr.length && !arr.includes('all')) this.sessionPerm.set(id, arr);
294
305
  else this.sessionPerm.delete(id);
295
306
  }
296
307
 
@@ -347,8 +358,8 @@ class Engine {
347
358
 
348
359
  sessionPermFor(sessionId) {
349
360
  const p = this.sessionPerm.get(String(sessionId || ''));
350
- if (p) return p;
351
- return this.lockdown ? 'chat' : 'all';
361
+ if (p && p.length) return p;
362
+ return this.lockdown ? ['chat'] : ['all'];
352
363
  }
353
364
 
354
365
  setRoleModels(map) {
@@ -413,32 +424,66 @@ class Engine {
413
424
  return per || this.limits.default || 0;
414
425
  }
415
426
 
416
- /* Payload limiti aşıyorsa sıkıştır: system korunur, sondan geriye sığanlar
417
- alınır, kopuk tool zinciri hizalanır, araya kısa sıkıştırma notu girer.
418
- withNote=false ise sessiz kırpım yapılır. */
427
+ /* Tek mesajı token bütçesine göre kes: string/array içerik kırpılır,
428
+ tool_calls korunur (araç zinciri bozulmasın) */
429
+ _truncateMsgTokens(m, tokenBudget) {
430
+ if (!m) return m;
431
+ const cap = (txt) =>
432
+ String(txt).slice(0, Math.max(200, tokenBudget * 4)) + '\n…[girdi limiti için kırpıldı]';
433
+ if (typeof m.content === 'string') return { ...m, content: cap(m.content) };
434
+ if (Array.isArray(m.content)) {
435
+ return {
436
+ ...m,
437
+ content: m.content.map((p) =>
438
+ p && p.type === 'text' ? { ...p, text: cap(p.text) } : p
439
+ ),
440
+ };
441
+ }
442
+ return m;
443
+ }
444
+
445
+ /* Payload limiti aşıyorsa SERT şekilde sıkıştır: system korunur, sondan
446
+ bağlantı pencere alınır, en yeni mesaj tek başına bile taşarsa İÇERİĞİ
447
+ kırpılarak tutulur, hâlâ taşarsa en eskiler atılır. Kullanıcının son
448
+ mesajı asla sessizce düşürülmez. */
419
449
  _compressPayload(payload, maxTokens, withNote) {
420
450
  if (!Array.isArray(payload) || payload.length < 2) return payload;
421
451
  const sys = payload[0];
422
452
  const rest = payload.slice(1);
423
- const sysCost = Math.ceil(estMsgTokens(sys) * this.tokRatio);
424
- const budget = maxTokens - sysCost - 48;
425
- if (budget <= 0) return [sys, rest[rest.length - 1]].filter(Boolean);
453
+ const cost = (m) => Math.ceil(estMsgTokens(m) * this.tokRatio);
454
+ const sysCost = cost(sys);
455
+ const over = (arr) => arr.reduce((a, m) => a + cost(m), 0) + sysCost > maxTokens;
456
+ if (!over(rest)) return payload;
457
+
458
+ const budget = Math.max(0, maxTokens - sysCost - 48);
459
+
460
+ /* 1) sondan geriye bağlantı pencere (kesintisiz — kronoloji korunur) */
426
461
  const picked = [];
427
462
  let used = 0;
428
463
  for (let i = rest.length - 1; i >= 0; i--) {
429
- const cost = Math.ceil(estMsgTokens(rest[i]) * this.tokRatio);
430
- if (used + cost > budget) {
431
- if (picked.length) break; // sığmayan eski mesajlar atılır
432
- continue; // en yeni mesaj bile sığmıyorsa onu da at (aşağıda min. 1 tutulur)
433
- }
464
+ const c = cost(rest[i]);
465
+ if (used + c > budget) break;
434
466
  picked.unshift(rest[i]);
435
- used += cost;
467
+ used += c;
468
+ }
469
+
470
+ /* 2) hiçbir mesaj sığmadıysa: EN YENİ mesajı keserek tut */
471
+ if (!picked.length && rest.length) {
472
+ picked.push(this._truncateMsgTokens(rest[rest.length - 1], budget));
436
473
  }
437
- if (!picked.length) picked.push(rest[rest.length - 1]);
438
- /* kopuk tool yanıtıyla başlama — assistant/tool_calls grubu bozulmasın */
474
+
475
+ /* 3) kopuk tool yanıtıyla başlama — assistant/tool_calls grubu bozulmasın */
439
476
  while (picked.length && picked[0].role === 'tool') picked.shift();
440
- if (!picked.length) picked.push(rest[rest.length - 1]);
441
- if (picked.length >= rest.length) return payload; // zaten sığıyordu
477
+ if (!picked.length) picked.push(this._truncateMsgTokens(rest[rest.length - 1], budget));
478
+
479
+ /* 4) hâlâ limite taşıyorsa en eskilerini at */
480
+ while (picked.length > 1) {
481
+ const t = picked.reduce((a, m) => a + cost(m), 0) + sysCost + 48;
482
+ if (t <= maxTokens) break;
483
+ picked.shift();
484
+ }
485
+
486
+ if (picked.length >= rest.length && !over(rest)) return payload; // zaten sığıyordu
442
487
  if (withNote) {
443
488
  const dropped = rest.length - picked.length;
444
489
  picked.unshift({
@@ -516,32 +561,53 @@ class Engine {
516
561
  return list;
517
562
  }
518
563
 
519
- /* Adayları sırayla dene. Hata olursa durumu kaydet, zincirdeki sonrakine geç. */
564
+ /* Adayları sırayla dene. Hata olursa durumu kaydet, zincirdeki sonrakine geç.
565
+ 413/TPM "Requested N" hatası → gerçek token sayısı öğrenilir, tahmin
566
+ kalibre edilir ve aynı model BİR KEZ daha sıkı sıkıştırmayla denenir. */
520
567
  async _streamWithFallbacks(session, payload, activeTools, signal, onDelta, sel, wasVision) {
521
568
  const cands = this._chatCandidates(sel, wasVision);
522
569
  let lastErr = null;
523
570
  for (let i = 0; i < cands.length; i++) {
524
571
  const c = cands[i];
572
+ let retried413 = false;
525
573
  try {
526
- /* provider bazlı girdi limiti: payload aşıyorsa sıkıştırılıp öyle gönderilir */
574
+ /* provider bazlı girdi limiti: payload aşıyorsa sıkıştırılıp öyle gönderilir.
575
+ Tahmin: mesajlar + TOOLS şemaları (sağlayıcı onları da sayar) + %30 pay.
576
+ Hedef %15 tamponlu — katı sağlayıcılar (TPM/413) için limit aşılmasın */
527
577
  let msgs = payload;
528
578
  const lim = this._inputLimitFor(c);
579
+ const toolsCost =
580
+ activeTools && activeTools.length
581
+ ? Math.ceil(estTokens(JSON.stringify(activeTools)) * 1.1)
582
+ : 0;
583
+ const rawNow = payload.reduce((a, m) => a + estMsgTokens(m), 0) + toolsCost;
529
584
  if (lim > 0) {
530
- const est = Math.ceil(payload.reduce((a, m) => a + estMsgTokens(m), 0) * this.tokRatio);
585
+ const est = Math.ceil(rawNow * this.tokRatio * 1.3);
531
586
  if (est > lim) {
532
587
  const before = msgs.length;
533
- msgs = this._compressPayload(payload, lim, this.limits.compress);
588
+ const target = Math.max(600, Math.round(lim * 0.85) - toolsCost);
589
+ msgs = this._compressPayload(payload, target, this.limits.compress);
534
590
  if (i === 0) {
535
591
  emitSafe(this, session.id, {
536
592
  type: 'status',
537
593
  status: `bağlam sıkıştırıldı: ~${est} token → limit ${lim} (${c.providerName}, ${before}→${msgs.length} mesaj)`,
538
594
  });
539
595
  }
596
+ /* system + araçlar tek başına limiti aşıyorsa kullanıcıya net söyle */
597
+ const floor = Math.ceil(
598
+ (estMsgTokens(msgs[0]) + toolsCost) * this.tokRatio
599
+ );
600
+ if (floor > lim) {
601
+ emitSafe(this, session.id, {
602
+ type: 'status',
603
+ status: `⚠ girdi limiti (${lim}) çok düşük: system + araç tanımları tek başına ~${floor} token — limiti en az ${Math.ceil(floor * 1.15)} yap`,
604
+ });
605
+ }
540
606
  }
541
607
  }
542
608
  const res = await chatStream(
543
609
  c,
544
- { messages: msgs, tools: activeTools, reasoningEffort: this._thinkEffort() },
610
+ { messages: msgs, tools: activeTools, reasoningEffort: this._thinkEffortFor(session) },
545
611
  {
546
612
  signal,
547
613
  onDelta,
@@ -571,6 +637,23 @@ class Engine {
571
637
  if (e && (e.name === 'AbortError' || (signal && signal.aborted))) throw e;
572
638
  lastErr = e;
573
639
  const msgStr = String((e && e.message) || '');
640
+ /* 413 TPM: sağlayıcı gerçek istek boyutunu söylüyor → kalibre et,
641
+ aynı modeli yeniden sıkıştırıp BİR KEZ daha dene */
642
+ const m413 = /Requested\s+([\d,]+)/i.exec(msgStr) || (e && e.status === 413 ? [, 0] : null);
643
+ if (m413 && !retried413) {
644
+ retried413 = true;
645
+ const real = Number(String(m413[1] || '0').replace(/,/g, ''));
646
+ if (real > 0) {
647
+ const r = clamp(real / Math.max(1, rawNow), 0.75, 3);
648
+ if (r > this.tokRatio) this.tokRatio = r; /* tahmin yukarı kilitlenir */
649
+ emitSafe(this, session.id, {
650
+ type: 'status',
651
+ status: `⇩ 413 kalibrasyonu: gerçek ${real} token — daha sıkı sıkıştırıp yeniden deneniyor`,
652
+ });
653
+ }
654
+ i--; /* aynı adayı tekrar dene */
655
+ continue;
656
+ }
574
657
  const imgIssue =
575
658
  wasVision &&
576
659
  ([400, 404, 422].includes(e.status) ||
@@ -658,6 +741,13 @@ class Engine {
658
741
  this.workspace = dir;
659
742
  }
660
743
 
744
+ /* Oturum bazlı çalışma klasörü — Beast Code gibi özel oturumlar sol paneldeki
745
+ klasörde çalışabilsin diye (sess.workspace yoksa global workspace) */
746
+ _sessionWorkspace(sessionId) {
747
+ const s = sessionId ? this.cache.get(String(sessionId)) : null;
748
+ return (s && s.workspace) || this.workspace;
749
+ }
750
+
661
751
  setThinkLevel(v) {
662
752
  this.thinkLevel = Math.min(5, Math.max(0, Math.round(Number(v) || 0)));
663
753
  return this.thinkLevel;
@@ -668,6 +758,13 @@ class Engine {
668
758
  return lv.effort || null;
669
759
  }
670
760
 
761
+ /* Oturum bazlı düşünme çabası: Beast Code hız modunda en fazla 'low' */
762
+ _thinkEffortFor(session) {
763
+ const e = this._thinkEffort();
764
+ if (!session || !session.bcCode) return e;
765
+ return e ? 'low' : null;
766
+ }
767
+
671
768
  _thinkLabel() {
672
769
  const lv = THINK_LEVELS[this.thinkLevel] || THINK_LEVELS[0];
673
770
  return lv.label;
@@ -716,6 +813,27 @@ class Engine {
716
813
  if (session.code) this._codeIndex.set(session.code, session.id);
717
814
  }
718
815
 
816
+ /* Oturum dosyasındaki son 'msg' satırını (user) güncel içerikle değiştir —
817
+ yanıtsız kalmış user mesajıyla yeni mesaj birleştirildiğinde kullanılır */
818
+ _rewriteLastMsg(id, content) {
819
+ try {
820
+ const file = this._file(String(id));
821
+ const lines = fs.readFileSync(file, 'utf8').split('\n');
822
+ for (let i = lines.length - 1; i >= 0; i--) {
823
+ if (!lines[i].trim()) continue;
824
+ let r;
825
+ try { r = JSON.parse(lines[i]); } catch { continue; }
826
+ if (r.t === 'msg' && r.role === 'user') {
827
+ lines[i] = JSON.stringify({ t: 'msg', role: 'user', content: String(content || '') });
828
+ break;
829
+ }
830
+ }
831
+ const tmp = file + '.tmp';
832
+ fs.writeFileSync(tmp, lines.join('\n'));
833
+ fs.renameSync(tmp, file);
834
+ } catch {}
835
+ }
836
+
719
837
  _load(id) {
720
838
  if (this.cache.has(id)) return this.cache.get(id);
721
839
  const file = this._file(id);
@@ -822,7 +940,7 @@ class Engine {
822
940
  for (const v of this.listSessions()) {
823
941
  const s = this._load(v.id);
824
942
  if (s && s.notes && String(s.notes).trim()) {
825
- out.push({ id: s.id, code: s.code || '', title: v.title, updatedAt: v.updatedAt, count: v.count, notes: String(s.notes) });
943
+ out.push({ id: s.id, code: s.code || '', title: v.title, updatedAt: v.updatedAt, count: v.count, notes: String(s.notes), botId: s.botId || '' });
826
944
  }
827
945
  }
828
946
  return out;
@@ -1001,6 +1119,28 @@ class Engine {
1001
1119
 
1002
1120
  /* ---------- prompt assembly (the frugal part) ---------- */
1003
1121
 
1122
+ /* Beast Code (IDE paneli): VS Code hızında çalışsın diye SECMET prompt.
1123
+ Hafıza embedding araması, skills taraması, kişilik/kural blokları YOK —
1124
+ prompt kısa kalır → ilk token hızlı, tur maliyeti düşük. */
1125
+ buildBcSystem(session) {
1126
+ const nowD = new Date();
1127
+ const localDate = nowD.toLocaleDateString('tr-TR');
1128
+ const localTime = nowD.toLocaleTimeString('tr-TR', { hour: '2-digit', minute: '2-digit' });
1129
+ return (
1130
+ 'Sen BEAST CODE\u2019sun — IDE panelinde çalışan hızlı bir kodlama ajanı. VS Code gibi çevik ol.\n' +
1131
+ `Çalışma klasörü: ${(session && session.workspace) || this.workspace}\n` +
1132
+ `Yerel zaman: ${localDate} ${localTime}\n` +
1133
+ 'HIZ KURALLARI:\n' +
1134
+ '- İLK EYLEMİN daima todo_write olsun: işi 2-6 maddelik plana böl; her adım bitince status:"done" ile güncelle.\n' +
1135
+ '- Kodu ve dosyaları DOĞRUDAN write_file ile yaz/güncelle. Basit dosya oluşturma/düzenleme için asla Python scripti yazma; python_run yalnız gerçek hesap/veri işleme gerekiyorsa.\n' +
1136
+ '- Kurulum, test, build, git gibi işler için run_command kullan (Windows PowerShell ortamı).\n' +
1137
+ '- Bağımsız araç çağrılarını AYNI TURDA paralel ver.\n' +
1138
+ '- Dosyayı kullanıcı söylememişse list_dir ile yapıyı görüp kendin karar ver.\n' +
1139
+ '- Uzun açıklama yok: doğrudan çalış, bitince 1-3 satırlık kısa özet ver. Soru sorma, sohbet etme.\n' +
1140
+ FORMAT_RULES
1141
+ );
1142
+ }
1143
+
1004
1144
  buildSystem(queryText, session) {
1005
1145
  /* BOT OTURUMU: non-admin bota bağlıysa global Beast hafızası HIÇ girmez —
1006
1146
  o botun kendi SOUL/USER/MEMORY dosyaları kullanılır (tam izolasyon) */
@@ -1041,6 +1181,7 @@ class Engine {
1041
1181
  '# SKILLS\nKullanmadan önce ilgili SKILL.md dosyasını read_file ile oku.\n' + sk.join('\n')
1042
1182
  );
1043
1183
  }
1184
+ /* Beast Code (bcCode) oturumları buildBcSystem kullanır — buraya düşmez */
1044
1185
  const nowD = new Date();
1045
1186
  const localDate = nowD.toLocaleDateString('tr-TR');
1046
1187
  const localTime = nowD.toLocaleTimeString('tr-TR', { hour: '2-digit', minute: '2-digit' });
@@ -1049,7 +1190,7 @@ class Engine {
1049
1190
  tz = Intl.DateTimeFormat().resolvedOptions().timeZone || '';
1050
1191
  } catch {}
1051
1192
  parts.push(
1052
- `# ORTAM\nWindows + PowerShell. Çalışma klasörü: ${this.workspace}\n` +
1193
+ `# ORTAM\nWindows + PowerShell. Çalışma klasörü: ${(session && session.workspace) || this.workspace}\n` +
1053
1194
  `Yerel tarih: ${localDate} (${['Pazar','Pazartesi','Salı','Çarşamba','Perşembe','Cuma','Cumartesi'][nowD.getDay()]})\n` +
1054
1195
  `Yerel saat: ${localTime}${tz ? ` (saat dilimi: ${tz})` : ''}\n` +
1055
1196
  'Tarih/saat gerektiren her işte bu YEREL zamanı esas al; UTC varsayma, kendin dönüşüm yapma. Bu satırlar her mesajda güncellenir.\n' +
@@ -1327,12 +1468,29 @@ class Engine {
1327
1468
  }
1328
1469
 
1329
1470
  if (!String(typeof msg.content === 'string' ? msg.content : msg.content[0].text || '').trim()) return false;
1330
- /* #12 kişilik kalibrasyonu: kullanıcı cümlelerini örnek havuzuna düşür */
1331
- try { memory.addStyleSample(this._plainText(msg)); } catch {}
1332
- s.messages.push(msg);
1333
- try {
1334
- this._append(s, msg);
1335
- } catch {}
1471
+ /* #12 kişilik kalibrasyonu: kullanıcı cümlelerini örnek havuzuna düşür (Beast Code hariç — hız) */
1472
+ if (!s.bcCode) {
1473
+ try { memory.addStyleSample(this._plainText(msg)); } catch {}
1474
+ }
1475
+
1476
+ /* Önceki tur yanıtlanmadan kaldıysa (hata/durdurma sonrası artık user mesajı)
1477
+ katı sağlayıcılar art arda user mesajını reddeder: "messages illegal" (400).
1478
+ Yeni metni o mesajla BİRLEŞTİR — dosyadaki son satır da güncellenir. */
1479
+ const lastMsg = s.messages[s.messages.length - 1];
1480
+ if (
1481
+ lastMsg && lastMsg.role === 'user' &&
1482
+ typeof lastMsg.content === 'string' &&
1483
+ typeof msg.content === 'string'
1484
+ ) {
1485
+ lastMsg.content = lastMsg.content + '\n' + msg.content;
1486
+ msg = lastMsg;
1487
+ this._rewriteLastMsg(s.id, lastMsg.content);
1488
+ } else {
1489
+ s.messages.push(msg);
1490
+ try {
1491
+ this._append(s, msg);
1492
+ } catch {}
1493
+ }
1336
1494
  this.emit({ type: 'message', sessionId: s.id, message: msg });
1337
1495
  this.emit({ type: 'sessions' });
1338
1496
  this._run(s).catch(() => {});
@@ -1871,20 +2029,59 @@ class Engine {
1871
2029
  _bgTrim(session) {
1872
2030
  const max = Engine.BG_KEEP_MSGS;
1873
2031
  const msgs = session.messages;
1874
- if (!session.bgJob || msgs.length <= max) return;
1875
- const keep = msgs.slice(-max);
1876
- while (keep.length && keep[0].role === 'tool') keep.shift();
2032
+ /* bg işleri + Beast Code: uzun geçmiş payload'ı şişirip yavaşlatır */
2033
+ if ((!session.bgJob && !session.bcCode) || msgs.length <= max) return;
2034
+ let keep = msgs.slice(-max);
2035
+ /* GÜVENLİ SINIR: kesim bir assistant(tool_calls) + tool sonuç çiftini BÖLMESİN.
2036
+ Yetim 'tool' mesajı ya da sonuçları pencere dışında kalan tool_calls'lı
2037
+ asistan mesajı sağlayıcıda "messages parameter is illegal" (HTTP 400) verir. */
2038
+ while (keep.length) {
2039
+ const f = keep[0];
2040
+ if (f.role === 'tool') { keep.shift(); continue; }
2041
+ if (f.role === 'assistant' && Array.isArray(f.tool_calls) && f.tool_calls.length) {
2042
+ const need = f.tool_calls.length;
2043
+ let got = 0;
2044
+ for (let i = 1; i < keep.length && keep[i].role === 'tool' && got < need; i++) got++;
2045
+ if (got < need) { keep.shift(); continue; }
2046
+ }
2047
+ break;
2048
+ }
1877
2049
  const firstUser = msgs.find((m) => m.role === 'user');
1878
2050
  if (firstUser && !keep.includes(firstUser)) keep.unshift(firstUser);
1879
2051
  session.messages = keep;
1880
2052
  }
1881
2053
 
1882
2054
  async _chatTurn(session, signal, onDelta, toolsList = TOOLS) {
2055
+ /* Beast Code: todo_write açıklaması "3+ adım" kısıtı içerir ve model küçük
2056
+ işlerde atlar — bcCode oturumunda HER işte İLK araç olacak şekilde değiştir */
2057
+ if (session && session.bcCode) {
2058
+ toolsList = toolsList.map((t) =>
2059
+ t && t.function && t.function.name === 'todo_write'
2060
+ ? {
2061
+ ...t,
2062
+ function: {
2063
+ ...t.function,
2064
+ description:
2065
+ 'Replace the visible task checklist for this chat. Beast Code session: ALWAYS call this tool as your VERY FIRST action for EVERY job (even tiny 1-step jobs) BEFORE any text or other tool, then update statuses (status:"done") as you complete each step. Keep titles short, verb-first.',
2066
+ },
2067
+ }
2068
+ : t
2069
+ );
2070
+ }
1883
2071
  const promptText = this._lastUserText(session);
1884
- let system = session.bgJob ? this.buildBgSystem(session) : this.buildSystem(promptText, session);
1885
- // Granül izin: oturumun yetki seviyesine göre araç seti daraltılır
1886
- const perm = this.sessionPermFor(session.id);
1887
- const allowedSet = PERM_TOOL_SETS[perm] || null;
2072
+ let system = session.bgJob
2073
+ ? this.buildBgSystem(session)
2074
+ : session.bcCode
2075
+ ? this.buildBcSystem(session)
2076
+ : this.buildSystem(promptText, session);
2077
+ // Granül izin: oturumun yetki seviyesine göre araç seti daraltılır.
2078
+ // Çoklu izin (ör. web+read) seçiliyse kümeler BİRLEŞİR — hepsinin araçları açık olur.
2079
+ const perms = this.sessionPermFor(session.id);
2080
+ let allowedSet = null;
2081
+ if (!perms.includes('all')) {
2082
+ allowedSet = new Set();
2083
+ for (const p of perms) for (const t of PERM_TOOL_SETS[p] || []) allowedSet.add(t);
2084
+ }
1888
2085
  let activeTools = allowedSet ? toolsList.filter((t) => allowedSet.has(t.function.name)) : toolsList;
1889
2086
  /* bot skill kısıtı: bota verilen yetkiye göre araç seti daraltılır */
1890
2087
  const toolLimit = this.sessionTools.get(String(session.id));
@@ -1896,17 +2093,16 @@ class Engine {
1896
2093
  /* CEO: uygulayıcı araçlar kapalı — her şey paralel ajana devredilir */
1897
2094
  activeTools = activeTools.filter((t) => !CEO_EXEC_TOOLS.has(t.function.name));
1898
2095
  }
1899
- if (perm === 'chat') {
2096
+ if (perms.length === 1 && perms[0] === 'chat') {
1900
2097
  system +=
1901
2098
  '\n\n# KISITLI MOD\nTüm araçların (komut, dosya, web, tarayıcı, hafıza) kapalı. Sadece yazarak cevap ver. ' +
1902
2099
  'Bilgisayarla ilgili bir işlem istenirse bu modda yapamayacağını kibarca söyle.';
1903
- } else if (perm === 'web') {
1904
- system +=
1905
- '\n\n# SINIRLI YETKİ (web)\nSadece web ve tarayıcı araçlarına erişimin var; dosya okuma/yazma ve komut çalıştırma YOK. ' +
1906
- 'Böyle bir istek gelirse yetkin olmadığını söyle.';
1907
- } else if (perm === 'read') {
2100
+ } else if (!perms.includes('all')) {
2101
+ const bits = [];
2102
+ if (perms.includes('web')) bits.push('web ve tarayıcı araçlarına erişimin var');
2103
+ if (perms.includes('read')) bits.push('bilgisayarı SADECE OKUYABİLİRSİN (klasör listeleme, dosya okuma) + web/tarayıcı');
1908
2104
  system +=
1909
- '\n\n# SINIRLI YETKİ (salt-okunur)\nBilgisayarı SADECE OKUYABİLİRSİN (klasör listeleme, dosya okuma) + web/tarayıcı. ' +
2105
+ '\n\n# SINIRLI YETKİ\n' + bits.join('; ') + '. ' +
1910
2106
  'Dosya yazma, silme ve komut çalıştırma YOK; istenirse yapamayacağını söyle.';
1911
2107
  }
1912
2108
  /* bot kimliği: bota bağlı oturumlarda kişilik + izolasyon kuralları */
@@ -1943,8 +2139,8 @@ class Engine {
1943
2139
  if (actual > 0) {
1944
2140
  const predicted = this._payloadTokens(payload);
1945
2141
  if (predicted > 50) {
1946
- const r = clamp(actual / predicted, 0.4, 3);
1947
- this.tokRatio = clamp(this.tokRatio * 0.7 + r * 0.3, 0.4, 3);
2142
+ const r = clamp(actual / predicted, 0.75, 3);
2143
+ this.tokRatio = clamp(this.tokRatio * 0.7 + r * 0.3, 0.75, 3);
1948
2144
  }
1949
2145
  }
1950
2146
  return res;
@@ -1975,8 +2171,10 @@ class Engine {
1975
2171
 
1976
2172
  if (!res.toolCalls || !res.toolCalls.length) {
1977
2173
  // cevap tamam — pencereden düşecek kısmın oturum notlarını güncelle
1978
- /* #21 bg ajanı: görev sonu ekstra LLM çağrıları YOK — hızlı kapansın */
1979
- if (!session.bgJob) {
2174
+ /* #21 bg ajanı: görev sonu ekstra LLM çağrıları YOK — hızlı kapansın.
2175
+ Beast Code (bcCode) oturumları da aynı şekilde ANINDA kapansın:
2176
+ not/hafıza/skill yansıtma çağrıları yapılmaz → done hemen gelir */
2177
+ if (!session.bgJob && !session.bcCode) {
1980
2178
  const r = await this._updateSessionNotes(session, ctrl.signal);
1981
2179
  if (r && r.ok) {
1982
2180
  emitSafe(this, sid, { type: 'status', status: `oturum notları güncellendi (${session.code || ''})` });
@@ -1985,13 +2183,13 @@ class Engine {
1985
2183
  }
1986
2184
  }
1987
2185
  /* otomatik memory döngüsü: kalıcı değerli bilgiyi MEMORY.md'ye düşür */
1988
- if (!ctrl.signal.aborted && !session.bgJob) {
2186
+ if (!ctrl.signal.aborted && !session.bgJob && !session.bcCode) {
1989
2187
  try {
1990
2188
  await this._autoMemory(session, ctrl.signal);
1991
2189
  } catch {}
1992
2190
  }
1993
2191
  /* deneyimden skill doğurma (#2): 5+ yeni araç çağrısı biriktiyse yansıt */
1994
- if (this.reflection.enabled && !ctrl.signal.aborted && !session.bgJob) {
2192
+ if (this.reflection.enabled && !ctrl.signal.aborted && !session.bgJob && !session.bcCode) {
1995
2193
  try {
1996
2194
  const made = await this._maybeReflectSkill(session);
1997
2195
  if (made) emitSafe(this, sid, { type: 'status', status: `skill taslağı doğdu: ${made}` });
@@ -2625,7 +2823,7 @@ class Engine {
2625
2823
  }
2626
2824
  if (!r || !r.ok || !(r.results || []).length) {
2627
2825
  try {
2628
- r = JSON.parse(await tools.exec('web_search', args, { cwd: this.workspace, signal }));
2826
+ r = JSON.parse(await tools.exec('web_search', args, { cwd: this._sessionWorkspace(sessionId), signal }));
2629
2827
  } catch {}
2630
2828
  }
2631
2829
  return JSON.stringify(r || { ok: false, error: 'web arama başarısız — tüm motorlar boş döndü' });
@@ -2672,7 +2870,7 @@ class Engine {
2672
2870
  name === 'web_search' || name === 'http_fetch' || name === 'python_run')) {
2673
2871
  return JSON.stringify({ ok: false, error: `unknown tool ${name}` });
2674
2872
  }
2675
- return await tools.exec(name, args, { cwd: this.workspace, signal });
2873
+ return await tools.exec(name, args, { cwd: this._sessionWorkspace(sessionId), signal });
2676
2874
  } catch (e) {
2677
2875
  return JSON.stringify({ ok: false, error: String((e && e.message) || e) });
2678
2876
  }
@@ -3301,3 +3499,6 @@ module.exports.sanitizeTodoItems = sanitizeTodoItems;
3301
3499
  module.exports.parseReflectionJson = parseReflectionJson;
3302
3500
  module.exports.CEO_EXEC_TOOLS = CEO_EXEC_TOOLS;
3303
3501
  module.exports.BG_HIDDEN_TOOLS = BG_HIDDEN_TOOLS;
3502
+ module.exports.PERM_TOOL_SETS = PERM_TOOL_SETS;
3503
+ module.exports.PERM_LEVELS = PERM_LEVELS;
3504
+ module.exports.normalizePerms = normalizePerms;