beast-agent 0.29.0 → 1.0.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.
Files changed (47) hide show
  1. package/package.json +6 -3
  2. package/src/agent/engine.js +554 -33
  3. package/src/agent/llm.js +52 -3
  4. package/src/agent/obscura.js +26 -10
  5. package/src/agent/skills.js +87 -29
  6. package/src/agent/tools.js +519 -7
  7. package/src/main.js +48 -7
  8. package/src/preload.js +2 -0
  9. package/src/renderer/i18n.js +11 -7
  10. package/src/renderer/index.html +5 -2
  11. package/src/renderer/renderer.js +246 -8
  12. package/src/renderer/style.css +60 -4
  13. package/.env.example +0 -8
  14. package/config.example.yaml +0 -39
  15. package/eng.traineddata +0 -0
  16. package/scripts/fix-electron.js +0 -126
  17. package/scripts/release.js +0 -121
  18. package/scripts/swap-electron.js +0 -40
  19. package/store/skills.json +0 -5
  20. package/tests/approval.test.js +0 -56
  21. package/tests/bg-jobs.test.js +0 -379
  22. package/tests/bots-name.test.js +0 -42
  23. package/tests/bus.test.js +0 -46
  24. package/tests/computeruse-context.test.js +0 -69
  25. package/tests/cron.test.js +0 -95
  26. package/tests/engine.test.js +0 -203
  27. package/tests/eval.test.js +0 -91
  28. package/tests/fallout.test.js +0 -188
  29. package/tests/llm.test.js +0 -205
  30. package/tests/memory.test.js +0 -33
  31. package/tests/memoryloop.test.js +0 -26
  32. package/tests/notegen.test.js +0 -63
  33. package/tests/obscura.test.js +0 -124
  34. package/tests/owner.test.js +0 -32
  35. package/tests/python.test.js +0 -76
  36. package/tests/research.test.js +0 -116
  37. package/tests/scenarios.json +0 -81
  38. package/tests/sessioncode.test.js +0 -50
  39. package/tests/setup.js +0 -9
  40. package/tests/tokens.test.js +0 -42
  41. package/tests/tools.test.js +0 -84
  42. package/tests/usage.test.js +0 -44
  43. package/tests/v11.test.js +0 -76
  44. package/tests/watchers.test.js +0 -206
  45. package/tests/watext.test.js +0 -53
  46. package/tests/whatsapp.test.js +0 -103
  47. package/tests/wherewasi.test.js +0 -40
@@ -13,6 +13,7 @@ const research = require('./research');
13
13
  const memory = require('./memory');
14
14
  const skills = require('./skills');
15
15
  const { estTokens, estMsgTokens } = require('./tokens');
16
+ const log = require('./logger');
16
17
 
17
18
  const MAX_TURNS = 25;
18
19
  const SUB_MAX_TURNS = 8;
@@ -62,6 +63,38 @@ const THINK_LEVELS = [
62
63
  { v: 5, label: 'Max', effort: 'max' },
63
64
  ];
64
65
 
66
+ /* ---------- opencode motoru port: bağlam bütçesi + compaction + prune ----------
67
+ Kaynak: opencode-dev/packages/opencode/src/session/{overflow,compaction}.ts
68
+ Sabitler birebir alınmıştır (COMPACTION_BUFFER 20k, PRUNE_PROTECT 40k,
69
+ PRUNE_MINIMUM 20k, DOOM_LOOP 3, özet çıktı kırpma 2000 karakter). */
70
+ const COMPACTION_BUFFER = 20000; // overflow.ts:8 — özet turu için ayrılan pay
71
+ const OUTPUT_TOKEN_MAX = 32000; // transform.ts:18 — çıktı tavanı (rezerv hesabı)
72
+ const PRUNE_PROTECT = 40000; // compaction.ts:29 — en yeni araç çıktısı koruması
73
+ const PRUNE_MINIMUM = 20000; // compaction.ts:28 — bu kadar kazanç yoksa dokunma
74
+ const SUMMARY_OUT_KEEP = 2000; // compaction.ts:30 — özetteki araç çıktısı kırpma
75
+ const MIN_PRESERVE_RECENT = 2000; // compaction.ts:32 — korunan kuyruk alt sınırı
76
+ const MAX_PRESERVE_RECENT = 15000; // compaction.ts:33 — korunan kuyruk üst sınırı
77
+ const DOOM_LOOP_THRESHOLD = 3; // processor.ts:29 — aynı araç+argüman sınırı
78
+ const HISTORY_BUDGET_MAX = 80000; // dinamik bütçe tavanı (cache'siz sağlayıcı koruması)
79
+
80
+ /* Bilinen model ailelerinin bağlam penceresi (models.dev değerleri).
81
+ Eşleşmezse 128k varsayılır — asla gerçek limitin üstünü tahmin etme. */
82
+ const MODEL_CONTEXT_TABLE = [
83
+ [/gemini-[23]/i, 1000000],
84
+ [/gpt-5|gpt-4\.1|gpt-4o|o[134]-/i, 400000],
85
+ [/glm-4\.[67]/i, 200000],
86
+ [/claude/i, 200000],
87
+ [/grok-[34]/i, 256000],
88
+ [/kimi/i, 256000],
89
+ [/qwen.*coder/i, 256000],
90
+ [/deepseek|qwen|glm|llama|mistral|minimax/i, 131072],
91
+ ];
92
+ function modelContextOf(sel) {
93
+ const s = String((sel && sel.model) || '');
94
+ for (const [re, ctx] of MODEL_CONTEXT_TABLE) if (re.test(s)) return ctx;
95
+ return 131072;
96
+ }
97
+
65
98
  /* Çıktı biçimi kuralı: # ve * karakteri yasak (tüm ajanlar için ortak metin) */
66
99
  const FORMAT_RULES =
67
100
  'ÇIKTI BİÇİMİ (ZORUNLU):\n' +
@@ -82,12 +115,21 @@ const PERM_TOOL_SETS = {
82
115
  read: new Set([
83
116
  'web_search', 'http_fetch', 'deep_search',
84
117
  'browser_open', 'browser_read', 'browser_snapshot',
85
- 'list_dir', 'read_file',
118
+ 'list_dir', 'read_file', 'grep', 'glob',
86
119
  ]),
87
120
  chat: new Set([]), // sadece sohbet
88
121
  };
89
122
  const PERM_LEVELS = ['all', 'web', 'read', 'chat'];
90
123
 
124
+ /* opencode plan agent portu: PLAN modu PROMPT düzeyinde değil GERÇEKten
125
+ salt-okurdur — yazma/çalıştırma araçları setten düşer, sadece plan çıkar */
126
+ const PLAN_ALLOW_TOOLS = new Set([
127
+ 'read_file', 'list_dir', 'grep', 'glob',
128
+ 'web_search', 'http_fetch', 'deep_search',
129
+ 'browser_open', 'browser_read', 'browser_snapshot',
130
+ 'todo_write', 'memory_search', 'kb_search', 'ocr_read',
131
+ ]);
132
+
91
133
  /* İzin değerini normalize eder: 'all' → ['all'], 'web' → ['web'],
92
134
  'web,read' / ['web','read'] → ['web','read'] (sıra PERM_LEVELS'e göre dizilir).
93
135
  Geçersiz değerler atılır; hiçbiri kalmazsa boş dizi döner. */
@@ -102,7 +144,7 @@ function normalizePerms(p) {
102
144
  Bunların hepsi run_background ile paralel ajana devredilir — CEO sadece
103
145
  konuşur, planlar, emir verir ve takip eder. */
104
146
  const CEO_EXEC_TOOLS = new Set([
105
- 'run_command', 'read_file', 'write_file', 'list_dir',
147
+ 'run_command', 'read_file', 'write_file', 'edit_file', 'list_dir',
106
148
  'python_run',
107
149
  'web_search', 'http_fetch', 'deep_search',
108
150
  'browser_open', 'browser_read', 'browser_screenshot', 'browser_snapshot',
@@ -258,6 +300,8 @@ class Engine {
258
300
  this._bgLimit = Math.max(1, Number(opts.bgLimit || process.env.BEAST_BG_LIMIT) || BG_MAX_CONCURRENT_DEFAULT);
259
301
  this._bgPendingStart = new Map(); // sid -> ilk görev metni (slot açılınca gönderilir)
260
302
  this._bgGroups = new Map(); // groupId -> {parentId,total,results,dead}
303
+ /* silinen oturum işaretleri: rapor/zincir buraya gönderilmez (hayalet oturum yok) */
304
+ this._deletedSessions = new Set();
261
305
  /* #17 kalıcı ajan geçmişi: restart sonrası işler + sohbetler kaybolmasın */
262
306
  this._bgJobsFile = path.join(this.sessionsDir, 'bg-jobs.json');
263
307
  this._loadBgJobsPersisted();
@@ -626,7 +670,14 @@ class Engine {
626
670
  }
627
671
  const res = await chatStreamAuto(
628
672
  c,
629
- { messages: msgs, tools: activeTools, reasoningEffort: this._thinkEffortFor(session) },
673
+ {
674
+ messages: msgs,
675
+ tools: activeTools,
676
+ reasoningEffort: this._thinkEffortFor(session),
677
+ /* opencode cache disiplini (transform.ts:1270): oturum-sabit
678
+ önbellek anahtarı — destekleyen sağlayıcıda önek cache'i tutar */
679
+ cacheKey: String(session.id || ''),
680
+ },
630
681
  {
631
682
  signal,
632
683
  onDelta,
@@ -926,6 +977,10 @@ class Engine {
926
977
  } else if (rec.t === 'notes') {
927
978
  session.notes = String(rec.text || '');
928
979
  session.notesAt = Number(rec.at) || 0;
980
+ } else if (rec.t === 'summary') {
981
+ /* opencode compaction: konuşma özeti — birebir geçmişin yerine geçmez,
982
+ payload'da system'den sonra sabit user mesajı olarak girer */
983
+ if (rec.text) session.summary = String(rec.text);
929
984
  } else if (rec.t === 'botdm') {
930
985
  /* botlar arası DM oturumu — admin izleyebilir, sidebar'da görünmez */
931
986
  session.isBotDm = true;
@@ -1126,6 +1181,7 @@ class Engine {
1126
1181
  s.messages = [];
1127
1182
  delete s.notes;
1128
1183
  delete s.notesAt;
1184
+ delete s.summary; // özet de sıfırlanır — taze başlangıç
1129
1185
  this.todos.set(sid, []);
1130
1186
  this.emit({ type: 'sessions' });
1131
1187
  return true;
@@ -1173,13 +1229,14 @@ class Engine {
1173
1229
  }
1174
1230
 
1175
1231
  deleteSession(id) {
1232
+ const sid = String(id || '');
1176
1233
  try {
1177
- const c = this.ctrls.get(id);
1234
+ const c = this.ctrls.get(sid);
1178
1235
  if (c) {
1179
1236
  this._abortReasons = this._abortReasons || new Map();
1180
- this._abortReasons.set(String(id), 'oturum silindiği için tur iptal edildi');
1237
+ this._abortReasons.set(sid, 'oturum silindiği için tur iptal edildi');
1181
1238
  c.abort();
1182
- this.ctrls.delete(id);
1239
+ this.ctrls.delete(sid);
1183
1240
  }
1184
1241
  /* #17 silinecek ajanın bekleyen öz-kurtarma zamanlayıcısını iptal et —
1185
1242
  yoksa send() oturum dosyasını yeniden yaratıp işi diriltir */
@@ -1206,30 +1263,82 @@ class Engine {
1206
1263
  this._saveBgJobs();
1207
1264
  this._bgEmit();
1208
1265
  }
1266
+ /* silinme işareti: bekleyen raporlar/rapor zinciri bu oturuma ASLA düşmez */
1267
+ if (this._deletedSessions) {
1268
+ this._deletedSessions.add(sid);
1269
+ if (this._deletedSessions.size > 300) {
1270
+ const first = this._deletedSessions.values().next().value;
1271
+ if (first !== undefined) this._deletedSessions.delete(first);
1272
+ }
1273
+ }
1209
1274
  this.emit({ type: 'sessions' });
1210
1275
  return true;
1211
1276
  }
1212
1277
 
1213
1278
  /* ---------- prompt assembly (the frugal part) ---------- */
1214
1279
 
1280
+ /* opencode instruction.ts port: workspace'teki proje talimatları (AGENTS.md,
1281
+ CLAUDE.md, CONTEXT.md) Beast Code system promptuna girer. Oturum başına
1282
+ BİR KEZ okunup session'da saklanır — system promptu epoch boyunca sabit
1283
+ kalır, önek cache bozulmaz (dosya sonradan değişse bile). */
1284
+ _projectInstructions(session) {
1285
+ if (!session) return '';
1286
+ if (session._projCtx !== undefined) return session._projCtx;
1287
+ let out = '';
1288
+ try {
1289
+ const ws = (session && session.workspace) || this.workspace;
1290
+ for (const name of ['AGENTS.md', 'CLAUDE.md', 'CONTEXT.md']) {
1291
+ let txt = '';
1292
+ try { txt = fs.readFileSync(path.join(ws, name), 'utf8'); } catch {}
1293
+ txt = String(txt || '').trim();
1294
+ if (!txt) continue;
1295
+ out += (out ? '\n\n' : '') + `--- ${name} ---\n` + txt.slice(0, 6000);
1296
+ if (out.length >= 8000) break;
1297
+ }
1298
+ } catch {}
1299
+ out = out.slice(0, 9000);
1300
+ session._projCtx = out;
1301
+ return out;
1302
+ }
1303
+
1215
1304
  /* Beast Code (IDE paneli): VS Code hızında çalışsın diye SECMET prompt.
1305
+ OpenCode kodlama disiplini gömülü: bağlam → plan → küçük diff → doğrula.
1216
1306
  Hafıza embedding araması, skills taraması, kişilik/kural blokları YOK —
1217
1307
  prompt kısa kalır → ilk token hızlı, tur maliyeti düşük. */
1218
1308
  buildBcSystem(session) {
1219
1309
  const nowD = new Date();
1220
1310
  const localDate = nowD.toLocaleDateString('tr-TR');
1221
- const localTime = nowD.toLocaleTimeString('tr-TR', { hour: '2-digit', minute: '2-digit' });
1311
+ /* önek-cache disiplini (opencode Context Epoch): dakika yerine saat
1312
+ system prompt her dakika değişse sağlayıcı önbelleği sürekli bozulur */
1313
+ const localTime = String(nowD.getHours()).padStart(2, '0') + ':00';
1314
+ const mode = String((session && session.bcMode) || 'auto').toLowerCase();
1315
+ const modeBlock =
1316
+ mode === 'plan'
1317
+ ? 'ÇALIŞMA MODU: PLAN 🔍 — dosyaları oku/incele (read_file, list_dir), KOD YAZMA; mevcut yapıyı özetle ve adım adım UYGULAMA PLANI ver (hangi dosya, ne değişecek, nasıl doğrulanır). Kullanıcı /build deyince plan uygulanır.'
1318
+ : mode === 'build'
1319
+ ? 'ÇALIŞMA MODU: BUILD 🛠 — son planı SOHBETTEKİ bağlamdan al ve UYGULA: dosyaları düzenle, komutları çalıştır, doğrula. Yeni plan açma; en fazla 1 cümlelik yön gösterimi + uygulama.'
1320
+ : 'ÇALIŞMA MODU: OTOMATİK ⚡ — önce 2-4 satırlık mini plan (todo_write), sonra hemen uygula + doğrula.';
1321
+ const proj = this._projectInstructions(session);
1222
1322
  return (
1223
- 'Sen BEAST CODE\u2019sun — IDE panelinde çalışan hızlı bir kodlama ajanı. VS Code gibi çevik ol.\n' +
1323
+ 'Sen BEAST CODE\u2019sun — IDE panelinde çalışan, OpenCode disiplinli hızlı bir kodlama ajanı. VS Code gibi çevik ol.\n' +
1224
1324
  `Çalışma klasörü: ${(session && session.workspace) || this.workspace}\n` +
1225
1325
  `Yerel zaman: ${localDate} ${localTime}\n` +
1326
+ `${modeBlock}\n` +
1327
+ (proj
1328
+ ? '# PROJE TALİMATLARI (workspace AGENTS/CLAUDE/CONTEXT dosyalarından — daima uy)\n' + proj + '\n'
1329
+ : '') +
1330
+ 'WORKFLOW (her işte bu sıra):\n' +
1331
+ '1) BAĞLAM: değiştirmeden önce ilgili dosyaları OKU — read_file satır numaralı döner (N: içerik), offset/limit ile büyük dosyayı pencerele; içerik araması için grep (regex), dosya adı araması için glob kullan; varsayım yapma, mevcut stili/konvansiyonu takip et.\n' +
1332
+ '2) PLAN: 2+ adımlı işlerde İLK EYLEM todo_write olsun (2-6 madde); her adım bitince status:"done" ile GÜNCELLE — liste bitene kadar iş bitmiş sayılmaz.\n' +
1333
+ '3) EDİT: VAR OLAN dosyada önce edit_file kullan (old_string/new_string ile yalnız ilgili bölümü değiştir; birden çok eşleşme varsa bağlam ekle ya da replace_all); write_file yalnız YENİ dosya ya da tam yeniden yazım için. İlgisiz yeniden biçimleme/kayıt boşluk değişikliği YAPMA.\n' +
1334
+ '4) DOĞRULA: edit sonrası mümkünse derle/test et/lint çalıştır (run_command); hata varsa DÜZELT ve TEKRAR dene (en fazla 2 doğrulama turu) — kırmızı bırakma.\n' +
1335
+ '5) RAPOR: 1-3 satır — ne değişti + doğrulama sonucu (ör. "npm test ✓ 154/154"). Uzun açıklama yok.\n' +
1226
1336
  'HIZ KURALLARI:\n' +
1227
- '- İ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' +
1228
- '- 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' +
1229
- '- Kurulum, test, build, git gibi işler için run_command kullan (Windows PowerShell ortamı).\n' +
1230
- '- Bağımsız araç çağrılarını AYNI TURDA paralel ver.\n' +
1337
+ '- Kodu ve dosyaları DOĞRUDAN write_file ile yaz; basit dosya oluşturma/düzenleme için Python scripti yazma; python_run yalnız gerçek hesap/veri işleme gerekiyorsa.\n' +
1338
+ '- Bağımsız araç çağrılarını AYNI TURDA paralel ver (birden çok read_file tek turda).\n' +
1231
1339
  '- Dosyayı kullanıcı söylememişse list_dir ile yapıyı görüp kendin karar ver.\n' +
1232
- '- Uzun açıklama yok: doğrudan çalış, bitince 1-3 satırlık kısa özet ver. Soru sorma, sohbet etme.\n' +
1340
+ '- Run_command PowerShell ortamında çalışır (Windows): KALICI oturum cd ve $env değişkenleri çağrılar arasında korunur; büyük çıktıda tam çıktı geçici dosyaya düşer, read_file ile oku. shell:"bash" verirsen git-bash ile bash sözdizimi koşar.\n' +
1341
+ '- Soru sorma, sohbet etme; uygulayıp özetle. Kullanıcı /plan /build /auto ile modu değiştirir.\n' +
1233
1342
  FORMAT_RULES
1234
1343
  );
1235
1344
  }
@@ -1277,7 +1386,9 @@ class Engine {
1277
1386
  /* Beast Code (bcCode) oturumları buildBcSystem kullanır — buraya düşmez */
1278
1387
  const nowD = new Date();
1279
1388
  const localDate = nowD.toLocaleDateString('tr-TR');
1280
- const localTime = nowD.toLocaleTimeString('tr-TR', { hour: '2-digit', minute: '2-digit' });
1389
+ /* önek-cache disiplini (opencode Context Epoch): dakika yerine saat
1390
+ system prompt her dakika değişse sağlayıcı önbelleği sürekli bozulur */
1391
+ const localTime = String(nowD.getHours()).padStart(2, '0') + ':00';
1281
1392
  let tz = '';
1282
1393
  try {
1283
1394
  tz = Intl.DateTimeFormat().resolvedOptions().timeZone || '';
@@ -1303,6 +1414,7 @@ class Engine {
1303
1414
  '# ARAÇ KURALLARI\n' +
1304
1415
  [
1305
1416
  'Basit soruları araç kullanmadan doğrudan cevapla — hız önceliklidir.',
1417
+ 'Kod/dosya işlerinde: içerik araması grep (regex), dosya adı araması glob, VAR OLAN dosyayı değiştirme edit_file (write_file yalnız yeni dosya/tam yeniden yazım). read_file satır numaralı döner; offset/limit ile pencerele.',
1306
1418
  'Kullanıcı bir tarihte/saatte hatırlatılmasını isterse set_reminder kullan; when değerini ORTAMdaki bugüne göre hesapla (yerel saat). "Her sabah/gün/hafta" gibi tekrarlı isteklerde repeat alanını da ver (daily/weekly/monthly/weekdays veya cron).',
1307
1419
  'Kullanıcı kalıcı bir arka plan takibi isterse (fiyat eşiği, pil seviyesi, sayfa değişikliği) watcher_add ile izleyici kur; kurduktan sonra watcher_list ile doğrula ve kullanıcıya koşulu + kontrol sıklığını kısaca bildir.',
1308
1420
  'Anlık olay takipleri için (yeni mail, fiyat eşiği, dosya değişimi, webhook) event_subscribe kullan — cron/polling gerekmez; listeyi event_list ile göster, vazgeçirirse event_unsubscribe.',
@@ -1367,7 +1479,34 @@ class Engine {
1367
1479
  return 0;
1368
1480
  }
1369
1481
 
1370
- _buildPayload(system, messages, notes) {
1482
+ /* ---------- opencode port: bağlam matematiği (overflow.ts) ---------- */
1483
+
1484
+ /* Seçili model için özet payı düşülmüş kullanılabilir girdi alanı */
1485
+ _usableContext(sel) {
1486
+ const ctx = modelContextOf(sel);
1487
+ if (!ctx) return 0;
1488
+ /* overflow.ts:14-19: reserved = min(COMPACTION_BUFFER, maxOutput) */
1489
+ const reserved = Math.min(COMPACTION_BUFFER, OUTPUT_TOKEN_MAX);
1490
+ return Math.max(0, ctx - reserved);
1491
+ }
1492
+
1493
+ /* Dinamik geçmiş bütçesi: model bağlamı genişse 18k sabitinin ÜSTÜNE çıkar
1494
+ (asla altına inmez); cache'siz sağlayıcılarda şişmeyi HISTORY_BUDGET_MAX sınırlar */
1495
+ _historyBudget(sel) {
1496
+ const usable = this._usableContext(sel);
1497
+ if (!usable) return this.historyTokenBudget;
1498
+ return Math.max(this.historyTokenBudget, Math.min(Math.floor(usable * 0.85), HISTORY_BUDGET_MAX));
1499
+ }
1500
+
1501
+ /* Geçmiş + sistem + araç şemaları için kaba toplam token tahmini */
1502
+ _contextEstimate(session, sel) {
1503
+ const msgs = session.messages.reduce((a, m) => a + estMsgTokens(m), 0);
1504
+ const toolsCost = TOOLS.length ? estTokens(JSON.stringify(TOOLS)) : 0;
1505
+ return Math.ceil((msgs + toolsCost + 3500) * this.tokRatio); // 3500 ≈ system payı
1506
+ }
1507
+
1508
+ /* assistant(tool_calls) + sonuç tool mesajlarını BÖLMEYEN birimler */
1509
+ _msgUnits(messages) {
1371
1510
  const units = [];
1372
1511
  let i = 0;
1373
1512
  while (i < messages.length) {
@@ -1382,19 +1521,22 @@ class Engine {
1382
1521
  i++;
1383
1522
  }
1384
1523
  }
1385
- const budget = this.historyTokenBudget;
1524
+ return units;
1525
+ }
1526
+
1527
+ _buildPayload(system, messages, notes, budget, summary) {
1528
+ const units = this._msgUnits(messages);
1529
+ const B = Math.max(2000, Number(budget) || this.historyTokenBudget);
1386
1530
  const maxUnits = notes ? NOTES_MAX_UNITS : units.length; // notlar varken pencereyi sıkılaştır
1387
1531
  const picked = [];
1388
1532
  let used = 0;
1389
1533
  for (let u = units.length - 1; u >= 0; u--) {
1390
1534
  if (picked.length >= maxUnits) break;
1391
1535
  const cost = units[u].reduce(
1392
- (a, m) =>
1393
- a +
1394
- clamp(Math.ceil(estMsgTokens(m) * this.tokRatio), 1, this.historyTokenBudget),
1536
+ (a, m) => a + clamp(Math.ceil(estMsgTokens(m) * this.tokRatio), 1, B),
1395
1537
  0
1396
1538
  );
1397
- if (picked.length && used + cost > budget) break;
1539
+ if (picked.length && used + cost > B) break;
1398
1540
  picked.unshift(units[u]);
1399
1541
  used += cost;
1400
1542
  }
@@ -1416,7 +1558,20 @@ class Engine {
1416
1558
  flat[k] = { ...m, content: String(m.content).slice(0, TOOL_OUT_KEEP) + '\n…[kırpıldı]' };
1417
1559
  }
1418
1560
  }
1419
- return [{ role: 'system', content: system }, ...flat];
1561
+ /* opencode compaction replay (message-v2.ts:521-572): özet, system'den
1562
+ HEMEN sonra sabit user mesajı olarak girer → önek cache'e uygun
1563
+ (compaction sonrası yalnız eklemeli büyür, system bir daha değişmez) */
1564
+ const head = [];
1565
+ if (summary) {
1566
+ head.push({
1567
+ role: 'user',
1568
+ content:
1569
+ '[ÖNCEKİ KONUŞMANIN ÖZETİ — birebir geçmişi buradan hatırla]\n' +
1570
+ summary +
1571
+ '\n[Konuşma aşağıdaki mesajlarla DEVAM EDİYOR]',
1572
+ });
1573
+ }
1574
+ return [{ role: 'system', content: system }, ...head, ...flat];
1420
1575
  }
1421
1576
 
1422
1577
  _lastUserText(session) {
@@ -1479,6 +1634,7 @@ class Engine {
1479
1634
  if (seen++ >= keepFrom) msgLines.push(line);
1480
1635
  }
1481
1636
  out.push(...msgLines);
1637
+ out.push(JSON.stringify({ t: 'summary', text: session.summary || '', at: 0 }));
1482
1638
  out.push(JSON.stringify({ t: 'compacted', at: nowIso(), droppedBefore: keepFrom, noteBased: true }));
1483
1639
  const tmp = file + '.tmp';
1484
1640
  fs.writeFileSync(tmp, out.join('\n') + '\n');
@@ -1486,6 +1642,202 @@ class Engine {
1486
1642
  } catch {}
1487
1643
  }
1488
1644
 
1645
+ /* ---------- opencode port: gerçek compaction + prune (compaction.ts) ---------- */
1646
+
1647
+ /* Head bölgesini düz metne serileştirir (compaction.ts:54-85 port).
1648
+ Araç çıktıları SUMMARY_OUT_KEEP ile kırpılır — özet çağrısı küçük kalır. */
1649
+ _serializeHead(msgs) {
1650
+ const lines = [];
1651
+ for (const m of msgs) {
1652
+ if (m.role === 'user') {
1653
+ lines.push('[Kullanıcı]: ' + this._plainText(m).slice(0, 1500));
1654
+ } else if (m.role === 'assistant') {
1655
+ const txt = this._plainText(m).trim();
1656
+ if (txt) lines.push('[Asistan]: ' + txt.slice(0, 1200));
1657
+ for (const tc of m.tool_calls || []) {
1658
+ let args = '';
1659
+ try {
1660
+ args = JSON.stringify(JSON.parse((tc.function && tc.function.arguments) || '{}'));
1661
+ } catch {
1662
+ args = String((tc.function && tc.function.arguments) || '');
1663
+ }
1664
+ lines.push(`[Araç çağrısı]: ${tc.function && tc.function.name}(${args.slice(0, 800)})`);
1665
+ }
1666
+ } else if (m.role === 'tool') {
1667
+ const out = String(m.content || '');
1668
+ lines.push(
1669
+ '[Araç sonucu]: ' + (out.length <= SUMMARY_OUT_KEEP ? out : out.slice(0, SUMMARY_OUT_KEEP) + '\n[kırpıldı]')
1670
+ );
1671
+ }
1672
+ }
1673
+ return lines.filter(Boolean).join('\n');
1674
+ }
1675
+
1676
+ /* compaction.ts:319-557 port. Bağlam taşarsa: kuyruk (son ~%25) birebir
1677
+ korunur, baş metne serileştirilip TEK küçük model çağrısıyla özetlenir,
1678
+ geçmiş [özet + kuyruk] olarak yeniden yazılır. Döndürür: compact oldu mu */
1679
+ async _compactHistory(session, sel, signal) {
1680
+ try {
1681
+ const usable = this._usableContext(sel) || this.historyTokenBudget * 2;
1682
+ const budget = clamp(Math.floor(usable * 0.25), MIN_PRESERVE_RECENT, MAX_PRESERVE_RECENT);
1683
+ const units = this._msgUnits(session.messages);
1684
+ if (units.length < 3) return false;
1685
+ let keepIdx = 0;
1686
+ let used = 0;
1687
+ for (let u = units.length - 1; u >= 0; u--) {
1688
+ const cost = units[u].reduce((a, m) => a + Math.ceil(estMsgTokens(m) * this.tokRatio), 0);
1689
+ if (used + cost > budget) break;
1690
+ keepIdx = u;
1691
+ used += cost;
1692
+ }
1693
+ if (keepIdx <= 0) return false; // özetlenecek head yok
1694
+ const headMsgs = units.slice(0, keepIdx).flat();
1695
+ const transcript = this._serializeHead(headMsgs);
1696
+ if (!transcript.trim()) return false;
1697
+
1698
+ /* core/session/compaction.ts:16-56 şablonu (Türkçe başlıklarla) */
1699
+ const prev = session.summary
1700
+ ? `<önceki-özet>\n${session.summary}\n</önceki-özet>\n\n` +
1701
+ 'önceki-özet, konuşmadan önceki her şeyi özetler: ikisini BİRLEŞTİREREK yeni özet çıkar. ' +
1702
+ 'önceki-özet bu birleşimden sonra atılır — yeni özete taşımadığın her şey KAYBOLUR. ' +
1703
+ 'Konuşma daha yenidir; çelişkide konuşma kazanır. Biten işleri "Aktif"ten "Tamamlanan"a taşı.\n\n'
1704
+ : '';
1705
+ const prompt =
1706
+ 'Aşağıdaki <konuşma> etiketindeki sohbet geçmişinden, işe başka bir ajan devam edecek şekilde özet çıkar.\n' +
1707
+ 'Tam olarak <şablon> içindeki Markdown yapısını koru; şablon etiketlerini yanıtına yazma.\n' +
1708
+ '<şablon>\n' +
1709
+ '## Amaç\n- [kullanıcının ne yapmaya çalıştığı, 1-2 cümle]\n\n' +
1710
+ '## Önemli Detaylar\n- [kısıtlar, kararlar ve gerekçeleri, önemli olgular, kesin yollar/sayılar; yoksa "(yok)"]\n\n' +
1711
+ '## İş Durumu\n### Tamamlanan\n- [biten işler, doğrulanan olgular; yoksa "(yok)"]\n\n' +
1712
+ '### Aktif\n- [sürüyen işler, yarım değişiklikler; yoksa "(yok)"]\n\n' +
1713
+ '### Engelli\n- [hatalar, bilinmeyenler; yoksa "(yok)"]\n\n' +
1714
+ '## Sonraki Adım\n1. [somut ilk eylem; yoksa "(yok)"]\n\n' +
1715
+ '## İlgili Dosyalar\n- [dosya/klasör yolu: neden önemli; yoksa "(yok)"]\n' +
1716
+ '</şablon>\n\n' +
1717
+ 'Kurallar:\n- Boş olsa bile her bölümü koru.\n- Kısa madde maddeler yaz, paragraf kurma.\n' +
1718
+ '- Dosya yollarını, komutları, hata metinlerini, URL\u2019leri BİREBİR aktar.\n' +
1719
+ '- Özetleme yaptığını asla belirtme.\n\n' +
1720
+ prev +
1721
+ `<konuşma>\n${transcript}\n</konuşma>`;
1722
+
1723
+ emitSafe(this, session.id, {
1724
+ type: 'status',
1725
+ status: `⇩ bağlam doldu — opencode tarzı özetleme devrede (head ${headMsgs.length} mesaj, kuyruk ${session.messages.length - headMsgs.length} korunuyor)`,
1726
+ });
1727
+ const small = this.modelFor('subagent') || sel || this.sel;
1728
+ const res = await chatOnce(
1729
+ small,
1730
+ { messages: [{ role: 'user', content: prompt }], temperature: 0.2 },
1731
+ { signal }
1732
+ );
1733
+ const text = String(res.content || '').replace(/```(?:markdown)?/gi, '').trim();
1734
+ if (!text) return false;
1735
+ session.summary = text.slice(0, 12000);
1736
+ const tail = units.slice(keepIdx).flat();
1737
+ session.messages = tail;
1738
+ session.notesAt = clamp(session.notesAt || 0, 0, tail.length);
1739
+ this._persistCompaction(session);
1740
+ emitSafe(this, session.id, {
1741
+ type: 'status',
1742
+ status: `✓ bağlam özeti yazıldı — geçmiş ${headMsgs.length + tail.length} → ${tail.length} mesaj, önek cache tazelendi`,
1743
+ });
1744
+ return true;
1745
+ } catch {
1746
+ return false; // compaction asla sohbeti bozmasın
1747
+ }
1748
+ }
1749
+
1750
+ /* Compaction sonrası disk yeniden yazımı: meta/bot/todo/notes/summary +
1751
+ kuyruk mesajları. Atomik — patlarsa eski dosya sağlam kalır. */
1752
+ _persistCompaction(session) {
1753
+ try {
1754
+ const file = this._file(session.id);
1755
+ let meta = null, meta2 = null, bot = null, todo = null;
1756
+ for (const line of fs.readFileSync(file, 'utf8').split('\n')) {
1757
+ if (!line.trim()) continue;
1758
+ let r;
1759
+ try { r = JSON.parse(line); } catch { continue; }
1760
+ if (r.t === 'meta') meta = line;
1761
+ else if (r.t === 'meta2') meta2 = line;
1762
+ else if (r.t === 'bot') bot = line;
1763
+ else if (r.t === 'todo') todo = line; // append-only: son satır kazanır
1764
+ }
1765
+ const out = [];
1766
+ if (meta) out.push(meta);
1767
+ else out.push(JSON.stringify({ t: 'meta', id: session.id, code: session.code, createdAt: session.createdAt }));
1768
+ if (meta2) out.push(meta2);
1769
+ if (bot) out.push(bot);
1770
+ if (todo) out.push(todo);
1771
+ out.push(JSON.stringify({ t: 'notes', text: session.notes || '', at: session.notesAt || 0 }));
1772
+ out.push(JSON.stringify({ t: 'summary', text: session.summary || '', at: nowIso() }));
1773
+ for (const m of session.messages) out.push(JSON.stringify({ t: 'msg', ...m }));
1774
+ out.push(JSON.stringify({ t: 'compacted', at: nowIso(), summary: true }));
1775
+ const tmp = file + '.tmp';
1776
+ fs.writeFileSync(tmp, out.join('\n') + '\n');
1777
+ fs.renameSync(tmp, file);
1778
+ } catch {}
1779
+ }
1780
+
1781
+ /* Taşma kontrolü (overflow.ts:22-34): gerçek usage varsa onu esas al,
1782
+ yoksa tahmini kullan. usable'ın %95'i dolunca compaction tetiklenir. */
1783
+ _overContext(session, sel, lastUsageTotal) {
1784
+ const usable = this._usableContext(sel);
1785
+ if (!usable) return false;
1786
+ const total =
1787
+ lastUsageTotal ||
1788
+ session.messages.reduce((a, m) => a + estMsgTokens(m), 0) * this.tokRatio + 6000;
1789
+ return total >= usable * 0.95;
1790
+ }
1791
+
1792
+ /* compaction.ts:273-317 prune port: sondan geriye yürür, en yeni kullanıcı
1793
+ turunu korur; PRUNE_PROTECT (40k) aşan eski araç çıktılarını temizler.
1794
+ En az PRUNE_MINIMUM (20k) kazanç yoksa hiç dokunmaz. tool_call_id çiftleri
1795
+ korunur — sadece içerik temizlenir, önek cache bozulmaz. Döndürür: kazanılan token */
1796
+ _pruneSession(session) {
1797
+ try {
1798
+ if (this.ctrls.has(session.id)) return 0;
1799
+ const msgs = session.messages;
1800
+ let total = 0, pruned = 0, turns = 0;
1801
+ const targets = [];
1802
+ for (let i = msgs.length - 1; i >= 0; i--) {
1803
+ const m = msgs[i];
1804
+ if (m.role === 'user') turns++;
1805
+ if (turns < 2) continue; // en yeni tur dokunulmaz
1806
+ if (m.role === 'tool' && typeof m.content === 'string' && m.content.length > 400) {
1807
+ const est = Math.ceil(estTokens(m.content) * this.tokRatio);
1808
+ total += est;
1809
+ if (total > PRUNE_PROTECT) {
1810
+ pruned += est;
1811
+ targets.push(m);
1812
+ }
1813
+ }
1814
+ }
1815
+ if (pruned <= PRUNE_MINIMUM) return 0;
1816
+ const marker = '[eski araç çıktısı temizlendi — özet ve son çıktılar yeterli]';
1817
+ const ids = new Map(targets.map((m) => [m.tool_call_id, marker]));
1818
+ for (const m of targets) m.content = marker;
1819
+ try {
1820
+ const file = this._file(session.id);
1821
+ const lines = fs.readFileSync(file, 'utf8').split('\n');
1822
+ for (let i = 0; i < lines.length; i++) {
1823
+ if (!lines[i].trim()) continue;
1824
+ let r;
1825
+ try { r = JSON.parse(lines[i]); } catch { continue; }
1826
+ if (r.t === 'msg' && r.role === 'tool' && ids.has(r.tool_call_id)) {
1827
+ r.content = marker;
1828
+ lines[i] = JSON.stringify({ t: 'msg', ...r });
1829
+ }
1830
+ }
1831
+ const tmp = file + '.tmp';
1832
+ fs.writeFileSync(tmp, lines.join('\n'));
1833
+ fs.renameSync(tmp, file);
1834
+ } catch {}
1835
+ return pruned;
1836
+ } catch {
1837
+ return 0;
1838
+ }
1839
+ }
1840
+
1489
1841
  /* Cevap verildikten sonra çağrılır: son not update'ten bu yana NOTES_EVERY
1490
1842
  (14) mesaj biriktiginde özet çıkarıp oturum notlarını günceller.
1491
1843
  Notlar oturum dosyasına (code ile eşli) yazılır ve _chatTurn'da aynı
@@ -1801,6 +2153,11 @@ class Engine {
1801
2153
  _bgGroupRecord(title, status, summary) {
1802
2154
  for (const [gid, g] of this._bgGroups) {
1803
2155
  if (g.dead) continue;
2156
+ /* üst oturum silinmişse grup ölü sayılır — hayalet oturuma yazma */
2157
+ if (this._deletedSessions && this._deletedSessions.has(g.parentId)) {
2158
+ g.dead = true;
2159
+ continue;
2160
+ }
1804
2161
  g.results.push({ title, status, summary: String(summary || '').slice(0, 900) });
1805
2162
  if (g.results.length >= g.total) {
1806
2163
  this._bgGroups.delete(gid);
@@ -1998,8 +2355,18 @@ class Engine {
1998
2355
  const reason = Engine.superviseReason(job, now);
1999
2356
  if (!reason) continue;
2000
2357
  /* #16 öz-kurtarma: takılan ajan CEO'yu meşgul etmeden kendini toparlar */
2001
- if (reason === 'stuck' && (job.fixes || 0) < BG_FIX_MAX) {
2002
- this._bgSelfHeal(job, now);
2358
+ if (reason === 'stuck') {
2359
+ if ((job.fixes || 0) < BG_FIX_MAX) {
2360
+ this._bgSelfHeal(job, now);
2361
+ } else {
2362
+ /* öz-kurtarma hakları tükendi + hâlâ takılı → ZORLA KAPAT:
2363
+ ajan sonsuza dek 'running' kalırsa fan-out grubu ASLA tamamlanmaz
2364
+ ve birleşik rapor hiç düşmez — CEO "uyuyor" kalır */
2365
+ this._bgForceClose(
2366
+ job,
2367
+ `superyorizon: ajan ${Math.round((now - Date.parse(job.startedAt)) / 60000)} dk'dır takılı, öz-kurtarma hakları tükendi`
2368
+ );
2369
+ }
2003
2370
  continue;
2004
2371
  }
2005
2372
  job.lastNudgeAt = nowIso();
@@ -2040,6 +2407,26 @@ class Engine {
2040
2407
  this._bgKick(job, text);
2041
2408
  }
2042
2409
 
2410
+ /* Takılı işi ZORLA sonlandır: tur abort edilir, iş kaydı SEBEPİYLE kapanır,
2411
+ grup üyesiyse 'aborted' olarak gruba yazılır → birleşik rapor AKAR,
2412
+ CEO "uyumak" yerine hemen sonucu kullanıcıya aktarır. */
2413
+ _bgForceClose(job, reason) {
2414
+ const sid = String(job.id);
2415
+ job.revive = false; /* kick-in-flight guard'ı devre dışı — bu gerçek bir bitiş */
2416
+ this._clearKicks(sid);
2417
+ this._abortReasons = this._abortReasons || new Map();
2418
+ this._abortReasons.set(sid, this._abortReason(reason));
2419
+ const ctrl = this.ctrls.get(sid);
2420
+ if (ctrl) {
2421
+ try { ctrl.abort(); } catch {}
2422
+ /* _run catch'i sebebi okuyup _bgFinish('aborted') yapacak */
2423
+ } else {
2424
+ /* tur zaten yok — kaydı doğrudan kapat */
2425
+ this._bgFinish(sid, 'aborted', reason);
2426
+ }
2427
+ try { log.info('bg', `iş zorla kapatıldı: ${job.title} — ${reason}`); } catch {}
2428
+ }
2429
+
2043
2430
  /* Arka plan oturumuna müdahale: asılı turu kes (ctrl.abort), sonra
2044
2431
  kurtarma mesajını bas. abort → eski _run sonlanır → send yeni tur açar.
2045
2432
  revive bayrağı: /stop veya silme sonrası bekleyen müdahaleler patlamasın. */
@@ -2093,6 +2480,11 @@ class Engine {
2093
2480
  try {
2094
2481
  const meta = this._bgMeta && this._bgMeta.get(String(bgSessionId));
2095
2482
  if (!meta) return false;
2483
+ /* üst oturum silinmişse rapor düşer — hayalet oturum yaratma */
2484
+ if (this._deletedSessions && this._deletedSessions.has(meta.parentId)) {
2485
+ this._bgMeta.delete(String(bgSessionId));
2486
+ return false;
2487
+ }
2096
2488
  /* Öz-kurtarma/wrap-up kick'i bekliyorsa bu bir ABORT turudur — gerçek
2097
2489
  bitiş DEĞİL; erken "ARKA PLAN BİTTİ (sonuca ulaşmadan)" ilanı basma */
2098
2490
  const pendingKick = this._bgJobs.get(String(bgSessionId));
@@ -2134,14 +2526,19 @@ class Engine {
2134
2526
  }
2135
2527
  }
2136
2528
 
2137
- /* Ana oturum işini bitirince bekleyen arka plan raporlarını boşalt */
2529
+ /* Ana oturum işini bitirince bekleyen arka plan raporlarını boşalt.
2530
+ Güvenli: oturum SİLİNMİŞSE rapor düşer (hayalet oturum yaratılmaz);
2531
+ send başarısızsa (aniden meşgul oldu) rapor sırada kalır, sonra denenir. */
2138
2532
  flushPendingReports(sessionId) {
2139
2533
  if (!this._pendingReports || !this._pendingReports.length) return;
2140
2534
  const rest = [];
2141
2535
  for (const r of this._pendingReports) {
2142
- if (r.parentId === sessionId && !this.isBusy(sessionId)) {
2143
- try { this.send(sessionId, { text: r.text }); } catch {}
2144
- } else rest.push(r);
2536
+ if (r.parentId !== sessionId) { rest.push(r); continue; }
2537
+ if (this._deletedSessions && this._deletedSessions.has(sessionId)) continue;
2538
+ if (this.isBusy(sessionId)) { rest.push(r); continue; }
2539
+ let sent = false;
2540
+ try { sent = !!this.send(sessionId, { text: r.text }); } catch {}
2541
+ if (!sent) rest.push(r);
2145
2542
  }
2146
2543
  this._pendingReports = rest;
2147
2544
  }
@@ -2237,6 +2634,10 @@ class Engine {
2237
2634
  /* CEO: uygulayıcı araçlar kapalı — her şey paralel ajana devredilir */
2238
2635
  activeTools = activeTools.filter((t) => !CEO_EXEC_TOOLS.has(t.function.name));
2239
2636
  }
2637
+ if (session.bcCode && session.bcMode === 'plan') {
2638
+ /* opencode plan agent: salt-okur zorlaması — araç seti GERÇEKten daralır */
2639
+ activeTools = activeTools.filter((t) => PLAN_ALLOW_TOOLS.has(t.function.name));
2640
+ }
2240
2641
  if (perms.length === 1 && perms[0] === 'chat') {
2241
2642
  system +=
2242
2643
  '\n\n# KISITLI MOD\nTüm araçların (komut, dosya, web, tarayıcı, hafıza) kapalı. Sadece yazarak cevap ver. ' +
@@ -2257,7 +2658,15 @@ class Engine {
2257
2658
  `\n\n# OTURUM NOTLARI (oturum ${session.code || '?'} — bu oturumun önceki konuşma özeti; birebir geçmişi buradan hatırla)\n` +
2258
2659
  session.notes;
2259
2660
  }
2260
- let payload = this._buildPayload(system, session.messages, session.notes);
2661
+ /* önek-cache disiplini (opencode request.ts:184): araç sırası oturum
2662
+ boyunca sabit olmalı — alfabetik sıralama sağlayıcı önbelleğini bozmaz */
2663
+ activeTools = [...activeTools].sort((a, b) =>
2664
+ String(a.function.name).localeCompare(String(b.function.name))
2665
+ );
2666
+ /* opencode port: model bağlamına göre dinamik bütçe + compaction özeti */
2667
+ const selEarly = this.sessionModel.get(String(session.id)) || this.modelFor(null) || this.sel;
2668
+ const dynBudget = this._historyBudget(selEarly);
2669
+ let payload = this._buildPayload(system, session.messages, session.notes, dynBudget, session.summary);
2261
2670
  // Vision model sadece mesajda görsel varsa devreye girer; terminal ise prompt'un
2262
2671
  // shell/command işi olduğunda. Aksi halde ana model kullanılır.
2263
2672
  let role = this._lastUserHasImage(session) ? 'vision' : null;
@@ -2304,7 +2713,7 @@ class Engine {
2304
2713
  type: 'status',
2305
2714
  status: `\u{1F5BC} bu model görüntü girişini desteklemiyor — sohbetten ${removed} görsel kaldırıldı, mesaj görüntüsüz yanıtlanıyor (/change ile görsel destekli modele geçebilirsin)`,
2306
2715
  });
2307
- payload = this._buildPayload(system, session.messages, session.notes);
2716
+ payload = this._buildPayload(system, session.messages, session.notes, dynBudget, session.summary);
2308
2717
  res = await this._streamWithFallbacks(
2309
2718
  session,
2310
2719
  payload,
@@ -2327,6 +2736,38 @@ class Engine {
2327
2736
  return res;
2328
2737
  }
2329
2738
 
2739
+ /* opencode port (prompt.ts:96-100 + processor cleanup): önceki tur yarıda
2740
+ kesildiyse (abort/hata), sonucu gelmeyen tool çağrılarına sentetik hata
2741
+ yanıtı yazılır — katı sağlayıcıdaki "messages illegal" (400) önlenir */
2742
+ _repairOrphanTools(session) {
2743
+ try {
2744
+ const msgs = session.messages;
2745
+ for (let i = msgs.length - 1; i >= 0; i--) {
2746
+ const m = msgs[i];
2747
+ if (m.role === 'user') return; // temiz sınır — sorun yok
2748
+ if (m.role === 'assistant' && Array.isArray(m.tool_calls) && m.tool_calls.length) {
2749
+ const answered = new Set();
2750
+ for (let j = i + 1; j < msgs.length; j++) {
2751
+ if (msgs[j].role === 'tool') answered.add(msgs[j].tool_call_id);
2752
+ }
2753
+ for (const tc of m.tool_calls) {
2754
+ if (!tc || !tc.id || answered.has(tc.id)) continue;
2755
+ const toolMsg = {
2756
+ role: 'tool',
2757
+ tool_call_id: tc.id,
2758
+ name: (tc.function && tc.function.name) || '',
2759
+ content:
2760
+ '[araç çalıştırması kesildi — tur iptal/kesinti nedeniyle tamamlanamadı; gerekiyorsa yeniden çağır]',
2761
+ };
2762
+ msgs.push(toolMsg);
2763
+ try { this._append(session, toolMsg); } catch {}
2764
+ }
2765
+ return;
2766
+ }
2767
+ }
2768
+ } catch {}
2769
+ }
2770
+
2330
2771
  async _run(session) {
2331
2772
  const ctrl = new AbortController();
2332
2773
  this.ctrls.set(session.id, ctrl);
@@ -2335,8 +2776,16 @@ class Engine {
2335
2776
  try {
2336
2777
  if (!this.sel && !this.sessionModel.get(String(session.id))) throw new Error('Model yapılandırılmadı — %APPDATA%\\beast\\config.yaml ve .env kontrol et');
2337
2778
 
2779
+ /* opencode port: yetim tool_calls onarımı — önceki tur iptal/kesintiye
2780
+ uğradıysa sonuç bekleyen araç çağrılarına sentetik hata yanıtı yazılır;
2781
+ yoksa katı sağlayıcılar sonraki istekte 400 verir */
2782
+ this._repairOrphanTools(session);
2783
+
2338
2784
  let nudged = false; // görev listesi disiplini: run başına en fazla 1 hatırlatma
2339
2785
  let wrapNudged = false; // tur limitine yaklaşınca zarif kapanış (bg ajanlar)
2786
+ let usageTotal = 0; // en yüksek görülen total_tokens — compaction tetiği
2787
+ let compacted = false; // run başına en fazla 1 compaction
2788
+ const recentSigs = []; // doom-loop dedektörü: son araç imzaları
2340
2789
  for (let turn = 0; turn < MAX_TURNS; turn++) {
2341
2790
  /* SÜRE SINIRI YOK — ama sonsuz tur da yok: bg ajan son 3 tura gelirken
2342
2791
  "raporu yaz ve bitir" uyarısı alır; MAX_TURNS sert tavan olarak kalır. */
@@ -2353,11 +2802,40 @@ class Engine {
2353
2802
  } catch {}
2354
2803
  emit({ type: 'message', message: wmsg });
2355
2804
  }
2805
+ /* opencode port (prompt.ts:1281): ana oturumda son turda zorunlu
2806
+ nihai cevap — tur limiti sessizce dolup boş 'done' dönmesin */
2807
+ if (!session.bgJob && turn === MAX_TURNS - 1 && !wrapNudged) {
2808
+ wrapNudged = true;
2809
+ const wmsg = {
2810
+ role: 'user',
2811
+ content:
2812
+ '[SON TUR] Tur limitine ulaşıldı: YENİ araç çağırma — eldeki bilgilerle NİHAİ cevabını şimdi ver. Eksikler varsa açıkça belirt.',
2813
+ };
2814
+ session.messages.push(wmsg);
2815
+ try {
2816
+ this._append(session, wmsg);
2817
+ } catch {}
2818
+ emit({ type: 'message', message: wmsg });
2819
+ }
2820
+ /* opencode port (prompt.ts:1161 + overflow.ts): bağlam dolduysa
2821
+ compaction checkpoint — kuyruk korunur, baş özetlenir */
2822
+ const selNow = this.sessionModel.get(String(sid)) || this.modelFor(null) || this.sel;
2823
+ if (!compacted && this._overContext(session, selNow, usageTotal)) {
2824
+ compacted = true;
2825
+ await this._compactHistory(session, selNow, ctrl.signal);
2826
+ }
2356
2827
  this._bgTrim(session); // #21 bg geçmişini olabildiğince ince tut
2357
2828
  emit({ type: 'status', status: 'thinking' });
2358
2829
  const res = await this._chatTurn(session, ctrl.signal, (delta) =>
2359
2830
  emit({ type: 'token', delta })
2360
2831
  );
2832
+ /* opencode port (processor.ts:477-482): step-finish usage'ı compaction
2833
+ tetiğine besle — gerçek toplam biliniyorsa tahmine gerek kalmaz */
2834
+ const uTot =
2835
+ res.usage &&
2836
+ (res.usage.total_tokens ||
2837
+ (res.usage.prompt_tokens || 0) + (res.usage.completion_tokens || 0));
2838
+ if (uTot > usageTotal) usageTotal = uTot;
2361
2839
 
2362
2840
  const assistant = { role: 'assistant', content: res.content || '' };
2363
2841
  if (res.toolCalls && res.toolCalls.length) assistant.tool_calls = res.toolCalls;
@@ -2393,6 +2871,7 @@ class Engine {
2393
2871
  Not/memory/skill bakımı artık arka planda (_postRunHousekeeping). */
2394
2872
  this._clearCrash();
2395
2873
  this._maybeCompact(sid);
2874
+ this._pruneSession(session); // opencode port: eski araç çıktıları temizlenir (fork edilen prune)
2396
2875
  emit({ type: 'done', usage: res.usage || null, meta: res.meta || null });
2397
2876
  this._bgFinish(sid, 'done');
2398
2877
  this.flushPendingReports(sid);
@@ -2402,16 +2881,45 @@ class Engine {
2402
2881
 
2403
2882
  // Paralel yürütme — bağımsız çağrılar beklemesin
2404
2883
  session.toolsSinceReflect = (session.toolsSinceReflect || 0) + res.toolCalls.length;
2884
+ if (!this._knownToolNames) this._knownToolNames = new Set(TOOLS.map((t) => t.function.name));
2405
2885
  await Promise.all(
2406
2886
  res.toolCalls.map(async (tc) => {
2407
- const name = tc.function && tc.function.name;
2887
+ let name = tc.function && tc.function.name;
2888
+ /* opencode port (llm.ts:296-312 repairToolCall): model aracı adını
2889
+ yanlış yazdıysa büyük/küçük harf normalizasyonuyle onar — tur kaybı yok */
2890
+ if (name && !this._knownToolNames.has(name)) {
2891
+ const fixed = TOOLS.find((t) => t.function.name.toLowerCase() === String(name).trim().toLowerCase());
2892
+ if (fixed) {
2893
+ emitSafe(this, sid, {
2894
+ type: 'status',
2895
+ status: `🔧 araç adı düzeltildi: ${name} → ${fixed.function.name}`,
2896
+ });
2897
+ name = fixed.function.name;
2898
+ }
2899
+ }
2408
2900
  let args = {};
2409
2901
  try {
2410
2902
  args = JSON.parse((tc.function && tc.function.arguments) || '{}');
2411
2903
  } catch {}
2412
2904
  emit({ type: 'tool-start', callId: tc.id, name, args });
2413
2905
  emit({ type: 'status', status: name });
2414
- let out = await this._execTool(name, args, ctrl.signal, sid);
2906
+ /* opencode port (processor.ts:29 + 356-380): aynı araç + birebir
2907
+ aynı argüman 3 kez koştuysa 4.'yü çalıştırma — döngüye para/hız
2908
+ akıtma; modele hata bildirimiyle yol göster */
2909
+ const sig = name + '\u0000' + String((tc.function && tc.function.arguments) || '');
2910
+ const dups = recentSigs.reduce((a, s) => a + (s === sig ? 1 : 0), 0);
2911
+ recentSigs.push(sig);
2912
+ let out;
2913
+ if (dups >= DOOM_LOOP_THRESHOLD) {
2914
+ out = JSON.stringify({
2915
+ error:
2916
+ `doom-loop: ${name} aynı argümanlarla ${DOOM_LOOP_THRESHOLD} kez çalıştı ve hep aynı sonucu verdi. ` +
2917
+ 'Aynı çağrıyı tekrarlamak işe yaramaz — farklı bir yöntem/argüman dene ya da eldeki bilgilerle nihai cevabı ver.',
2918
+ });
2919
+ emitSafe(this, sid, { type: 'status', status: `⛔ doom-loop: ${name} tekrarı engellendi` });
2920
+ } else {
2921
+ out = await this._execTool(name, args, ctrl.signal, sid);
2922
+ }
2415
2923
  // Ekran görüntüsü gibi araçlar görseli sonraki tura enjekte eder
2416
2924
  let injectedImage = null;
2417
2925
  try {
@@ -2474,6 +2982,14 @@ class Engine {
2474
2982
  } finally {
2475
2983
  this.ctrls.delete(sid);
2476
2984
  emit({ type: 'status', status: 'idle' });
2985
+ /* bekleyen paralel-ajan raporları HER bitiş yolunda boşaltılır —
2986
+ hata/durdurma/tur-limiti sonrası bile sonuç raporu kullanıcıya ulaşır.
2987
+ DİKKAT: buradan flush'u DOĞRUDAN çağırma! send → _run senkron-hata
2988
+ yolunda (model-yok gibi) await'siz zincirlenir → stack taşması.
2989
+ Macrotask'a ertele: her hop taze stack alır, pop-gönder sırası bozulmaz. */
2990
+ const fl = () => { try { this.flushPendingReports(sid); } catch {} };
2991
+ const t = setTimeout(fl, 0);
2992
+ if (t.unref) t.unref();
2477
2993
  }
2478
2994
  }
2479
2995
 
@@ -2774,7 +3290,7 @@ class Engine {
2774
3290
  t.function.name !== 'delegate_task' &&
2775
3291
  t.function.name !== 'set_reminder' &&
2776
3292
  !t.function.name.startsWith('email_')
2777
- );
3293
+ ).sort((a, b) => String(a.function.name).localeCompare(String(b.function.name))); // önek-cache: sabit sıra
2778
3294
  const role = 'subagent';
2779
3295
  const sel = this.modelFor(role) || this.sel;
2780
3296
  if (this.roleModels[role]) {
@@ -2796,7 +3312,12 @@ class Engine {
2796
3312
  for (let turn = 0; turn < SUB_MAX_TURNS; turn++) {
2797
3313
  const res = await chatStreamAuto(
2798
3314
  sel,
2799
- { messages: [{ role: 'system', content: system }, ...msgs.slice(1)], tools: subTools, reasoningEffort: this._thinkEffort() },
3315
+ {
3316
+ messages: [{ role: 'system', content: system }, ...msgs.slice(1)],
3317
+ tools: subTools,
3318
+ reasoningEffort: this._thinkEffort(),
3319
+ cacheKey: String(sessionId || 'subagent'),
3320
+ },
2800
3321
  { signal: ctrl.signal }
2801
3322
  );
2802
3323
  const assistant = { role: 'assistant', content: res.content || '' };