beast-agent 0.27.0 → 0.29.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": "0.27.0",
4
+ "version": "0.29.0",
5
5
  "description": "Ultra-fast local agent shell for Windows.",
6
6
  "author": "algokodcom (AlgoKod)",
7
7
  "license": "MIT",
package/src/agent/bots.js CHANGED
@@ -199,6 +199,8 @@ function add({ name, icon, prompt }) {
199
199
  loadRegistry();
200
200
  const n = String(name || '').trim().slice(0, 40);
201
201
  if (!n) return { ok: false, error: 'bot adı zorunlu' };
202
+ /* İLK HARF ZORUNLU: bot adı harf karakteriyle başlamalı */
203
+ if (!/^\p{L}/u.test(n)) return { ok: false, error: 'bot adı harf ile başlamalı — ilk karakter harf olmalı' };
202
204
  if (REG.bots.length >= MAX_BOTS) return { ok: false, error: `en fazla ${MAX_BOTS} bot olabilir (1 admin + ${MAX_BOTS - 1} müşteri)` };
203
205
  let id = slugify(n);
204
206
  while (get(id)) id = slugify(n) + '-' + uid().slice(-4);
@@ -261,6 +263,12 @@ function update(id, patch) {
261
263
  loadRegistry();
262
264
  const b = get(id);
263
265
  if (!b) return { ok: false, error: 'bot yok' };
266
+ /* İLK HARF ZORUNLU: ad değişikliği harf karakteriyle başlamalı —
267
+ diğer alanlara dokunmadan reddet */
268
+ if (patch && typeof patch.name === 'string' && patch.name.trim() &&
269
+ !/^\p{L}/u.test(patch.name.trim())) {
270
+ return { ok: false, error: 'bot adı harf ile başlamalı — ilk karakter harf olmalı' };
271
+ }
264
272
  const changes = [];
265
273
  if (patch && typeof patch === 'object') {
266
274
  if (typeof patch.name === 'string' && patch.name.trim() && patch.name.trim() !== b.name) {
@@ -630,10 +630,13 @@ class Engine {
630
630
  {
631
631
  signal,
632
632
  onDelta,
633
- onRetry: (attempt) =>
633
+ onRetry: (attempt, st) =>
634
634
  emitSafe(this, session.id, {
635
635
  type: 'status',
636
- status: `sağlayıcı yanıtsız — tekrar deniyor (${attempt}/${3})`,
636
+ status:
637
+ st === undefined || st === null
638
+ ? `bağlantı sorunu — internet dönünce ya da yeniden denemeyle sürer (${attempt}/6)`
639
+ : `sağlayıcı yanıtsız — tekrar deniyor (${attempt}/3)`,
637
640
  }),
638
641
  }
639
642
  );
@@ -1173,6 +1176,8 @@ class Engine {
1173
1176
  try {
1174
1177
  const c = this.ctrls.get(id);
1175
1178
  if (c) {
1179
+ this._abortReasons = this._abortReasons || new Map();
1180
+ this._abortReasons.set(String(id), 'oturum silindiği için tur iptal edildi');
1176
1181
  c.abort();
1177
1182
  this.ctrls.delete(id);
1178
1183
  }
@@ -1182,6 +1187,7 @@ class Engine {
1182
1187
  const j = this._bgJobs.get(String(id));
1183
1188
  j.revive = false;
1184
1189
  j.status = 'aborted';
1190
+ j.error = j.error || 'oturum silindi — paralel ajan iptal edildi';
1185
1191
  this._clearKicks(id);
1186
1192
  }
1187
1193
  fs.unlinkSync(this._file(String(id)));
@@ -1316,7 +1322,7 @@ class Engine {
1316
1322
  'Tarayıcı eylemlerinin yanıtındaki recent günlüğü ve navigated bilgisini takip et; eylem yanıtları zaten taze snapshot içerir — refler tutarsız görünürse yeni snapshot al.',
1317
1323
  'CONUŞMA ODAĞI SENDE KALSIN: kullanıcı seninle konuşurken iş çıkmışsa — uzun da olsa UFACIK da (tek komutluk dizin listesi, tek dosya okuma, tek arama…) — run_background ile PARALEL ajana devret; ana sohbet hiçbir işi beklemez; bittiğinde özet otomatik düşer.',
1318
1324
  'Python işleri için python_run kullan: küçük betikler inline code ile; tekrarlayan işler %APPDATA%\\beast\\scripts klasöründeki dosyalarla (ör. news.py = RSS haber toplayıcı: args ["--limit","8","--json"]). Python kurulu olmasa bile ilk çağrıda taşınabilir gömülü runtime otomatik iner.',
1319
- 'PYTHON DURUMU: makinede sistem Python\'u görünmese bile ŞAŞIRMA ve "python yok" DEME — python_run aracı kendi taşınabilir runtime\'ını (%APPDATA%\\beast\\py\\python.exe) otomatik indirir/kullanır ve bu klasör run_command PATH\'inde önceliklidir; yani run_command içinde de `python` çalışır. Ham Google/Bing scrape yerine önce web_search aracını kullan (zaten Python çoklu-motor + Exa destekli), script gerektiğinde python_run yaz.',
1325
+ 'PYTHON DURUMU: makinede sistem Python\'u görünmese bile ŞAŞIRMA ve "python yok" DEME — python_run aracı kendi taşınabilir runtime\'ını (%APPDATA%\\beast\\py\\python.exe) otomatik indirir/kullanır ve bu klasör run_command PATH\'inde önceliklidir; yani run_command içinde de `python` çalışır. Ham Google/Bing scrape yerine önce web_search aracını kullan (zaten Obscura stealth tarayıcı + TinyFish + Python çoklu-motor destekli), script gerektiğinde python_run yaz.',
1320
1326
  'PDF ÇIKTI KURALI: PDF üretirken pip\u2019ten pdf paketi (fpdf, fpdf2, markdown-pdf, weasyprint, reportlab vb.) KURMA/KULLANMA — bunlar Türkçe karakterleri bozar. Doğru kit Node tarafında ZATEN kurulu: `pdf-lib` + `@pdf-lib/fontkit` (Türkçe font gömme) ve `pdfkit`. python_run ile DEĞİL; write_file ile .js script yazıp run_command ile `node script.js` çalıştır. md→pdf çevirici YOKTUR ve kurulmaz: kullanıcıya rapor/özet/belge çıktısı vereceksen .md dosyası gönderme — aynı içeriği DOĞRUDAN pdf-lib/pdfkit ile PDF olarak üret ve send_file ile o PDF\u2019i gönder. Ayrıntılı örnekler: pdf skill\u2019i (SKILL.md).',
1321
1327
  'Eski bir hafıza kaydına ihtiyacın olursa memory_search ile ara; kalıcı bilgi/birikim için kb_search kullan, yeni bilgi öğrenirsen kb_add ile kaynak belirt.',
1322
1328
  'Bir oturumda 3+ kez memory_write yaptıysan iş bitince memory_hygiene çağır (duplike/eskime temizliği).',
@@ -1335,7 +1341,8 @@ class Engine {
1335
1341
  'Görev tanımı kendi başına YETERLİ olsun: tüm bağlam, dosya/URL yolları, kısıtlar ve beklenen çıktı görev metnine yazılır — paralel ajan senin bağlamını göremez.',
1336
1342
  'Birden fazla bağımsız iş varsa hepsini aynı anda ayrı run_background çağrılarıyla devret (paralel çalışsınlar).',
1337
1343
  'HIZ FAN-OUT: aynı işin farklı adımlarını run_background_many ile TEK SEFERDE paralel aç (örnek: haber taraması → TR / Dünya / Ekonomi ayrı ajan; araştırma+veri çekme+analiz ayrı ajan). Hepsi bitince sana TEK birleşik rapor düşer — hız dramatik artar. Bağımsız 2+ adım görürsen bunu tercih et.',
1338
- 'Takip araçları: tasks_list (genel tablo), task_status (tek ajanın canlı dökümü), task_cancel (iptal). Kullanıcı ilerleme sorarsa buradan raporla.',
1344
+ 'Takip araçları: tasks_list (genel tablo), task_status (tek ajanın canlı dökümü), task_cancel (iptal — reason ZORUNLU). Kullanıcı ilerleme sorarsa buradan raporla.',
1345
+ 'İPTAL DİSİPLİNİ: bir paralel ajanı task_cancel ile iptal edersen ya da bir işi yarıda bırakırsan, kullanıcıya TEK CÜMLEyle MUTLAKA SEBEBİNİ yaz (neden iptal ettin, ne kayboldu, yerine ne yapacaksın). Sebepsiz iptal YASAK.',
1339
1346
  'Bilgi sorularını, sohbeti, planlamayı, hafıza/hatırlatıcı/izleyici kurulumunu YİNE SEN yaparsın — bunlar "iş" değildir.',
1340
1347
  ].join('\n')
1341
1348
  );
@@ -1847,7 +1854,7 @@ class Engine {
1847
1854
  if (j.status === 'running' || j.status === 'queued') {
1848
1855
  j.status = 'aborted';
1849
1856
  j.endedAt = new Date().toISOString();
1850
- j.error = String(j.error || '') || 'uygulama kapatıldı';
1857
+ j.error = String(j.error || '') || 'uygulama kapatıldı — paralel ajan yarıda kesildi';
1851
1858
  migrated = true;
1852
1859
  }
1853
1860
  j._persisted = true;
@@ -1940,9 +1947,18 @@ class Engine {
1940
1947
  this._pendingReports = this._pendingReports || [];
1941
1948
  this._pendingReports.push({ parentId: job.parentId, text });
1942
1949
  this.flushPendingReports(job.parentId);
1943
- } else if (status === 'aborted' && grouped) {
1944
- /* iptal edilen üye gruptan düşmüş sayılır */
1945
- this._bgGroupRecord(job.title, 'aborted', '');
1950
+ } else if (status === 'aborted') {
1951
+ /* İPTAL SEBEBİ ZORUNLU: sebebi kayda yaz ve üst sohbete/gruba bildir */
1952
+ const why = String(errorMsg || job.error || 'sebep belirtilmedi').slice(0, 300);
1953
+ if (grouped) {
1954
+ /* iptal edilen üye gruptan düşmüş sayılır — birleşik raporda SEBEPİYLE görünür */
1955
+ this._bgGroupRecord(job.title, 'aborted', 'İPTAL: ' + why);
1956
+ } else {
1957
+ const text = `[ARKA PLAN İPTAL: ${job.title}]\nSebep: ${why}`;
1958
+ this._pendingReports = this._pendingReports || [];
1959
+ this._pendingReports.push({ parentId: job.parentId, text });
1960
+ this.flushPendingReports(job.parentId);
1961
+ }
1946
1962
  }
1947
1963
  }
1948
1964
 
@@ -2432,8 +2448,21 @@ class Engine {
2432
2448
  const aborted = e && (e.name === 'AbortError' || ctrl.signal.aborted);
2433
2449
  if (aborted) {
2434
2450
  this._clearCrash(); // kullanıcı durdurdu — kurtarma yok
2435
- emit({ type: 'done', aborted: true });
2436
- this._bgFinish(sid, 'aborted');
2451
+ /* öz-kurtarma/superyorizon kick'i bekliyorsa bu tur BİTİŞ değildir —
2452
+ işi aborted KAPATMA, kick mesajı yeni turu açar */
2453
+ const kickJob = this._bgJobs && this._bgJobs.get(sid);
2454
+ if (kickJob && kickJob.revive === true) {
2455
+ emit({ type: 'done', aborted: true });
2456
+ } else {
2457
+ /* İPTAL SEBEBİ ZORUNLU: interrupt() tarafından kaydedilir */
2458
+ const why =
2459
+ (this._abortReasons && this._abortReasons.get(sid)) ||
2460
+ (kickJob && kickJob.error) ||
2461
+ 'kullanıcı talebiyle iptal edildi';
2462
+ if (this._abortReasons) this._abortReasons.delete(sid);
2463
+ emit({ type: 'done', aborted: true, reason: why });
2464
+ this._bgFinish(sid, 'aborted', why);
2465
+ }
2437
2466
  } else {
2438
2467
  // FALLOUT: tüm zincir tükendi — durumu kaydet, açılışta kaldığı yerden devam edilir
2439
2468
  this._saveCrash(sid, e, 'chain-exhausted');
@@ -2676,7 +2705,7 @@ class Engine {
2676
2705
  if (!this.ctrls.has(pairId)) break;
2677
2706
  }
2678
2707
  if (this.ctrls.has(pairId)) {
2679
- try { this.interrupt(pairId); } catch {}
2708
+ try { this.interrupt(pairId, 'bot DM zaman aşımına uğradı (120 sn) — tur kesildi'); } catch {}
2680
2709
  return { ok: false, error: 'hedef bot zaman aşımına uğradı (120 sn)' };
2681
2710
  }
2682
2711
  const after = this.cache.get(pairId) || pair;
@@ -2790,39 +2819,19 @@ class Engine {
2790
2819
  }
2791
2820
  }
2792
2821
 
2793
- /* web_search zinciri — web_search VE deep_search aynı zinciri kullanır:
2794
- 1) dahili tarayıcı (DİREK GOOGLE ücretsiz + AI cevabı)
2795
- 2) TinyFish (tarayıcı sorun çıkarsa HEMEN devreye girer — anahtar girildiyse)
2796
- 3) python çoklu-motor (ddgs / DDG+Bing+Mojeek)
2797
- 4) Exa (anahtar girildiyse, son çare)
2798
- Tarayıcıda CAPTCHA/"olağandışı trafik"/boş sonuç algılanırsa 10 dk boyunca
2799
- tarayıcı ATLANIR — her arama TinyFish'ten başlar, süre dolunca tarayıcı geri döner. */
2800
- async _webSearchChain(q, n, sessionId, signal) {
2801
- let r = null;
2802
- let banned = false;
2822
+ /* web_search zinciri — web_search VE deep_search aynı zinciri kullanır.
2823
+ Sıra + aç/kapa Ayarlar Web Arama'dan değiştirilir (tools.searchChainWeb):
2824
+ varsayılan: 1) dahili tarayıcı (DİREK GOOGLE) 2) Obscura (stealth headless
2825
+ DuckDuckGo; otomatik kurulur, varsayılan AKTİF) 3) TinyFish (anahtar
2826
+ girildiyse) 4) python çoklu-motor. Tarayıcı CAPTCHA/trafik verirse 10 dk
2827
+ atlanır sıradaki motor hemen devreye girer. */
2828
+ _webSearchChain(q, n, sessionId, signal) {
2803
2829
  const browserUsable = this.browser && typeof this.browser.search === 'function';
2804
- if (Date.now() < (this._webSearchBrowserBanUntil || 0)) banned = true;
2805
- if (browserUsable && !banned) {
2806
- try { r = await this.browser.search(q, signal, { sessionId }); } catch {}
2807
- if (!r || !r.ok || !(r.results || []).length) {
2808
- /* tarayıcı sorunlu — 10 dk atla, alternatif zincir hemen devrede */
2809
- this._webSearchBrowserBanUntil = Date.now() + 10 * 60 * 1000;
2810
- r = null;
2811
- banned = true;
2812
- }
2813
- }
2814
- if (!r || !r.ok || !(r.results || []).length) {
2815
- try { r = await tools.tinyfishSearch(q, n, signal); } catch {}
2816
- }
2817
- if (!r || !r.ok || !(r.results || []).length) {
2818
- try {
2819
- r = JSON.parse(await tools.exec('web_search', { query: q, max_results: n }, { cwd: this._sessionWorkspace(sessionId), signal }));
2820
- } catch {}
2821
- }
2822
- const out = r || { ok: false, error: 'web arama başarısız — tüm motorlar boş döndü' };
2823
- if (out && out.ok && banned) out.note = 'tarayıcı araması 10 dk askıda (CAPTCHA/trafik) — TinyFish/alternatif zincir kullanıldı';
2824
- return out;
2825
- }
2830
+ return tools.searchChainWeb(q, n, {
2831
+ signal,
2832
+ browser: browserUsable ? () => this.browser.search(q, signal, { sessionId }) : null,
2833
+ });
2834
+ }
2826
2835
 
2827
2836
  /* Dahili OCR aracı: görüntüden metin çıkarır (görsel desteklemeyen modeller için).
2828
2837
  Kaynak: 'browser' (dahili tarayıcı görüntüsü, varsayılan) | 'screen' (masaüstü) | dosya yolu. */
@@ -3051,8 +3060,11 @@ class Engine {
3051
3060
  }
3052
3061
  if (name === 'task_cancel') {
3053
3062
  const id = String(args.id || '');
3054
- const ok = this.interrupt(id);
3055
- return JSON.stringify(ok ? { ok: true, id } : { ok: false, error: 'çalışan ajan yok: ' + id });
3063
+ const why = String(args.reason || '').trim() || 'ana ajan (CEO) bu ajanı task_cancel ile iptal etti';
3064
+ const ok = this.interrupt(id, why);
3065
+ return JSON.stringify(ok
3066
+ ? { ok: true, id, reason: why, note: 'Ajan iptal edildi — kullanıcıya MUTLAKA iptal sebebini tek cümleyle açıkla: ' + why }
3067
+ : { ok: false, error: 'çalışan ajan yok: ' + id });
3056
3068
  }
3057
3069
  if (name === 'set_reminder') {
3058
3070
  if (!this.reminders || typeof this.reminders.add !== 'function') {
@@ -3174,10 +3186,8 @@ class Engine {
3174
3186
  }
3175
3187
  if (name === 'deep_search') {
3176
3188
  /* agentic derin araştırma: çoklu sorgu + gizli tarayıcıda sayfa okuma.
3177
- ARAMA zinciri web_search ile BİREBİR AYNI — eski sürüm yalnız python
3178
- motorlarını (ddgs/DDG/Bing) kullanıyordu; CAPTCHA/engel sonucu motorlar
3179
- boş dönünce "tüm motorlar boş döndü" hatası veriyordu. Artık önce
3180
- DAHİLİ TARAYICI (Google) denenir, sonra TinyFish → python → Exa. */
3189
+ ARAMA zinciri web_search ile BİREBİR AYNI — sıralı zincir
3190
+ (tarayıcı → Obscura → TinyFish → python; Ayarlar'dan değiştirilir). */
3181
3191
  try {
3182
3192
  emitSafe(this, sessionId, { type: 'status', status: 'derin araştırma: çoklu sorgu + gizli sayfa okuma' });
3183
3193
  } catch {}
@@ -3242,9 +3252,34 @@ class Engine {
3242
3252
  }
3243
3253
  }
3244
3254
 
3245
- interrupt(sessionId) {
3246
- const c = this.ctrls.get(String(sessionId));
3247
- if (!c) return false;
3255
+ /* İptal SEBEBİ zorunlu: abort edilen her tur/_bgFinish bu haritadan sebebini
3256
+ okur; ajan neden durdurulduğunu kullanıcıya MUTLAKA bildirir. */
3257
+ _abortReason(reason) {
3258
+ const r = String(reason || '').trim();
3259
+ return (r || 'sebep belirtilmedi — iptal talebi').slice(0, 400);
3260
+ }
3261
+
3262
+ interrupt(sessionId, reason) {
3263
+ const sid = String(sessionId || '');
3264
+ const why = this._abortReason(reason);
3265
+ const c = this.ctrls.get(sid);
3266
+ if (!c) {
3267
+ /* koşan tur yok — kuyrukta bekleyen / öz-kurtarma kick'i bekleyen
3268
+ paralel ajanı da SEBEPİYLE kes */
3269
+ const j = this._bgJobs && this._bgJobs.get(sid);
3270
+ if (j && (j.status === 'queued' || j.revive === true)) {
3271
+ j.status = 'aborted';
3272
+ j.revive = false;
3273
+ j.endedAt = nowIso();
3274
+ j.error = why;
3275
+ this._clearKicks(sid);
3276
+ this._bgEmit();
3277
+ return true;
3278
+ }
3279
+ return false;
3280
+ }
3281
+ this._abortReasons = this._abortReasons || new Map();
3282
+ this._abortReasons.set(sid, why);
3248
3283
  c.abort();
3249
3284
  return true;
3250
3285
  }
@@ -3254,19 +3289,22 @@ class Engine {
3254
3289
  main katmanında BAĞIMSIZ yaşar — /stop onlara dokunmaz. */
3255
3290
  stopAll() {
3256
3291
  let aborted = 0;
3292
+ const why = '/stop: kullanıcı tüm ajanları ve turları durdurdu';
3257
3293
  /* fan-out gruplarını kapat — yarım grup artık birleşik rapor beklemesin */
3258
3294
  if (this._bgGroups) {
3259
3295
  for (const g of this._bgGroups.values()) g.dead = true;
3260
3296
  }
3261
3297
  if (this._bgPendingStart) this._bgPendingStart.clear();
3298
+ this._abortReasons = this._abortReasons || new Map();
3262
3299
  for (const [sid, c] of this.ctrls) {
3263
- try { c.abort(); aborted++; } catch {}
3300
+ try { this._abortReasons.set(sid, why); c.abort(); aborted++; } catch {}
3264
3301
  /* abort sonrası _run finally bloğu temizler; paralel ajan kaydını biz işaretleyelim */
3265
3302
  if (this._bgJobs.has(sid)) {
3266
3303
  const job = this._bgJobs.get(sid);
3267
3304
  if (job.status === 'running' || job.status === 'queued') {
3268
3305
  job.status = 'aborted';
3269
3306
  job.endedAt = nowIso();
3307
+ job.error = job.error || why;
3270
3308
  }
3271
3309
  job.revive = false; // bekleyen öz-kurtarma gönderimini de öldür
3272
3310
  this._clearKicks(sid);
@@ -3277,6 +3315,7 @@ class Engine {
3277
3315
  if (j.status === 'queued') {
3278
3316
  j.status = 'aborted';
3279
3317
  j.endedAt = nowIso();
3318
+ j.error = j.error || why;
3280
3319
  aborted++;
3281
3320
  }
3282
3321
  }
@@ -3648,13 +3687,15 @@ const TOOLS = [
3648
3687
  type: 'function',
3649
3688
  function: {
3650
3689
  name: 'task_cancel',
3651
- description: 'Cancel a running background agent by id (from tasks_list).',
3690
+ description:
3691
+ 'Cancel a running background agent by id (from tasks_list). ALWAYS pass reason: a short sentence explaining WHY you are cancelling — it is recorded on the job and you must report it to the user.',
3652
3692
  parameters: {
3653
3693
  type: 'object',
3654
3694
  properties: {
3655
3695
  id: { type: 'string' },
3696
+ reason: { type: 'string', description: 'REQUIRED: why you are cancelling this agent (one short sentence)' },
3656
3697
  },
3657
- required: ['id'],
3698
+ required: ['id', 'reason'],
3658
3699
  },
3659
3700
  },
3660
3701
  },
package/src/agent/llm.js CHANGED
@@ -1,10 +1,51 @@
1
1
  'use strict';
2
2
 
3
3
  /* OpenAI-compatible streaming chat client with SSE parsing.
4
- Geçici sağlayıcı hatalarında (5xx/429/ağ) üstel beklemeyle otomatik retry. */
4
+ Geçici sağlayıcı hatalarında (5xx/429) üstel beklemeyle retry; İNTERNET
5
+ KOPMASINDA (fetch failed) çok daha sabırlı: 6 deneme + internet dönene
6
+ dek bekleme (main'deki net izleyicisinden beslenir). Akış ortasında kopan
7
+ bağlantıda: hiç veri akmadıysa sessiz baştan dener, kısmi metin geldiyse
8
+ 'length' gibi döner → chatStreamAuto kaldığı yerden DEVAM ETTİRİR. */
5
9
 
6
10
  const RETRYABLE_STATUS = new Set([408, 409, 425, 429, 500, 502, 503, 504]);
7
- const MAX_RETRIES = 3;
11
+ const MAX_RETRIES = 3; /* sağlayıcı geçici HTTP hataları */
12
+ const NET_MAX_RETRIES = 6; /* bağlantı kopması — daha sabırlı */
13
+ const NET_BACKOFF = [1000, 2000, 4000, 8000, 15000, 25000]; /* ≈55 sn toplam */
14
+ const NET_WAIT_CAP_MS = 120000; /* internet dönene dek bekleme üst sınırı */
15
+
16
+ /* main process'teki net izleyicisi besler: () => boolean | Promise<boolean> */
17
+ let _netProbe = null;
18
+ function setNetProbe(fn) {
19
+ _netProbe = typeof fn === 'function' ? fn : null;
20
+ }
21
+
22
+ /* fetch failed / ENOTFOUND / ECONNRESET / socket hang up / terminated —
23
+ HTTP yanıtı ALINAMADI demektir (status yok) */
24
+ function isNetworkError(e) {
25
+ if (!e) return false;
26
+ if (e.name === 'AbortError') return false;
27
+ if (e.status !== undefined) return false;
28
+ return true;
29
+ }
30
+
31
+ /* internet yoksa dönene kadar bekle (en fazla NET_WAIT_CAP_MS); probe yoksa
32
+ ya da durum bilinmiyorsa hemen geç */
33
+ async function waitForNet(signal) {
34
+ if (!_netProbe) return true;
35
+ const t0 = Date.now();
36
+ for (;;) {
37
+ if (signal && signal.aborted) {
38
+ const e = new Error('iptal');
39
+ e.name = 'AbortError';
40
+ throw e;
41
+ }
42
+ let online;
43
+ try { online = await _netProbe(); } catch { online = null; }
44
+ if (online !== false) return true; /* false DEĞİLSE devam (bilinmiyor = iyimser) */
45
+ if (Date.now() - t0 > NET_WAIT_CAP_MS) return false;
46
+ await sleep(2000, signal);
47
+ }
48
+ }
8
49
 
9
50
  const HINTS = {
10
51
  401: 'API anahtarı geçersiz',
@@ -47,8 +88,9 @@ function sleep(ms, signal) {
47
88
  });
48
89
  }
49
90
 
50
- /* fn: denenecek istek. hataları ve RETRYABLE_STATUS durumlarında
51
- 0.8s 1.6s 3.2s bekleyip tekrar dener. */
91
+ /* fn: denenecek istek. Sağlayıcı geçici HTTP hatalarında 0.8s→1.6s→3.2s;
92
+ İNTERNET KOPMASINDA (status yok) 6 denemeye kadar 1s→2s→4s→8s→15s→25s ve
93
+ net izleyicisi "çevrimdışı" diyorsa internet dönene kadar bekler. */
52
94
  async function withRetries(fn, { signal, onRetry } = {}) {
53
95
  let attempt = 0;
54
96
  for (;;) {
@@ -57,14 +99,23 @@ async function withRetries(fn, { signal, onRetry } = {}) {
57
99
  } catch (e) {
58
100
  const aborted = e && (e.name === 'AbortError' || (signal && signal.aborted));
59
101
  const status = e && e.status;
60
- const retriable =
61
- !aborted && (status === undefined || RETRYABLE_STATUS.has(status));
62
- if (!retriable || attempt >= MAX_RETRIES) throw e;
102
+ const netErr = !aborted && status === undefined;
103
+ const retriable = !aborted && (status === undefined || RETRYABLE_STATUS.has(status));
104
+ if (!retriable) throw e;
105
+ const cap = netErr ? NET_MAX_RETRIES : MAX_RETRIES;
106
+ if (attempt >= cap) throw e;
107
+ if (netErr && _netProbe) {
108
+ /* internet kopmuş: dönene kadar bekle — kısa kopmalarda görev ölmez */
109
+ await waitForNet(signal);
110
+ }
63
111
  attempt++;
64
112
  try {
65
113
  onRetry && onRetry(attempt, status);
66
114
  } catch {}
67
- await sleep(800 * Math.pow(2, attempt - 1), signal);
115
+ const wait = netErr
116
+ ? NET_BACKOFF[Math.min(attempt - 1, NET_BACKOFF.length - 1)]
117
+ : 800 * Math.pow(2, attempt - 1);
118
+ await sleep(wait, signal);
68
119
  }
69
120
  }
70
121
  }
@@ -113,79 +164,101 @@ async function chatStream(sel, body, { signal, onDelta, onRetry } = {}) {
113
164
  }
114
165
 
115
166
  async function streamOnce(sel, body, { signal, onDelta, onRetry } = {}, omitReasoning = false) {
116
- const res = await withRetries(
117
- async () => {
118
- const r = await openChat(sel, body, { stream: true, signal, omitReasoning });
119
- if (!r.ok) {
120
- let detail = '';
121
- try {
122
- detail = (await r.text()).slice(0, 300);
123
- } catch {}
124
- const err = new Error(friendlyError(r.status, r.statusText, detail));
125
- err.status = r.status;
126
- throw err;
127
- }
128
- return r;
129
- },
130
- { signal, onRetry }
131
- );
167
+ /* akış durumu dışarıda tutulur: gövde okunurken bağlantı koparsa
168
+ ne kadarı geldiğine bakılır (kısmi metin → devam; boş/araç → baştan) */
169
+ const state = { content: '', reasoning: '', toolCalls: [], usage: null, finishReason: null };
170
+ let dropAttempt = 0;
171
+
172
+ for (;;) {
173
+ const res = await withRetries(
174
+ async () => {
175
+ const r = await openChat(sel, body, { stream: true, signal, omitReasoning });
176
+ if (!r.ok) {
177
+ let detail = '';
178
+ try {
179
+ detail = (await r.text()).slice(0, 300);
180
+ } catch {}
181
+ const err = new Error(friendlyError(r.status, r.statusText, detail));
182
+ err.status = r.status;
183
+ throw err;
184
+ }
185
+ return r;
186
+ },
187
+ { signal, onRetry }
188
+ );
132
189
 
133
- let content = '';
134
- let reasoning = '';
135
- const toolCalls = [];
136
- let usage = null;
137
- let finishReason = null;
190
+ try {
191
+ const reader = res.body.getReader();
192
+ const decoder = new TextDecoder();
193
+ let buf = '';
138
194
 
139
- const reader = res.body.getReader();
140
- const decoder = new TextDecoder();
141
- let buf = '';
195
+ while (true) {
196
+ const { done, value } = await reader.read();
197
+ if (done) break;
198
+ buf += decoder.decode(value, { stream: true });
142
199
 
143
- while (true) {
144
- const { done, value } = await reader.read();
145
- if (done) break;
146
- buf += decoder.decode(value, { stream: true });
200
+ let idx;
201
+ while ((idx = buf.indexOf('\n')) !== -1) {
202
+ const line = buf.slice(0, idx).trim();
203
+ buf = buf.slice(idx + 1);
204
+ if (!line.startsWith('data:')) continue;
205
+ const data = line.slice(5).trim();
206
+ if (!data || data === '[DONE]') continue;
147
207
 
148
- let idx;
149
- while ((idx = buf.indexOf('\n')) !== -1) {
150
- const line = buf.slice(0, idx).trim();
151
- buf = buf.slice(idx + 1);
152
- if (!line.startsWith('data:')) continue;
153
- const data = line.slice(5).trim();
154
- if (!data || data === '[DONE]') continue;
208
+ let json;
209
+ try {
210
+ json = JSON.parse(data);
211
+ } catch {
212
+ continue;
213
+ }
155
214
 
156
- let json;
157
- try {
158
- json = JSON.parse(data);
159
- } catch {
160
- continue;
215
+ if (json.usage) state.usage = json.usage;
216
+ const ch = json.choices && json.choices[0];
217
+ if (!ch) continue;
218
+ if (ch.finish_reason) state.finishReason = ch.finish_reason;
219
+ const d = ch.delta || {};
220
+ if (d.content) {
221
+ state.content += d.content;
222
+ onDelta && onDelta(d.content, state.content);
223
+ }
224
+ if (d.reasoning_content) state.reasoning += d.reasoning_content;
225
+ if (d.reasoning) state.reasoning += d.reasoning;
226
+ for (const tc of d.tool_calls || []) {
227
+ const i = typeof tc.index === 'number' ? tc.index : state.toolCalls.length;
228
+ while (state.toolCalls.length <= i) {
229
+ state.toolCalls.push({ id: '', type: 'function', function: { name: '', arguments: '' } });
230
+ }
231
+ if (tc.id) state.toolCalls[i].id = tc.id;
232
+ if (tc.function) {
233
+ if (tc.function.name) state.toolCalls[i].function.name += tc.function.name;
234
+ if (tc.function.arguments) state.toolCalls[i].function.arguments += tc.function.arguments;
235
+ }
236
+ }
237
+ }
161
238
  }
162
239
 
163
- if (json.usage) usage = json.usage;
164
- const ch = json.choices && json.choices[0];
165
- if (!ch) continue;
166
- if (ch.finish_reason) finishReason = ch.finish_reason;
167
- const d = ch.delta || {};
168
- if (d.content) {
169
- content += d.content;
170
- onDelta && onDelta(d.content, content);
171
- }
172
- if (d.reasoning_content) reasoning += d.reasoning_content;
173
- if (d.reasoning) reasoning += d.reasoning;
174
- for (const tc of d.tool_calls || []) {
175
- const i = typeof tc.index === 'number' ? tc.index : toolCalls.length;
176
- while (toolCalls.length <= i) {
177
- toolCalls.push({ id: '', type: 'function', function: { name: '', arguments: '' } });
178
- }
179
- if (tc.id) toolCalls[i].id = tc.id;
180
- if (tc.function) {
181
- if (tc.function.name) toolCalls[i].function.name += tc.function.name;
182
- if (tc.function.arguments) toolCalls[i].function.arguments += tc.function.arguments;
240
+ return { ...state };
241
+ } catch (e) {
242
+ /* okuma sırasında bağlantı koptu */
243
+ const aborted = e && (e.name === 'AbortError' || (signal && signal.aborted));
244
+ if (!aborted && isNetworkError(e)) {
245
+ if (state.content && !state.toolCalls.length) {
246
+ /* kısmi metin geldi, araç çağrısı yok → hata balonu YOK:
247
+ 'length' gibi dön, chatStreamAuto CONTINUE_PROMPT ile kaldığı
248
+ yerden sürdürür ve metni birleştirir */
249
+ return { ...state, finishReason: 'length' };
183
250
  }
251
+ /* hiç veri akmadı YA DA araç çağrısı yarım kaldı → BAŞTAN dene */
252
+ if (dropAttempt >= NET_MAX_RETRIES) throw e;
253
+ dropAttempt++;
254
+ try { onRetry && onRetry(dropAttempt, undefined); } catch {}
255
+ if (_netProbe) await waitForNet(signal);
256
+ await sleep(NET_BACKOFF[Math.min(dropAttempt - 1, NET_BACKOFF.length - 1)], signal);
257
+ continue; /* yeni stream — UI'daki kısmi token'lar nihai mesajla ezilir */
184
258
  }
259
+ throw e;
185
260
  }
186
261
  }
187
-
188
- return { content, reasoning, toolCalls, usage, finishReason };
189
262
  }
190
263
 
191
264
  /* usage toplama: devam turları gerçek faturalamayı yansıtsın (prompt yeniden sayılır) */
@@ -261,4 +334,13 @@ async function chatOnce(sel, body, { signal, onRetry } = {}) {
261
334
  };
262
335
  }
263
336
 
264
- module.exports = { chatStream, chatStreamAuto, chatOnce, withRetries, friendlyError, sumUsage };
337
+ module.exports = {
338
+ chatStream,
339
+ chatStreamAuto,
340
+ chatOnce,
341
+ withRetries,
342
+ friendlyError,
343
+ sumUsage,
344
+ setNetProbe,
345
+ isNetworkError,
346
+ };