beast-agent 1.4.2 → 1.5.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": "1.4.2",
4
+ "version": "1.5.0",
5
5
  "description": "Ultra-fast local agent shell for Windows.",
6
6
  "author": "algokodcom (AlgoKod)",
7
7
  "license": "MIT",
@@ -216,6 +216,7 @@ function sanitizeTodoItems(items) {
216
216
  return out;
217
217
  }
218
218
 
219
+
219
220
  function customChain(list) {
220
221
  const out = [];
221
222
  for (const p of Array.isArray(list) ? list : []) {
@@ -269,6 +270,11 @@ class Engine {
269
270
  this._customProviders = opts.customProviders || [];
270
271
  this.cache = new Map();
271
272
  this.ctrls = new Map();
273
+ /* /STOP KAPISI: stopAll sonrası SİSTEM tetikli gönderimler (rapor, kick,
274
+ self-heal, cron, izleyici, olay merkezi, bg kuyruk) engellenir — ajan
275
+ kendi kendine yeni sorgu açamaz. Gerçek kullanıcı mesajı (userAction)
276
+ veya /start (clearStop) kapıyı açar. */
277
+ this._stopped = false;
272
278
  this.todos = new Map(); // sessionId -> [{title,status}]
273
279
  this.tokRatio = 1; // gerçek prompt_tokens ile kalibre edilir
274
280
  this._codeIndex = new Map(); // kısa oturum kodu -> session id
@@ -946,7 +952,7 @@ class Engine {
946
952
 
947
953
  /* Oturum dosyasındaki son 'msg' satırını (user) güncel içerikle değiştir —
948
954
  yanıtsız kalmış user mesajıyla yeni mesaj birleştirildiğinde kullanılır */
949
- _rewriteLastMsg(id, content) {
955
+ _rewriteLastMsg(id, content, attachments) {
950
956
  try {
951
957
  const file = this._file(String(id));
952
958
  const lines = fs.readFileSync(file, 'utf8').split('\n');
@@ -955,7 +961,9 @@ class Engine {
955
961
  let r;
956
962
  try { r = JSON.parse(lines[i]); } catch { continue; }
957
963
  if (r.t === 'msg' && r.role === 'user') {
958
- lines[i] = JSON.stringify({ t: 'msg', role: 'user', content: String(content || '') });
964
+ const out = { t: 'msg', role: 'user', content: String(content || '') };
965
+ if (Array.isArray(attachments) && attachments.length) out.attachments = attachments;
966
+ lines[i] = JSON.stringify(out);
959
967
  break;
960
968
  }
961
969
  }
@@ -1394,7 +1402,8 @@ class Engine {
1394
1402
  '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. Dosya işlemleri için ÖZEL ARAÇLARI kullan (edit_file/write_file/read_file/grep/glob); run_command terminal işlerindir (build, git, kurulum, paket) — dosya düzenlemeyi komut/scripte yedirme, edit_file ile yap. İlgisiz yeniden biçimleme/kayıp boşluk değişikliği YAPMA. edit_file/write_file sonucu additions/deletions döner ve değişiklik diske ANINDA uygulanır — doğrulamak için dosyayı TEKRAR OKUMA YASAK; sonraki editi önceki okuduğun içerik + kendi değişikliklerin üzerinden zincirle.\n' +
1395
1403
  '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. Doğrulama read_file ile DEĞİL run_command ile yapılır.\n' +
1396
1404
  '5) RAPOR: 1-3 satır — ne değişti + doğrulama sonucu (ör. "npm test ✓ 154/154"). Uzun açıklama yok.\n' +
1397
- 'CANLI ÖNİZLEME: web sitesi/app üretirsen son adımda ÇALIŞIR halde bırak (dev server ayakta ya da index.html yazılmış) panel bitince ürettiğin şeyi dahili tarayıcıda otomatik canlı açar.\n' +
1405
+ 'CANLI ÖNİZLEME: panel Preview\u2019da DAHİLİ STATİK SUNUCUYU kendisi yönetir statik site için sunucu başlatma DENEME (python -m http.server vs. GEREKMEZ); index.html\u2019i hazır bırak, kullanıcı Preview\u2019a basınca site sunucudan canlı açılır ve sana observe ile haber verilir. Yalnızca gerçek dev-server/build gerektiren projelerde (React/Vite/Next/Expo) kendi sunucunu BLOKLAMADAN arka planda başlat ve çalışan adresi (http://localhost:PORT) raporda yaz. file:// protokolü ASLA kullanılmaz.\n' +
1406
+ 'MOBİL UYGULAMA: React Native/Expo ile yap — `npx create-expo-app <ad>` + `npx expo start` (BLOKLAMADAN arka planda; başlangıç çıktısında http://localhost:8081 görünür, panel Telefon Modunda canlı açar, dosya kaydında hot reload çalışır). APK: `npx expo prebuild` + android\\gradlew assembleDebug; iOS: Windows\u2019ta simülatör YOK — gerçek cihazda Expo Go (QR) veya EAS cloud build kullan.\n' +
1398
1407
  'HIZ KURALLARI:\n' +
1399
1408
  '- 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' +
1400
1409
  '- Bağımsız araç çağrılarını AYNI TURDA paralel ver (birden çok read_file tek turda).\n' +
@@ -1593,7 +1602,37 @@ class Engine {
1593
1602
  }
1594
1603
 
1595
1604
  _buildPayload(system, messages, notes, budget, summary) {
1596
- const units = this._msgUnits(messages);
1605
+ /* DOSYA EK ENJEKSİYONU (payload-only): kullanıcı mesajlarındaki dosya
1606
+ ekleri SADECE burada, LLM isteğine metin olarak eklenir. Oturum
1607
+ mesajı/ekran temiz kalır — chat'te yalnız dosya kartı görünür. */
1608
+ let payloadSrc = messages;
1609
+ const withFiles = messages.map((m) => {
1610
+ if (!m || m.role !== 'user' || !Array.isArray(m.attachments)) return m;
1611
+ const docs = m.attachments.filter((a) => a && a.type === 'file' && a.content);
1612
+ if (!docs.length) return m;
1613
+ const extra = docs
1614
+ .map((f) => `[Ek dosya: ${f.name}]\n${String(f.content || '')}`)
1615
+ .join('\n\n');
1616
+ const { attachments: _drop, ...rest } = m;
1617
+ if (typeof rest.content === 'string') {
1618
+ rest.content = (rest.content + (rest.content ? '\n\n' : '') + extra).trim();
1619
+ } else if (Array.isArray(rest.content)) {
1620
+ let hit = false;
1621
+ rest.content = rest.content.map((p) => {
1622
+ if (p && p.type === 'text' && !hit) {
1623
+ hit = true;
1624
+ return { ...p, text: ((p.text || '') + (p.text ? '\n\n' : '') + extra).trim() };
1625
+ }
1626
+ return p;
1627
+ });
1628
+ if (!hit) rest.content = [...rest.content, { type: 'text', text: extra }];
1629
+ } else {
1630
+ rest.content = extra;
1631
+ }
1632
+ return rest;
1633
+ });
1634
+ payloadSrc = withFiles;
1635
+ const units = this._msgUnits(payloadSrc);
1597
1636
  const B = Math.max(2000, Number(budget) || this.historyTokenBudget);
1598
1637
  const maxUnits = notes ? NOTES_MAX_UNITS : units.length; // notlar varken pencereyi sıkılaştır
1599
1638
  const picked = [];
@@ -1657,6 +1696,13 @@ class Engine {
1657
1696
  '\n[Konuşma aşağıdaki mesajlarla DEVAM EDİYOR]',
1658
1697
  });
1659
1698
  }
1699
+ /* emniyet: sağlayıcıya GİDEN hiçbir mesajda `attachments` alanı olmasın */
1700
+ for (let k = 0; k < flat.length; k++) {
1701
+ if (flat[k] && flat[k].attachments) {
1702
+ const { attachments: _drop, ...rest } = flat[k];
1703
+ flat[k] = rest;
1704
+ }
1705
+ }
1660
1706
  return [{ role: 'system', content: system }, ...head, ...flat];
1661
1707
  }
1662
1708
 
@@ -2009,14 +2055,16 @@ class Engine {
2009
2055
  const sid = session.id;
2010
2056
  if (session.bgJob || session.bcCode || session.isBotDm) return;
2011
2057
  if (prevSignal && prevSignal.aborted) return;
2058
+ /* /stop kapısı: not/memory/yansıma bakımı YENİ LLM SORGUSU açmaz */
2059
+ if (this._stopped) return;
2012
2060
  if (!this._housekeepingTail) this._housekeepingTail = new Map();
2013
2061
  const job = this._housekeepingTail.get(sid) || Promise.resolve();
2014
2062
  const next = job.catch(() => {}).then(async () => {
2015
2063
  const ctl = new AbortController();
2016
2064
  try { await this._updateSessionNotes(session, ctl.signal); } catch {}
2017
- if (ctl.signal.aborted || this.ctrls.has(sid)) return;
2065
+ if (ctl.signal.aborted || this._stopped || this.ctrls.has(sid)) return;
2018
2066
  try { await this._autoMemory(session, ctl.signal); } catch {}
2019
- if (this.reflection.enabled && !ctl.signal.aborted && !this.ctrls.has(sid)) {
2067
+ if (this.reflection.enabled && !ctl.signal.aborted && !this._stopped && !this.ctrls.has(sid)) {
2020
2068
  try { await this._maybeReflectSkill(session); } catch {}
2021
2069
  }
2022
2070
  try { memory.calibratePersona(); } catch {}
@@ -2026,7 +2074,12 @@ class Engine {
2026
2074
 
2027
2075
  /* ---------- agent loop ---------- */
2028
2076
 
2029
- send(sessionId, payload) {
2077
+ send(sessionId, payload, opts = {}) {
2078
+ const userAction = !!(opts && opts.userAction);
2079
+ /* /stop kapısı: sistem tetikli gönderim durdurulur; kullanıcının kendi
2080
+ mesajı (userAction) kapıyı kaldırır — "devam için bir şeyler yaz" */
2081
+ if (this._stopped && !userAction) return false;
2082
+ if (userAction) this._stopped = false;
2030
2083
  const s = this._load(String(sessionId));
2031
2084
  if (!s || this.ctrls.has(s.id)) return false;
2032
2085
 
@@ -2037,17 +2090,28 @@ class Engine {
2037
2090
  const text = String((payload && payload.text) || '').slice(0, USER_MAX);
2038
2091
  const atts = Array.isArray(payload && payload.attachments) ? payload.attachments : [];
2039
2092
  const imgs = atts.filter((a) => a.type === 'image' && a.dataUrl).slice(0, 4);
2040
- const files = atts.filter((a) => a.type === 'file');
2041
- let extra = '';
2042
- for (const f of files) {
2043
- extra += `\n\n[Ek dosya: ${f.name}]\n${String(f.content || '').slice(0, 20000)}`;
2044
- }
2093
+ /* DOSYA EKLERİ İÇERİĞE GÖMÜLMEZ: mesajda `attachments` alanında taşınır —
2094
+ chat ekranında DOSYA KARTI olarak görünür, içerik ekrana yazılmaz.
2095
+ İçeriği yalnızca _buildPayload LLM isteğine enjekte eder. */
2096
+ const files = atts
2097
+ .filter((a) => a.type === 'file')
2098
+ .map((a) => ({ name: String(a.name || 'dosya'), content: String(a.content || '').slice(0, 20000) }));
2099
+ const body = (text.trim() || (files.length || imgs.length ? '[dosya ekleri]' : '(ek)')).slice(0, USER_MAX);
2045
2100
  if (imgs.length) {
2046
- const parts = [{ type: 'text', text: (text + extra).trim() }];
2101
+ const parts = [{ type: 'text', text: body }];
2047
2102
  for (const im of imgs) parts.push({ type: 'image_url', image_url: { url: im.dataUrl } });
2048
2103
  msg = { role: 'user', content: parts };
2049
2104
  } else {
2050
- msg = { role: 'user', content: ((text + extra).trim() || '(ek)').slice(0, USER_MAX + extra.length) };
2105
+ msg = { role: 'user', content: body };
2106
+ }
2107
+ if (imgs.length || files.length) {
2108
+ /* UI dosya kartları + payload enjeksiyonu buradan beslenir:
2109
+ dosyalar İÇERİKLE birlikte `attachments`ta yaşar (görüntüler yalnız
2110
+ ad taşır — veri content parçalarındadır). Chat ekranı yalnız ADI basar. */
2111
+ msg.attachments = [
2112
+ ...imgs.map((im) => ({ type: 'image', name: String(im.name || 'resim') })),
2113
+ ...files,
2114
+ ];
2051
2115
  }
2052
2116
  }
2053
2117
 
@@ -2067,8 +2131,11 @@ class Engine {
2067
2131
  typeof msg.content === 'string'
2068
2132
  ) {
2069
2133
  lastMsg.content = lastMsg.content + '\n' + msg.content;
2134
+ if (msg.attachments || lastMsg.attachments) {
2135
+ lastMsg.attachments = [...(lastMsg.attachments || []), ...(msg.attachments || [])];
2136
+ }
2070
2137
  msg = lastMsg;
2071
- this._rewriteLastMsg(s.id, lastMsg.content);
2138
+ this._rewriteLastMsg(s.id, lastMsg.content, lastMsg.attachments);
2072
2139
  } else {
2073
2140
  s.messages.push(msg);
2074
2141
  try {
@@ -2141,6 +2208,8 @@ class Engine {
2141
2208
  runBackground(parentSessionId, task, title, opts = {}) {
2142
2209
  const t = String(task || '').trim();
2143
2210
  if (!t) return { ok: false, error: 'görev boş' };
2211
+ /* /stop kapısı: durdurulmuş sistemde yeni arka plan işi başlamaz */
2212
+ if (this._stopped) return { ok: false, error: '/stop aktif — arka plan işi başlatılamadı' };
2144
2213
  const parent = String(parentSessionId || '');
2145
2214
  /* createSession() view döndürür (messages yok) — tam oturumu cache'ten al.
2146
2215
  Aksi halde send() s.messages.push'da sessizce patlardı ve iş koşmazdı. */
@@ -2203,6 +2272,8 @@ class Engine {
2203
2272
 
2204
2273
  _bgAdmit(sid, fullTask) {
2205
2274
  try {
2275
+ /* /stop kapısı: kuyruğa alma ve başlatma durdurulur */
2276
+ if (this._stopped) return;
2206
2277
  const j = this._bgJobs.get(String(sid));
2207
2278
  if (!j || j.status !== 'running' || j.slot) return;
2208
2279
  if (this._runningBgCount() >= this._bgLimit) {
@@ -2221,6 +2292,9 @@ class Engine {
2221
2292
  }
2222
2293
 
2223
2294
  _bgResumeQueue() {
2295
+ /* /stop kapısı: sıradaki iş BAŞLATILMAZ (status mutate edilmeden dönülür —
2296
+ hayalet 'running' kaydı oluşmasın) */
2297
+ if (this._stopped) return;
2224
2298
  while (this._runningBgCount() < this._bgLimit) {
2225
2299
  let next = null;
2226
2300
  for (const j of this._bgJobs.values()) {
@@ -2274,6 +2348,7 @@ class Engine {
2274
2348
 
2275
2349
  /* #17 kalıcı arka plan hatası → owner'a tek seferlik uyarı maili (fire-and-forget) */
2276
2350
  _notifyOwnerTaskFailed(job, msg) {
2351
+ if (this._stopped) return; /* /stop: durdurulan iş için mail UYANDIRMA */
2277
2352
  if (this.notifyOwnerFail === false) return;
2278
2353
  if (!this.email || typeof this.email.send !== 'function') return;
2279
2354
  if (job._mailed) return;
@@ -2597,6 +2672,8 @@ class Engine {
2597
2672
 
2598
2673
  _bgKickSend(sid, text) {
2599
2674
  try {
2675
+ /* /stop kapısı: durdurulan işe kurtarma kick'i GİTMEZ */
2676
+ if (this._stopped) return;
2600
2677
  const j = this._bgJobs.get(String(sid));
2601
2678
  /* silinmiş/durdurulmuş işe geri dönüş yok — hayalet dosya yaratma */
2602
2679
  if (!j || j.revive !== true) return;
@@ -2672,6 +2749,12 @@ class Engine {
2672
2749
  send başarısızsa (aniden meşgul oldu) rapor sırada kalır, sonra denenir. */
2673
2750
  flushPendingReports(sessionId) {
2674
2751
  if (!this._pendingReports || !this._pendingReports.length) return;
2752
+ /* /stop kapısı: abort edilen işlerin iptal raporları ebeveynde YENİ SORGU
2753
+ başlatmasın — bekleyen raporlar düşürülür */
2754
+ if (this._stopped) {
2755
+ this._pendingReports = [];
2756
+ return;
2757
+ }
2675
2758
  const rest = [];
2676
2759
  for (const r of this._pendingReports) {
2677
2760
  if (r.parentId !== sessionId) { rest.push(r); continue; }
@@ -2960,6 +3043,13 @@ class Engine {
2960
3043
  iş bitince dahili tarayıcıda CANLI açılır (site/dev server/HTML) */
2961
3044
  const bcArtifacts = session.bcCode ? { html: [], serverUrl: null } : null;
2962
3045
  for (let turn = 0; turn < maxTurns; turn++) {
3046
+ /* /STOP: abort edilmişse yeni tur/araç döngüsü AÇILMAZ — hemen
3047
+ AbortError fırlat, catch bloğu done(aborted) basar */
3048
+ if (ctrl.signal.aborted) {
3049
+ const e = new Error('iptal');
3050
+ e.name = 'AbortError';
3051
+ throw e;
3052
+ }
2963
3053
  /* SÜRE SINIRI YOK. Ana oturum sınırsız tur koşar (opencode birebir);
2964
3054
  yalnız bg ajan son 3 tura gelirken "raporu yaz ve bitir" uyarısı alır. */
2965
3055
  if (session.bgJob && turn === maxTurns - 3 && !wrapNudged) {
@@ -3214,6 +3304,8 @@ class Engine {
3214
3304
  teşhis görevini AYNI oturuma düşürür; agent kendi hatasını analiz eder. */
3215
3305
  _maybeSelfHeal(sid, err) {
3216
3306
  try {
3307
+ /* /stop kapısı: durdurulan oturuma teşhis sorgusu AÇILMAZ */
3308
+ if (this._stopped) return;
3217
3309
  const key = String(sid);
3218
3310
  this._errCount = this._errCount || new Map();
3219
3311
  const n = (this._errCount.get(key) || 0) + 1;
@@ -3701,6 +3793,8 @@ class Engine {
3701
3793
  });
3702
3794
  }
3703
3795
  }
3796
+ /* GERİ ALMA GÜNLÜĞÜ: dosya yazımından ÖNCE eski içerik kayda geçer */
3797
+ this._journalBefore(sessionId, name, args);
3704
3798
  if (name === 'memory_write') {
3705
3799
  /* bot oturumu → botun KENDİ MEMORY.md'sine yaz (global Beast hafızasına değil) */
3706
3800
  const bctx = this._sessionBotCtx(sessionId ? this.cache.get(String(sessionId)) : null);
@@ -3736,6 +3830,7 @@ class Engine {
3736
3830
  }
3737
3831
  if (name === 'todo_write') {
3738
3832
  const items = sanitizeTodoItems(args.items);
3833
+ this._tagTodoIds(sessionId, items); /* her madde kalıcı ID taşır — geri alma buna bağlanır */
3739
3834
  this.todos.set(sessionId, items);
3740
3835
  try {
3741
3836
  fs.appendFileSync(
@@ -4041,6 +4136,10 @@ class Engine {
4041
4136
  stopAll() {
4042
4137
  let aborted = 0;
4043
4138
  const why = '/stop: kullanıcı tüm ajanları ve turları durdurdu';
4139
+ /* STOP KAPISI: abort sonrası rapor/kick/kurtarma zincirleri YENİ SORGU
4140
+ AÇAMAZ — kullanıcı gerçek bir mesaj yazana ya da /start deyinceye dek
4141
+ ajan faaliyeti tamamen durur */
4142
+ this._stopped = true;
4044
4143
  /* fan-out gruplarını kapat — yarım grup artık birleşik rapor beklemesin */
4045
4144
  if (this._bgGroups) {
4046
4145
  for (const g of this._bgGroups.values()) g.dead = true;
@@ -4077,6 +4176,11 @@ class Engine {
4077
4176
  return aborted;
4078
4177
  }
4079
4178
 
4179
+ /* /stop kapısını kaldır (/start ya da gerçek kullanıcı mesajıyla) */
4180
+ clearStop() {
4181
+ this._stopped = false;
4182
+ }
4183
+
4080
4184
  isBusy(sessionId) {
4081
4185
  return this.ctrls.has(String(sessionId));
4082
4186
  }
@@ -4670,6 +4774,126 @@ const TOOLS = [
4670
4774
  },
4671
4775
  ];
4672
4776
 
4777
+ /* ---------- GÖREV (TODO) ID + GERİ ALMA SİSTEMİ ----------
4778
+ todo_write maddelerine kalıcı ID (T1, T2…) atanır; write_file/edit_file
4779
+ ÖNCESİ dosyanın eski içeriği günlüğe yazılır. Panel bir maddenin
4780
+ değişikliklerini TEK TUŞLA önceki kod tabanına döndürebilir. */
4781
+
4782
+ Engine.prototype._tagTodoIds = function (sid, items) {
4783
+ const key = String(sid || '');
4784
+ this._todoSeq = this._todoSeq || new Map();
4785
+ this._todoIds = this._todoIds || new Map();
4786
+ const map = this._todoIds.get(key) || new Map();
4787
+ let seq = this._todoSeq.get(key) || 0;
4788
+ for (const it of items || []) {
4789
+ const t = String((it && it.title) || '').trim();
4790
+ if (!t) continue;
4791
+ if (!map.has(t)) map.set(t, 'T' + (++seq));
4792
+ it.id = map.get(t);
4793
+ }
4794
+ this._todoIds.set(key, map);
4795
+ this._todoSeq.set(key, seq);
4796
+ return items;
4797
+ };
4798
+
4799
+ Engine.prototype._journalBefore = function (sid, name, args) {
4800
+ try {
4801
+ if (name !== 'write_file' && name !== 'edit_file') return;
4802
+ const rel = String((args && args.path) || '').trim();
4803
+ if (!rel) return;
4804
+ const ws = this._sessionWorkspace(String(sid || ''));
4805
+ const abs = path.isAbsolute(rel) ? path.resolve(rel) : path.resolve(ws, rel);
4806
+ let before = null;
4807
+ try { before = fs.readFileSync(abs, 'utf8'); } catch {}
4808
+ const todos = this.todos.get(String(sid || '')) || [];
4809
+ const cur = todos.find((t) => t && t.status !== 'done') || null;
4810
+ const key = String(sid || '');
4811
+ this._undoJournal = this._undoJournal || new Map();
4812
+ const j = this._undoJournal.get(key) || [];
4813
+ j.push({
4814
+ todoId: (cur && cur.id) || 'T0',
4815
+ title: (cur && cur.title) || '(görev listesi dışı)',
4816
+ path: abs,
4817
+ before,
4818
+ at: nowIso(),
4819
+ });
4820
+ while (j.length > 120) j.shift();
4821
+ this._undoJournal.set(key, j);
4822
+ } catch {}
4823
+ };
4824
+
4825
+ Engine.prototype.todoUndoInfo = function (sid) {
4826
+ const key = String(sid || '');
4827
+ this._undoJournal = this._undoJournal || new Map();
4828
+ const todos = this.todos.get(key) || [];
4829
+ const j = this._undoJournal.get(key) || [];
4830
+ const counts = new Map();
4831
+ for (const e of j) counts.set(e.todoId, (counts.get(e.todoId) || 0) + 1);
4832
+ return {
4833
+ ok: true,
4834
+ sessionId: key,
4835
+ todos,
4836
+ undo: todos.map((t) => ({ id: t.id, files: counts.get(t.id) || 0 })),
4837
+ lastTodoId: j.length ? j[j.length - 1].todoId : null,
4838
+ busy: this.ctrls.has(key),
4839
+ };
4840
+ };
4841
+
4842
+ Engine.prototype.undoTodo = function (sid, todoId) {
4843
+ const key = String(sid || '');
4844
+ const id = String(todoId || '');
4845
+ if (this.ctrls.has(key)) {
4846
+ return { ok: false, error: 'ajan şu an çalışıyor — önce ■ ile durdur, sonra geri al' };
4847
+ }
4848
+ this._undoJournal = this._undoJournal || new Map();
4849
+ const j = this._undoJournal.get(key) || [];
4850
+ if (!j.some((e) => e.todoId === id)) {
4851
+ return { ok: false, error: 'bu görev için kayıtlı değişiklik yok' };
4852
+ }
4853
+ const done = [];
4854
+ /* sondan başa geri sar — en son yazım önce restore edilir */
4855
+ for (let i = j.length - 1; i >= 0; i--) {
4856
+ const e = j[i];
4857
+ if (e.todoId !== id) continue;
4858
+ try {
4859
+ if (e.before === null) {
4860
+ fs.rmSync(e.path, { force: true }); /* madde bu dosyayı YARATMIŞ → sil */
4861
+ } else {
4862
+ fs.mkdirSync(path.dirname(e.path), { recursive: true });
4863
+ fs.writeFileSync(e.path, e.before);
4864
+ }
4865
+ done.push(e.path);
4866
+ j.splice(i, 1);
4867
+ } catch {}
4868
+ }
4869
+ /* madde listede 'pending' kalır — ajan yeniden ele alabilir */
4870
+ const todos = this.todos.get(key) || [];
4871
+ for (const t of todos) {
4872
+ if (t && t.id === id && t.status === 'done') t.status = 'pending';
4873
+ }
4874
+ this.todos.set(key, todos);
4875
+ try {
4876
+ fs.appendFileSync(this._file(key), JSON.stringify({ t: 'todo', items: todos }) + '\n');
4877
+ } catch {}
4878
+ emitSafe(this, key, { type: 'todos', sessionId: key, todos });
4879
+ return {
4880
+ ok: true,
4881
+ todoId: id,
4882
+ reverted: done.length,
4883
+ paths: [...new Set(done)],
4884
+ note: done.length
4885
+ ? 'görev geri alındı — dosyalar önceki kod tabanına döndü'
4886
+ : 'geri alınacak dosya yok',
4887
+ };
4888
+ };
4889
+
4890
+ Engine.prototype.undoLastTodo = function (sid) {
4891
+ this._undoJournal = this._undoJournal || new Map();
4892
+ const j = this._undoJournal.get(String(sid || '')) || [];
4893
+ if (!j.length) return { ok: false, error: 'geri alınacak değişiklik yok' };
4894
+ return this.undoTodo(sid, j[j.length - 1].todoId);
4895
+ };
4896
+
4673
4897
  module.exports = Engine;
4674
4898
  module.exports.Engine = Engine;
4675
4899
  module.exports.sanitizeTodoItems = sanitizeTodoItems;
package/src/agent/llm.js CHANGED
@@ -235,53 +235,76 @@ async function streamOnce(sel, body, { signal, onDelta, onRetry } = {}, omitReas
235
235
  const reader = res.body.getReader();
236
236
  const decoder = new TextDecoder();
237
237
  let buf = '';
238
-
239
- while (true) {
240
- const { done, value } = await reader.read();
241
- if (done) break;
242
- buf += decoder.decode(value, { stream: true });
243
-
244
- let idx;
245
- while ((idx = buf.indexOf('\n')) !== -1) {
246
- const line = buf.slice(0, idx).trim();
247
- buf = buf.slice(idx + 1);
248
- if (!line.startsWith('data:')) continue;
249
- const data = line.slice(5).trim();
250
- if (!data || data === '[DONE]') continue;
251
-
252
- let json;
238
+ /* SERT İPTAL: abort gelirse gövdeyi fiziksel kapat (reader.cancel) —
239
+ sağlayıcı/fetch abort'u gövdeye taşımasa bile okuma döngüsü ANINDA
240
+ biter, token akışı kesilir */
241
+ let onAbort = null;
242
+ if (signal) {
243
+ onAbort = () => {
253
244
  try {
254
- json = JSON.parse(data);
255
- } catch {
256
- continue;
245
+ const p = reader.cancel();
246
+ if (p && typeof p.catch === 'function') p.catch(() => {});
247
+ } catch {}
248
+ };
249
+ signal.addEventListener('abort', onAbort, { once: true });
250
+ }
251
+ try {
252
+ while (true) {
253
+ const { done, value } = await reader.read();
254
+ if (signal && signal.aborted) {
255
+ const e = new Error('iptal');
256
+ e.name = 'AbortError';
257
+ throw e;
257
258
  }
259
+ if (done) break;
260
+ buf += decoder.decode(value, { stream: true });
258
261
 
259
- if (json.usage) state.usage = json.usage;
260
- const ch = json.choices && json.choices[0];
261
- if (!ch) continue;
262
- if (ch.finish_reason) state.finishReason = ch.finish_reason;
263
- const d = ch.delta || {};
264
- if (d.content) {
265
- state.content += d.content;
266
- onDelta && onDelta(d.content, state.content);
267
- }
268
- if (d.reasoning_content) state.reasoning += d.reasoning_content;
269
- if (d.reasoning) state.reasoning += d.reasoning;
270
- for (const tc of d.tool_calls || []) {
271
- const i = typeof tc.index === 'number' ? tc.index : state.toolCalls.length;
272
- while (state.toolCalls.length <= i) {
273
- state.toolCalls.push({ id: '', type: 'function', function: { name: '', arguments: '' } });
262
+ let idx;
263
+ while ((idx = buf.indexOf('\n')) !== -1) {
264
+ const line = buf.slice(0, idx).trim();
265
+ buf = buf.slice(idx + 1);
266
+ if (!line.startsWith('data:')) continue;
267
+ const data = line.slice(5).trim();
268
+ if (!data || data === '[DONE]') continue;
269
+
270
+ let json;
271
+ try {
272
+ json = JSON.parse(data);
273
+ } catch {
274
+ continue;
275
+ }
276
+
277
+ if (json.usage) state.usage = json.usage;
278
+ const ch = json.choices && json.choices[0];
279
+ if (!ch) continue;
280
+ if (ch.finish_reason) state.finishReason = ch.finish_reason;
281
+ const d = ch.delta || {};
282
+ if (d.content) {
283
+ state.content += d.content;
284
+ onDelta && onDelta(d.content, state.content);
274
285
  }
275
- if (tc.id) state.toolCalls[i].id = tc.id;
276
- if (tc.function) {
277
- if (tc.function.name) state.toolCalls[i].function.name += tc.function.name;
278
- if (tc.function.arguments) state.toolCalls[i].function.arguments += tc.function.arguments;
286
+ if (d.reasoning_content) state.reasoning += d.reasoning_content;
287
+ if (d.reasoning) state.reasoning += d.reasoning;
288
+ for (const tc of d.tool_calls || []) {
289
+ const i = typeof tc.index === 'number' ? tc.index : state.toolCalls.length;
290
+ while (state.toolCalls.length <= i) {
291
+ state.toolCalls.push({ id: '', type: 'function', function: { name: '', arguments: '' } });
292
+ }
293
+ if (tc.id) state.toolCalls[i].id = tc.id;
294
+ if (tc.function) {
295
+ if (tc.function.name) state.toolCalls[i].function.name += tc.function.name;
296
+ if (tc.function.arguments) state.toolCalls[i].function.arguments += tc.function.arguments;
297
+ }
279
298
  }
280
299
  }
281
300
  }
282
- }
283
301
 
284
- return { ...state };
302
+ return { ...state };
303
+ } finally {
304
+ if (signal && onAbort) {
305
+ try { signal.removeEventListener('abort', onAbort); } catch {}
306
+ }
307
+ }
285
308
  } catch (e) {
286
309
  /* okuma sırasında bağlantı koptu */
287
310
  const aborted = e && (e.name === 'AbortError' || (signal && signal.aborted));