beast-agent 2.8.3 → 2.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "beast-agent",
3
3
  "productName": "Beast Agent",
4
- "version": "2.8.3",
4
+ "version": "2.10.0",
5
5
  "description": "Ultra-fast local agent shell for Windows.",
6
6
  "author": "algokodcom (AlgoKod)",
7
7
  "license": "MIT",
@@ -334,6 +334,9 @@ class Engine {
334
334
  this._bgGroups = new Map(); // groupId -> {parentId,total,results,dead}
335
335
  /* silinen oturum işaretleri: rapor/zincir buraya gönderilmez (hayalet oturum yok) */
336
336
  this._deletedSessions = new Set();
337
+ /* OPENCODE STEER: koşan tur sırasında gelen mesajlar — güvenli floş
338
+ noktasına kadar burada bekler (sid -> msg[]) */
339
+ this._steerBuf = new Map();
337
340
  /* #17 kalıcı ajan geçmişi: restart sonrası işler + sohbetler kaybolmasın */
338
341
  this._bgJobsFile = path.join(this.sessionsDir, 'bg-jobs.json');
339
342
  this._loadBgJobsPersisted();
@@ -1405,6 +1408,7 @@ class Engine {
1405
1408
  if (cached && cached.code) this._codeIndex.delete(cached.code);
1406
1409
  this.cache.delete(String(id));
1407
1410
  this.todos.delete(String(id));
1411
+ this._steerBuf && this._steerBuf.delete(String(id));
1408
1412
  this.sessionPerm.delete(String(id));
1409
1413
  this.sessionTools.delete(String(id));
1410
1414
  this._errCount && this._errCount.delete(String(id));
@@ -2009,7 +2013,7 @@ class Engine {
2009
2013
  const small = this.modelFor('subagent') || sel || this.sel;
2010
2014
  const res = await chatOnce(
2011
2015
  small,
2012
- { messages: [{ role: 'user', content: prompt }], temperature: 0.2 },
2016
+ { messages: [{ role: 'user', content: prompt }], temperature: 0.2, cacheKey: String(session.id || '') },
2013
2017
  { signal }
2014
2018
  );
2015
2019
  const text = String(res.content || '').replace(/```(?:markdown)?/gi, '').trim();
@@ -2172,7 +2176,7 @@ class Engine {
2172
2176
  `# SOHBET PARÇASI\n${transcript}`;
2173
2177
  const res = await chatOnce(
2174
2178
  this.sel,
2175
- { messages: [{ role: 'user', content: prompt }], temperature: 0.2 },
2179
+ { messages: [{ role: 'user', content: prompt }], temperature: 0.2, cacheKey: String(session.id || '') },
2176
2180
  { signal }
2177
2181
  );
2178
2182
  const txt = String(res.content || '').trim();
@@ -2231,7 +2235,7 @@ class Engine {
2231
2235
  if (this._stopped && !userAction) return false;
2232
2236
  if (userAction) this._stopped = false;
2233
2237
  const s = this._load(String(sessionId));
2234
- if (!s || this.ctrls.has(s.id)) return false;
2238
+ if (!s) return false;
2235
2239
 
2236
2240
  let msg;
2237
2241
  if (typeof payload === 'string') {
@@ -2262,6 +2266,11 @@ class Engine {
2262
2266
  ...imgs.map((im) => ({ type: 'image', name: String(im.name || 'resim') })),
2263
2267
  ...files,
2264
2268
  ];
2269
+ /* MEDYA KAYDI: WA/Chat UI'dan gelen fotoğraflar ve belgeler OTURUMUN
2270
+ media klasörüne yazılır (orijinal dosya güvende). msg.media jsonl'e
2271
+ düşer; prune/compaction görselleri metne indirse bile kayıp yok. */
2272
+ const media = this._saveMedia(s.id, imgs, files);
2273
+ if (media) msg.media = media;
2265
2274
  }
2266
2275
  }
2267
2276
 
@@ -2274,6 +2283,35 @@ class Engine {
2274
2283
  /* Önceki tur yanıtlanmadan kaldıysa (hata/durdurma sonrası artık user mesajı)
2275
2284
  katı sağlayıcılar art arda user mesajını reddeder: "messages illegal" (400).
2276
2285
  Yeni metni o mesajla BİRLEŞTİR — dosyadaki son satır da güncellenir. */
2286
+ /* OPENCODE STEER PORTU (runtime.queue.ts + prompt.ts runLoop): oturum
2287
+ KOŞARKEN gelen mesaj hemen geçmişe yazılmaz — _steerBuf'ta bekler,
2288
+ koşan tur GÜVENLİ floş noktasında (tur başı / çıkış kontrolü) ekler.
2289
+ Böylece araç sonuçlarının arasına sıkışmaz, isteğin yarısındaki mesaj
2290
+ mutasyona uğramaz. UI'ya yine de ANINDA düşer. */
2291
+ if (this.ctrls.has(s.id)) {
2292
+ let buf = this._steerBuf.get(s.id);
2293
+ if (!buf) {
2294
+ buf = [];
2295
+ this._steerBuf.set(s.id, buf);
2296
+ }
2297
+ const lastBuf = buf[buf.length - 1];
2298
+ if (
2299
+ lastBuf && lastBuf.role === 'user' &&
2300
+ !Array.isArray(lastBuf.content) && !Array.isArray(msg.content) &&
2301
+ typeof msg.content === 'string'
2302
+ ) {
2303
+ /* ardışık steer'ler tek mesajda birleşir (art arda user reddi olmasın) */
2304
+ lastBuf.content = lastBuf.content + '\n' + msg.content;
2305
+ if (msg.attachments || lastBuf.attachments) {
2306
+ lastBuf.attachments = [...(lastBuf.attachments || []), ...(msg.attachments || [])];
2307
+ }
2308
+ } else {
2309
+ buf.push(msg);
2310
+ }
2311
+ this.emit({ type: 'message', sessionId: s.id, message: msg });
2312
+ this.emit({ type: 'sessions' });
2313
+ return true;
2314
+ }
2277
2315
  const lastMsg = s.messages[s.messages.length - 1];
2278
2316
  if (
2279
2317
  lastMsg && lastMsg.role === 'user' &&
@@ -2298,6 +2336,57 @@ class Engine {
2298
2336
  return true;
2299
2337
  }
2300
2338
 
2339
+ /* Steer tamponunu güvenli noktada geçmişe akıtır. Dönüş: bir şey akıtıldı mı. */
2340
+ _flushSteer(sid, session, emit) {
2341
+ const buf = this._steerBuf.get(sid);
2342
+ if (!buf || !buf.length) return false;
2343
+ this._steerBuf.delete(sid);
2344
+ for (const m of buf) {
2345
+ session.messages.push(m);
2346
+ try {
2347
+ this._append(session, m);
2348
+ } catch {}
2349
+ }
2350
+ if (emit) emit({ type: 'sessions' });
2351
+ return true;
2352
+ }
2353
+
2354
+ /* MEDYA KAYDI: mesaj eklerindeki fotoğraflar (dataUrl) ve metin belgeleri
2355
+ oturumun media klasörüne yazılır: <sessionsDir>/media/<sid>/zaman-metin.
2356
+ Dönen liste msg.media olarak jsonl'e yazılır — orijinal dosya hiç
2357
+ kaybolmaz (eski görsellerin metne indirilmesinden bağımsız). */
2358
+ _saveMedia(sid, imgs, files) {
2359
+ const out = [];
2360
+ try {
2361
+ const dir = path.join(this.sessionsDir, 'media', String(sid || ''));
2362
+ fs.mkdirSync(dir, { recursive: true });
2363
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
2364
+ const safe = (n, fb) => String(n || fb).replace(/[\\/:*?"<>|]/g, '_').slice(0, 80);
2365
+ const stripExt = (n) => String(n || '').replace(/\.[a-z0-9]{1,5}$/i, '');
2366
+ let i = 0;
2367
+ for (const im of imgs) {
2368
+ const m = /^data:([^;,]+)?(;base64)?,(.*)$/.exec(String(im.dataUrl || ''));
2369
+ if (!m) continue;
2370
+ try {
2371
+ const mime = String(m[1] || 'image/png');
2372
+ const ext = (mime.split('/')[1] || 'png').replace(/[^a-z0-9]/gi, '').slice(0, 5) || 'png';
2373
+ const buf = m[2] ? Buffer.from(m[3], 'base64') : Buffer.from(decodeURIComponent(m[3]), 'utf8');
2374
+ const file = stamp + '-' + ++i + '-' + safe(stripExt(im.name), 'resim') + '.' + ext;
2375
+ fs.writeFileSync(path.join(dir, file), buf);
2376
+ out.push({ file, name: String(im.name || 'resim'), kind: 'image' });
2377
+ } catch {}
2378
+ }
2379
+ for (const f of files) {
2380
+ try {
2381
+ const file = stamp + '-' + ++i + '-' + safe(stripExt(f.name), 'dosya') + '.txt';
2382
+ fs.writeFileSync(path.join(dir, file), String(f.content || ''), 'utf8');
2383
+ out.push({ file, name: String(f.name || 'dosya'), kind: 'file' });
2384
+ } catch {}
2385
+ }
2386
+ } catch {}
2387
+ return out.length ? out : null;
2388
+ }
2389
+
2301
2390
  /* SESSİZ BAĞLAM ENJEKSİYONU: metin oturum geçmişine düşer ama _run()
2302
2391
  TETİKLENMEZ — ajan "görür", cevap ÜRETMEZ. WhatsApp gruplarında
2303
2392
  @mention moduyla kullanılır: grubun tüm konuşması bağlam olarak
@@ -3249,6 +3338,10 @@ class Engine {
3249
3338
  e.name = 'AbortError';
3250
3339
  throw e;
3251
3340
  }
3341
+ /* STEER FLOŞU (tur başı): koşan tur sırasında gelen mesajlar burada
3342
+ geçmişe eklenir — araç sonuçlarının arasına sıkışmaz, sonraki
3343
+ _chatTurn isteğinde model görür */
3344
+ this._flushSteer(sid, session, emit);
3252
3345
  /* SÜRE SINIRI YOK. Ana oturum sınırsız tur koşar (opencode birebir);
3253
3346
  yalnız bg ajan son 3 tura gelirken "raporu yaz ve bitir" uyarısı alır. */
3254
3347
  if (session.bgJob && turn === maxTurns - 3 && !wrapNudged) {
@@ -3278,6 +3371,9 @@ class Engine {
3278
3371
  }
3279
3372
  this._bgTrim(session); // #21 bg geçmişini olabildiğince ince tut
3280
3373
  emit({ type: 'status', status: 'thinking' });
3374
+ /* opencode runLoop portu: istek kurulmadan ÖNCEKİ mesaj sayısı —
3375
+ koşan tur SIRASINDA gelen (steer) user mesajlarını saptar */
3376
+ const preLen = session.messages.length;
3281
3377
  const res = await this._chatTurn(session, ctrl.signal, (delta) =>
3282
3378
  emit({ type: 'token', delta })
3283
3379
  );
@@ -3298,6 +3394,20 @@ class Engine {
3298
3394
  emit({ type: 'message', message: assistant });
3299
3395
 
3300
3396
  if (!res.toolCalls || !res.toolCalls.length) {
3397
+ /* opencode runLoop portu (prompt.ts:1111-1115): koşan tur SIRASINDA
3398
+ yeni user mesajı geldiyse (steer) tur BİTMEZ — tampondaki mesaj
3399
+ geçmişe akıtılır, model sonraki istekte görür ve cevaplar;
3400
+ eski cevap akışı bozulmaz. */
3401
+ if (
3402
+ session.messages.slice(preLen).some((m) => m.role === 'user') ||
3403
+ this._flushSteer(sid, session, emit)
3404
+ ) {
3405
+ /* ARA CEVAP: bu cevap 'done' değil (tur steer'le sürüyor) —
3406
+ bağlı kanallara (WA/TG/DC) tek tek iletilir; SON cevap yine
3407
+ done'da gider (çift yok: bu artık 'son' değil). */
3408
+ emit({ type: 'interim-final', message: assistant });
3409
+ continue;
3410
+ }
3301
3411
  /* GÖREV LİSTESİ DİSİPLİNİ: ajan işi bitti sanıyor ama listede hâlâ
3302
3412
  bekleyen/aktif madde varsa BİR KEZ hatırlat ve tura devam et —
3303
3413
  liste ya tamamlanmalı ya kalan maddeler listeden düşürülmeli. */
@@ -3458,6 +3568,10 @@ class Engine {
3458
3568
  this._bgFinish(sid, 'error', 'bg görevi tur limitine ulaştı (maxTurns)');
3459
3569
  } catch (e) {
3460
3570
  const aborted = e && (e.name === 'AbortError' || ctrl.signal.aborted);
3571
+ /* STEER FLOŞU (hata/durdurma): tur sırasında gelen mesajlar yine de
3572
+ geçmişe yazılsın — konuşma bütünlüğü korunur, sonraki tur bağlamı
3573
+ görür (opencode interrupted-part disiplini) */
3574
+ this._flushSteer(sid, session, emit);
3461
3575
  if (aborted) {
3462
3576
  this._clearCrash(); // kullanıcı durdurdu — kurtarma yok
3463
3577
  /* öz-kurtarma/superyorizon kick'i bekliyorsa bu tur BİTİŞ değildir —
@@ -3571,7 +3685,7 @@ class Engine {
3571
3685
  const llm = async (sys, user) => {
3572
3686
  const res = await chatOnce(
3573
3687
  this.sel,
3574
- { messages: [{ role: 'user', content: sys + '\n\n' + user }], temperature: 0.1 },
3688
+ { messages: [{ role: 'user', content: sys + '\n\n' + user }], temperature: 0.1, cacheKey: String(session.id || '') },
3575
3689
  { signal }
3576
3690
  );
3577
3691
  return String(res.content || '');
@@ -3607,7 +3721,7 @@ class Engine {
3607
3721
 
3608
3722
  const res = await chatOnce(
3609
3723
  this.sel,
3610
- { messages: [{ role: 'user', content: prompt }], temperature: 0.1 },
3724
+ { messages: [{ role: 'user', content: prompt }], temperature: 0.1, cacheKey: String(session.id || '') },
3611
3725
  { signal }
3612
3726
  );
3613
3727
  let parsed = null;
@@ -3661,7 +3775,7 @@ const skills = require('./skills');
3661
3775
 
3662
3776
  const res = await chatOnce(
3663
3777
  this.sel,
3664
- { messages: [{ role: 'user', content: prompt }], temperature: 0.2 },
3778
+ { messages: [{ role: 'user', content: prompt }], temperature: 0.2, cacheKey: String(session.id || '') },
3665
3779
  {}
3666
3780
  );
3667
3781
  const draft = parseReflectionJson(res.content || '');
@@ -3745,7 +3859,7 @@ const skills = require('./skills');
3745
3859
  try {
3746
3860
  const res = await chatOnce(
3747
3861
  this.sel,
3748
- { messages: [{ role: 'user', content: prompt }], temperature: 0.2 },
3862
+ { messages: [{ role: 'user', content: prompt }], temperature: 0.2, cacheKey: 'nightref' },
3749
3863
  { signal: ctrl.signal }
3750
3864
  );
3751
3865
  return String(res.content || '');
package/src/main.js CHANGED
@@ -642,7 +642,7 @@ async function transcribeCloud(provider, buf, mimetype, lang) {
642
642
  if (lang === 'tr' || lang === 'en') form.append('language', lang);
643
643
  const res = await fetch(base + '/audio/transcriptions', {
644
644
  method: 'POST',
645
- headers: { Authorization: 'Bearer ' + key },
645
+ headers: { Authorization: 'Bearer ' + key, 'x-opencode-session': OPENCODE_SESSION },
646
646
  body: form,
647
647
  });
648
648
  if (!res.ok) throw new Error(provider + ' ' + res.status + ': ' + String(await res.text()).slice(0, 180));
@@ -954,7 +954,11 @@ async function synthesizeSpeech(text) {
954
954
  const url = String(cfg.baseUrl).replace(/\/+$/, '') + '/audio/speech';
955
955
  const res = await fetch(url, {
956
956
  method: 'POST',
957
- headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + cfg.key },
957
+ headers: {
958
+ 'Content-Type': 'application/json',
959
+ Authorization: 'Bearer ' + cfg.key,
960
+ 'x-opencode-session': OPENCODE_SESSION,
961
+ },
958
962
  body: JSON.stringify({
959
963
  model: cfg.model || 'tts-1',
960
964
  input: String(text).slice(0, 4000),
@@ -1726,6 +1730,23 @@ function sessionFileAlive(sid) {
1726
1730
 
1727
1731
  /* kanalın yaşayan tek oturumunu döndürür; geçersizse eski bağlamdan devralır,
1728
1732
  o da yoksa (ilk kullanım) yeni açar ve tüm jid'leri ona bağlar */
1733
+ /* WA'dan gelen belge orijinallerini oturumun media klasörüne yazar;
1734
+ döner: media/<sid>/<dosya> (relatif yol — ajan read_file ile açabilir) */
1735
+ function saveSessionMedia(sid, name, buf) {
1736
+ try {
1737
+ if (!buf || !buf.length) return null;
1738
+ const dir = path.join(engine.sessionsDir, 'media', String(sid || ''));
1739
+ fs.mkdirSync(dir, { recursive: true });
1740
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
1741
+ const safe = String(name || 'dosya').replace(/[\\/:*?"<>|]/g, '_').slice(0, 120);
1742
+ const file = stamp + '-' + safe;
1743
+ fs.writeFileSync(path.join(dir, file), buf);
1744
+ return 'media/' + String(sid || '') + '/' + file;
1745
+ } catch {
1746
+ return null;
1747
+ }
1748
+ }
1749
+
1729
1750
  function ensureWaSession(jid) {
1730
1751
  if (!waSingleSid || !sessionFileAlive(waSingleSid)) {
1731
1752
  let best = '';
@@ -2313,17 +2334,10 @@ async function processWaMessage(jid, payload, senderNum, requeues = 0) {
2313
2334
  waLog(`skip flush: izinli eşleşme yok (sender=+${senderNum || '?'})`);
2314
2335
  return;
2315
2336
  }
2337
+ /* OPENCODE STEER: oturum meşgulse mesaj bekleMEZ — engine.send tamponuna
2338
+ ekler, koşan tur sonraki istekte görür ve cevaplar (eski cevap bozulmaz).
2339
+ Sıra korunur: floş katmanı debounceli ve sıralı işler. */
2316
2340
  let sid = waChats.get(jid) || waSingleSid;
2317
- if (sid && engine.isBusy(sid)) {
2318
- /* oturum hâlâ önceki işle uğraşıyor — KAYBETME: sınırı kaldırdık,
2319
- iş bitene dek her debounce turunda yeniden deneriz; kullanıcıya
2320
- "işleyemedim" denmez, mesaj boşa gitmez. */
2321
- if (requeues === 1) {
2322
- waLog(`flush: oturum meşgul, mesaj kuyrukta bekliyor sid=${sid}`);
2323
- }
2324
- await new Promise((r) => setTimeout(r, WA_DEBOUNCE_MS));
2325
- return processWaMessage(jid, payload, senderNum, requeues + 1);
2326
- }
2327
2341
  if (!isGroup && requeues > 0) {
2328
2342
  // retry sonrası izin yeniden kontrol edilir (yukarıda hit zaten alınıyor)
2329
2343
  }
@@ -2429,12 +2443,15 @@ async function processWaMessage(jid, payload, senderNum, requeues = 0) {
2429
2443
  if (inst && inst.error) {
2430
2444
  text += `\n[SKILL.md alındı ama kaydedilemedi]`;
2431
2445
  } else {
2446
+ /* BELGE KAYDI: orijinal dosya oturum media klasörüne yazılır —
2447
+ model yalnız metnini görür, orijinale ajan read_file ile ulaşabilir */
2448
+ const savedDoc = saveSessionMedia(sid, md.name, md.buf);
2432
2449
  const dt = documentToText(md);
2433
2450
  if (dt && dt.trim()) {
2434
2451
  attachments.push({ type: 'file', name: md.name, content: dt.slice(0, 20000) });
2435
- text += `\n[belge alındı: ${md.name}]`;
2452
+ text += `\n[belge alındı: ${md.name}${savedDoc ? ' · kaydedildi: ' + savedDoc : ''}]`;
2436
2453
  } else {
2437
- text += `\n[belge alındı ama okunamadı: ${md.name}]`;
2454
+ text += `\n[belge alındı ama okunamadı: ${md.name}${savedDoc ? ' · kaydedildi: ' + savedDoc : ''}]`;
2438
2455
  }
2439
2456
  }
2440
2457
  }
@@ -2626,12 +2643,9 @@ async function processTgMessage(chatId, payload, requeues = 0) {
2626
2643
  tgLog(`skip flush: izinli eşleşme yok (sender=${payload.senderId || '?'})`);
2627
2644
  return;
2628
2645
  }
2646
+ /* OPENCODE STEER: meşgulse bekleme — engine.send tampona ekler, koşan tur
2647
+ sonraki istekte görür (WA ile aynı mantık) */
2629
2648
  let sid = tgChats.get(chatId) || tgSingleSid;
2630
- if (sid && engine.isBusy(sid)) {
2631
- /* oturum meşgul — WA ile aynı: kaybetme, iş bitene dek yeniden dene */
2632
- await new Promise((r) => setTimeout(r, TG_DEBOUNCE_MS));
2633
- return processTgMessage(chatId, payload, requeues + 1);
2634
- }
2635
2649
  /* KANAL TEK OTURUMU: eskisi silinmedikçe hep aynı oturumdan devam */
2636
2650
  sid = ensureTgSession(chatId);
2637
2651
  /* Kişi bazlı granül izin: all/web/read/chat */
@@ -2840,12 +2854,9 @@ async function processDcMessage(channelId, payload) {
2840
2854
  dcLog(`skip flush: izinli eşleşme yok (sender=${payload.senderId || '?'})`);
2841
2855
  return;
2842
2856
  }
2857
+ /* OPENCODE STEER: meşgulse bekleme — engine.send tampona ekler, koşan tur
2858
+ sonraki istekte görür (WA/TG ile aynı mantık) */
2843
2859
  let sid = dcChats.get(channelId) || dcSingleSid;
2844
- if (sid && engine.isBusy(sid)) {
2845
- /* oturum meşgul — WA/TG ile aynı: kaybetme, iş bitene dek yeniden dene */
2846
- await new Promise((r) => setTimeout(r, DC_DEBOUNCE_MS));
2847
- return processDcMessage(channelId, payload, 1);
2848
- }
2849
2860
  /* KANAL TEK OTURUMU: eskisi silinmedikçe hep aynı oturumdan devam */
2850
2861
  sid = ensureDcSession(channelId);
2851
2862
  /* Kişi bazlı granül izin: all/web/read/chat */
@@ -3091,6 +3102,34 @@ function reloadBackend() {
3091
3102
  })();
3092
3103
  }
3093
3104
  }
3105
+ /* OPENCODE STEER ARA CEVABI: koşan tur sırasında gelen mesajın cevabı.
3106
+ 'done' YOK (tur steer'le sürüyor) — bu yüzden done akışı YAZMAZ.
3107
+ Bağlı kanallara (WA/TG/DC) tek tek gider; SON cevap yine done'da. */
3108
+ if (ev.type === 'interim-final' && ev.message) {
3109
+ const txt = typeof ev.message.content === 'string' ? ev.message.content.trim() : '';
3110
+ if (txt) {
3111
+ (async () => {
3112
+ try {
3113
+ if (wa && wa.connected) {
3114
+ const jid = waReplyJid(ev.sessionId);
3115
+ if (jid) await sendWaSafe(jid, txt);
3116
+ }
3117
+ } catch {}
3118
+ try {
3119
+ if (tg && tg.connected) {
3120
+ const hit = [...tgChats.entries()].find(([, s]) => s === ev.sessionId);
3121
+ if (hit) await sendTgSafe(hit[0], txt);
3122
+ }
3123
+ } catch {}
3124
+ try {
3125
+ if (dc && dc.connected) {
3126
+ const hit = [...dcChats.entries()].find(([, s]) => s === ev.sessionId);
3127
+ if (hit) await sendDcSafe(hit[0], txt);
3128
+ }
3129
+ } catch {}
3130
+ })();
3131
+ }
3132
+ }
3094
3133
  // Telegram oturumlarının son cevabını geri gönder (WA ile aynı akış)
3095
3134
  if ((ev.type === 'done' || ev.type === 'error') && tg && tg.connected) {
3096
3135
  const hitT = [...tgChats.entries()].find(([, s]) => s === ev.sessionId);
@@ -5149,6 +5188,14 @@ function queueDesktopMessage(sessionId, text) {
5149
5188
  chatQueueOfflineAdd(sid, { text: t, attachments: hasAtts ? text.attachments : undefined });
5150
5189
  return;
5151
5190
  }
5191
+ /* OPENCODE STEER: koşan tur varken mesaj ANINDA konuşmaya eklenir —
5192
+ ajan sonraki istekte görür, eski cevap akışı bozulmaz. Kuyrukta beklemesine
5193
+ gerek yok; boşta debounce penceresi yine birleştirir. */
5194
+ if (engine.isBusy(sid)) {
5195
+ waLog(`desktop: oturum meşgul — mesaj steer olarak konuşmaya eklendi sid=${sid}`);
5196
+ engine.send(sid, hasAtts ? { text: t, attachments: text.attachments } : t, { userAction: true });
5197
+ return;
5198
+ }
5152
5199
  let q = desktopQueue.get(sid);
5153
5200
  if (!q) {
5154
5201
  q = { timer: null, msgs: [] };
@@ -5156,16 +5203,9 @@ function queueDesktopMessage(sessionId, text) {
5156
5203
  }
5157
5204
  q.msgs.push({ text: t, attachments: hasAtts ? text.attachments : undefined });
5158
5205
 
5159
- const busy = engine.isBusy(sid);
5160
- if (!busy) {
5161
- /* agent boşta: kısa pencere — hızlı ikinci mesaj ilkine eklenir */
5162
- clearTimeout(q.timer);
5163
- q.timer = setTimeout(() => flushDesktop(sid).catch(() => {}), DESKTOP_DEBOUNCE_MS);
5164
- } else {
5165
- /* agent çalışıyor: pencere yok, iş bitince hepsi birlikte gider.
5166
- Bekleyen paket olduğunda 'done' olayı flushDesktop'u çağırır. */
5167
- waLog(`desktop: oturum meşgul, mesaj beklemede (${q.msgs.length}) sid=${sid}`);
5168
- }
5206
+ /* agent boşta: kısa pencere — hızlı ikinci mesaj ilkine eklenir */
5207
+ clearTimeout(q.timer);
5208
+ q.timer = setTimeout(() => flushDesktop(sid).catch(() => {}), DESKTOP_DEBOUNCE_MS);
5169
5209
  }
5170
5210
 
5171
5211
  async function flushDesktop(sessionId) {
@@ -5402,9 +5402,25 @@ function opText(w) {
5402
5402
  return `${sym} ${w.value ?? '?'}`;
5403
5403
  }
5404
5404
 
5405
- async function renderWatchersModal() {
5405
+ /* kısa saat: bugünse "14:32", başka günse "07.09 14:32" */
5406
+ function fmtWhen(iso) {
5407
+ if (!iso) return '';
5408
+ const d = new Date(iso);
5409
+ if (isNaN(d.getTime())) return '';
5410
+ const t = d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
5411
+ if (d.toDateString() === new Date().toDateString()) return t;
5412
+ return d.toLocaleDateString([], { day: '2-digit', month: '2-digit' }) + ' ' + t;
5413
+ }
5414
+
5415
+ let lastWatchKey = '';
5416
+ async function renderWatchersModal(force) {
5406
5417
  let rows = [];
5407
5418
  try { rows = (await beast.watchersList()) || []; } catch {}
5419
+ /* canlı yenilemede içerik değişmediyse DOM'a dokunma — buton tıklamaları
5420
+ yeniden kurulurken yutulmasın */
5421
+ const key = JSON.stringify(rows);
5422
+ if (!force && key === lastWatchKey) return;
5423
+ lastWatchKey = key;
5408
5424
  els.watchList.innerHTML = '';
5409
5425
  if (!rows.length) {
5410
5426
  els.watchList.innerHTML = '<div class="mini-empty">' + _t('w_empty') + '</div>';
@@ -5443,9 +5459,13 @@ async function renderWatchersModal() {
5443
5459
  }
5444
5460
  }
5445
5461
 
5446
- async function renderCronModal() {
5462
+ let lastCronKey = '';
5463
+ async function renderCronModal(force) {
5447
5464
  let jobs = [];
5448
5465
  try { jobs = (await beast.cronList()) || []; } catch {}
5466
+ const key = JSON.stringify(jobs);
5467
+ if (!force && key === lastCronKey) return;
5468
+ lastCronKey = key;
5449
5469
  els.cronModalList.innerHTML = '';
5450
5470
  if (!jobs.length) {
5451
5471
  els.cronModalList.innerHTML = '<div class="mini-empty">' + _t('cr_modal_empty') + '</div>';
@@ -5454,11 +5474,15 @@ async function renderCronModal() {
5454
5474
  for (const j of jobs) {
5455
5475
  const row = document.createElement('div');
5456
5476
  row.className = 'mini-row' + (j.enabled ? '' : ' off');
5477
+ /* ANLIK DURUM: sıradaki tetiklenme + son çalışma (varsa) */
5478
+ const live =
5479
+ (j.enabled && j.nextRunAt ? ` · sonraki <b>${escapeHtml(fmtWhen(j.nextRunAt))}</b>` : '') +
5480
+ (j.lastRunAt ? ` · son ${escapeHtml(fmtWhen(j.lastRunAt))}` : '');
5457
5481
  row.innerHTML =
5458
5482
  `<span class="mr-dot"></span>` +
5459
5483
  `<div class="mr-main">` +
5460
5484
  `<div class="mr-name">${escapeHtml(j.name || j.id)}</div>` +
5461
- `<div class="mr-meta"><b>${escapeHtml(j.schedule)}</b> · ${escapeHtml(String(j.prompt || '').slice(0, 70))}</div>` +
5485
+ `<div class="mr-meta"><b>${escapeHtml(j.schedule)}</b> · ${escapeHtml(String(j.prompt || '').slice(0, 70))}${live}</div>` +
5462
5486
  `</div>`;
5463
5487
  const btns = document.createElement('div');
5464
5488
  btns.style.cssText = 'display:flex;gap:6px;flex:none';
@@ -5488,14 +5512,41 @@ function toggleMini(overlay, hide) {
5488
5512
  overlay.hidden = !!hide;
5489
5513
  }
5490
5514
 
5515
+ /* CANLI YENİLEME: mini modal açık kaldığı sürece periyodik taze çekim yapar;
5516
+ render fonksiyonları içerik değişmemişse DOM'u yeniden kurmaz. Kapalıysa
5517
+ zamanlayıcı kendini durdurur. */
5518
+ let miniRefreshTimer = null;
5519
+ function startMiniRefresh() {
5520
+ stopMiniRefresh();
5521
+ miniRefreshTimer = setInterval(async () => {
5522
+ if (els.watchOverlay && !els.watchOverlay.hidden) await renderWatchersModal();
5523
+ else if (els.cronOverlay && !els.cronOverlay.hidden) await renderCronModal();
5524
+ else stopMiniRefresh();
5525
+ }, 4000);
5526
+ }
5527
+ function stopMiniRefresh() {
5528
+ if (miniRefreshTimer) { clearInterval(miniRefreshTimer); miniRefreshTimer = null; }
5529
+ }
5530
+
5531
+ function closeWatchModal() {
5532
+ toggleMini(els.watchOverlay, true);
5533
+ stopMiniRefresh();
5534
+ }
5535
+ function closeCronModal() {
5536
+ toggleMini(els.cronOverlay, true);
5537
+ stopMiniRefresh();
5538
+ }
5539
+
5491
5540
  async function openWatchModal() {
5492
5541
  toggleMini(els.watchOverlay, false);
5493
- await renderWatchersModal();
5542
+ await renderWatchersModal(true); /* her basışta TAZE çekim */
5543
+ startMiniRefresh();
5494
5544
  }
5495
5545
 
5496
5546
  async function openCronModal() {
5497
5547
  toggleMini(els.cronOverlay, false);
5498
- await renderCronModal();
5548
+ await renderCronModal(true); /* her basışta TAZE çekim */
5549
+ startMiniRefresh();
5499
5550
  }
5500
5551
 
5501
5552
  /* ---------------- state / controls ---------------- */
@@ -5671,8 +5722,9 @@ async function sendCurrent() {
5671
5722
  if (!busy) {
5672
5723
  setBusy(true);
5673
5724
  } else {
5674
- /* agent çalışıyor — mesaj kuyruğa alınır, iş bitince otomatik gönderilir */
5675
- setStatus('kuyruğa eklendi — iş bitince gider');
5725
+ /* agent çalışıyor — mesaj ANINDA konuşmaya eklenir (opencode steer):
5726
+ koşan tur sonraki istekte görür, eski cevap akışı bozulmaz */
5727
+ setStatus('mesaj ajanın akışına eklendi — sıradaki adımda değerlendirir');
5676
5728
  }
5677
5729
  beast.send(activeId, { text, attachments: atts });
5678
5730
  }
@@ -6203,13 +6255,13 @@ async function init() {
6203
6255
  /* cron modülü — olay bağlama */
6204
6256
  if (els.watchBtn) els.watchBtn.addEventListener('click', openWatchModal);
6205
6257
  if (els.cronBtn) els.cronBtn.addEventListener('click', openCronModal);
6206
- if (els.watchClose) els.watchClose.addEventListener('click', () => toggleMini(els.watchOverlay, true));
6207
- if (els.cronClose) els.cronClose.addEventListener('click', () => toggleMini(els.cronOverlay, true));
6258
+ if (els.watchClose) els.watchClose.addEventListener('click', closeWatchModal);
6259
+ if (els.cronClose) els.cronClose.addEventListener('click', closeCronModal);
6208
6260
  if (els.watchOverlay) els.watchOverlay.addEventListener('click', (e) => {
6209
- if (e.target === els.watchOverlay) toggleMini(els.watchOverlay, true);
6261
+ if (e.target === els.watchOverlay) closeWatchModal();
6210
6262
  });
6211
6263
  if (els.cronOverlay) els.cronOverlay.addEventListener('click', (e) => {
6212
- if (e.target === els.cronOverlay) toggleMini(els.cronOverlay, true);
6264
+ if (e.target === els.cronOverlay) closeCronModal();
6213
6265
  });
6214
6266
  els.cronPreset.addEventListener('change', () => {
6215
6267
  if (els.cronPreset.value !== '__custom') els.cronSchedule.value = els.cronPreset.value;
@@ -6294,8 +6346,8 @@ async function init() {
6294
6346
  document.addEventListener('keydown', (e) => {
6295
6347
  if (e.key === 'Escape') {
6296
6348
  if ($('#botOverlay') && !$('#botOverlay').hidden) { botOverlaySetOpen(false); return; }
6297
- if (els.watchOverlay && !els.watchOverlay.hidden) { toggleMini(els.watchOverlay, true); return; }
6298
- if (els.cronOverlay && !els.cronOverlay.hidden) { toggleMini(els.cronOverlay, true); return; }
6349
+ if (els.watchOverlay && !els.watchOverlay.hidden) { closeWatchModal(); return; }
6350
+ if (els.cronOverlay && !els.cronOverlay.hidden) { closeCronModal(); return; }
6299
6351
  if (!els.settingsOverlay.hidden) closeSettings();
6300
6352
  }
6301
6353
  // Ctrl+Shift+S: ekran görüntüsü al → vision'a gönderilmek üzere ekle