beast-agent 2.34.0 → 2.35.1

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.34.0",
4
+ "version": "2.35.1",
5
5
  "description": "Ultra-fast local agent shell for Windows.",
6
6
  "author": "algokodcom (AlgoKod)",
7
7
  "license": "MIT",
@@ -25,6 +25,7 @@ const mcp = require('./mcp');
25
25
  const apps = require('./apps');
26
26
  const pdfwrite = require('./pdfwrite');
27
27
  const { estTokens, estMsgTokens } = require('./tokens');
28
+ const squeeze = require('./squeeze');
28
29
  const log = require('./logger');
29
30
  /* opencode mantığı (birebir port): ajanlar, izinler, tool seti, promptlar —
30
31
  Beast Code (bcCode) oturumları bu çekirdek üzerinden koşar */
@@ -767,6 +768,12 @@ class Engine {
767
768
  413/TPM "Requested N" hatası → gerçek token sayısı öğrenilir, tahmin
768
769
  kalibre edilir ve aynı model BİR KEZ daha sıkı sıkıştırmayla denenir. */
769
770
  async _streamWithFallbacks(session, payload, activeTools, signal, onDelta, sel, wasVision) {
771
+ /* SQUEEZE (varsayılan kapalı): istek kopyası sıkıştırılır — oturum kaydı
772
+ değişmez. Provider limiti kontrolünden ÖNCE uygulanır ki limit hesabı
773
+ küçülmüş gövdeyi görsün. */
774
+ if (squeeze.isEnabled()) {
775
+ payload = squeeze.apply(payload, { cacheKey: String(session.id || '') });
776
+ }
770
777
  const cands = this._chatCandidates(sel, wasVision);
771
778
  let lastErr = null;
772
779
  for (let i = 0; i < cands.length; i++) {
@@ -4773,10 +4780,14 @@ const skills = require('./skills');
4773
4780
  ];
4774
4781
  try {
4775
4782
  for (let turn = 0; turn < SUB_MAX_TURNS; turn++) {
4783
+ let sendMsgs = [{ role: 'system', content: system }, ...msgs.slice(1)];
4784
+ if (squeeze.isEnabled()) {
4785
+ sendMsgs = squeeze.apply(sendMsgs, { cacheKey: String(sessionId || 'subagent') });
4786
+ }
4776
4787
  const res = await chatStreamAuto(
4777
4788
  sel,
4778
4789
  {
4779
- messages: [{ role: 'system', content: system }, ...msgs.slice(1)],
4790
+ messages: sendMsgs,
4780
4791
  tools: subTools,
4781
4792
  reasoningEffort: this._thinkEffort(),
4782
4793
  cacheKey: String(sessionId || 'subagent'),
package/src/agent/llm.js CHANGED
@@ -347,18 +347,6 @@ function sessionHeaderId(body) {
347
347
  return _sessionFallback;
348
348
  }
349
349
 
350
- /* Headroom proxy yönlendirme: providerId -> yerel proxy portu haritası.
351
- main (agent/headroom.js) ayarlardan açıldığında kurar; null = düz bağlan. */
352
- let _headroomRoutes = null;
353
- function setHeadroomRoutes(map) {
354
- _headroomRoutes = map && Object.keys(map).length ? map : null;
355
- }
356
- function headroomUrlFor(sel) {
357
- if (!_headroomRoutes || !sel) return '';
358
- const port = Number(_headroomRoutes[String(sel.providerId || '')] || 0);
359
- return port ? 'http://127.0.0.1:' + port + '/v1/chat/completions' : '';
360
- }
361
-
362
350
  async function openChat(sel, body, { stream, signal, drops } = {}) {
363
351
  const d = drops || {};
364
352
  const payload = {
@@ -380,9 +368,7 @@ async function openChat(sel, body, { stream, signal, drops } = {}) {
380
368
  if (body.cacheKey && wantsCacheKey(sel) && !d.noCacheKey) {
381
369
  payload.prompt_cache_key = String(body.cacheKey);
382
370
  }
383
- /* Headroom açıkken istek yerel sıkıştırma proxy'sinden geçer (aynı gövde+başlıklar);
384
- yoksa sağlayıcıya düz bağlan */
385
- const target = headroomUrlFor(sel) || sel.url;
371
+ const target = sel.url;
386
372
  return fetch(target, {
387
373
  method: 'POST',
388
374
  signal,
@@ -641,7 +627,6 @@ module.exports = {
641
627
  capsFor,
642
628
  resetCaps,
643
629
  learnCaps,
644
- setHeadroomRoutes,
645
630
  PARAM_LABELS,
646
631
  isNetworkError,
647
632
  wantsCacheKey,
@@ -0,0 +1,312 @@
1
+ 'use strict';
2
+
3
+ /* Beast SQUEEZE — kendi bağlam (token) sıkıştırıcımız. VARSAYILAN KAPALI.
4
+ Her LLM isteğinden hemen önce mesaj dizisinin KOPYASINA uygulanır; oturum
5
+ kaydı/geçmiş DEĞİŞMEZ. Harici kurulum/proxy yok.
6
+
7
+ Kurallar:
8
+ 1) TEKRAR BLOK: Aynı büyük içerik (araç çıktısı / dosya eki / skill gövdesi)
9
+ aynı bağlamda birden çok kez varsa YALNIZ İLK kopya tam gider; sonrakiler
10
+ kısa işaretçiye iner. İlk kopya bağlamdan düşerse (compaction/bütçe) sonraki
11
+ kopya kendiliğinden yeniden TAM gönderilir — "bağlam koptuysa yeniden" kuralı.
12
+ 2) ESKİ ARAÇ ÇIKTISI: Son N araç sonucu birebir; daha eskiler başlık + baş/son
13
+ önizleme + "gerekirse tekrar çalıştır/oku" notu olarak sıkıştırılır.
14
+ Her dosyanın EN SON read_file sonucu asla sıkıştırılmaz (dosya hafızası).
15
+ 3) SKILL: Aynı skill gövdesi tekrarlıysa eski kopyalar stub'a iner; tek kopya tam.
16
+ 4) DOKUNULMAZLAR: system, son tur birimleri, assistant mesajları, zaten
17
+ sıkıştırılmış içerik (idempotans) ve minBlockChars'tan küçük bloklar. */
18
+
19
+ const crypto = require('crypto');
20
+ const { estMsgTokens } = require('./tokens');
21
+
22
+ const DEFAULTS = {
23
+ minTotalTokens: 1500,
24
+ minBlockChars: 700,
25
+ minUserCompactChars: 12000,
26
+ keepRecentTools: 0,
27
+ keepRecentUnits: 4,
28
+ protectReadsWindow: 8,
29
+ previewHead: 700,
30
+ previewTail: 250,
31
+ };
32
+
33
+ const MARK = '[squeeze:';
34
+
35
+ const state = {
36
+ enabled: false,
37
+ stats: {
38
+ calls: 0,
39
+ changedCalls: 0,
40
+ savedTokens: 0,
41
+ lastBeforeTokens: 0,
42
+ lastAfterTokens: 0,
43
+ lastSavedTokens: 0,
44
+ deduped: 0,
45
+ compactedTools: 0,
46
+ compactedSkills: 0,
47
+ compactedUsers: 0,
48
+ },
49
+ };
50
+
51
+ function setEnabled(on) {
52
+ state.enabled = !!on;
53
+ }
54
+
55
+ function isEnabled() {
56
+ return state.enabled;
57
+ }
58
+
59
+ function resetStats() {
60
+ for (const k of Object.keys(state.stats)) state.stats[k] = 0;
61
+ }
62
+
63
+ function getStats() {
64
+ const s = { ...state.stats };
65
+ s.lastPercent =
66
+ s.lastBeforeTokens > 0 ? Math.round((s.lastSavedTokens / s.lastBeforeTokens) * 100) : 0;
67
+ return s;
68
+ }
69
+
70
+ function _hash(s) {
71
+ try {
72
+ return crypto.createHash('sha1').update(s).digest('hex').slice(0, 16);
73
+ } catch {
74
+ return String(s.length) + ':' + String(s.charCodeAt(0) || 0);
75
+ }
76
+ }
77
+
78
+ function _isSqueezed(content) {
79
+ return typeof content === 'string' && content.includes(MARK);
80
+ }
81
+
82
+ /* assistant(tool_calls) + ardışık tool sonuçlarını BÖLMEYEN birimler (indis) */
83
+ function _indexUnits(messages) {
84
+ const units = [];
85
+ let i = 0;
86
+ while (i < messages.length) {
87
+ const m = messages[i];
88
+ if (m && m.role === 'assistant' && m.tool_calls && m.tool_calls.length) {
89
+ const grp = [i];
90
+ i++;
91
+ while (i < messages.length && messages[i] && messages[i].role === 'tool') grp.push(i++);
92
+ units.push(grp);
93
+ } else {
94
+ units.push([i]);
95
+ i++;
96
+ }
97
+ }
98
+ return units;
99
+ }
100
+
101
+ /* tool_call_id → {name, args} (assistant mesajlarındaki çağrılardan) */
102
+ function _toolCallMap(messages) {
103
+ const map = new Map();
104
+ for (const m of messages) {
105
+ if (!m || m.role !== 'assistant' || !Array.isArray(m.tool_calls)) continue;
106
+ for (const tc of m.tool_calls) {
107
+ if (!tc || !tc.id) continue;
108
+ let args = {};
109
+ try {
110
+ args = JSON.parse((tc.function && tc.function.arguments) || '{}');
111
+ } catch {}
112
+ map.set(String(tc.id), {
113
+ name: String((tc.function && tc.function.name) || ''),
114
+ args,
115
+ });
116
+ }
117
+ }
118
+ return map;
119
+ }
120
+
121
+ function _readTarget(args) {
122
+ if (!args || typeof args !== 'object') return '';
123
+ for (const k of ['file_path', 'path', 'file', 'filename', 'filePath', 'target']) {
124
+ if (args[k]) return String(args[k]);
125
+ }
126
+ return '';
127
+ }
128
+
129
+ function _pointerFor(role, name) {
130
+ if (role === 'tool') {
131
+ return (
132
+ MARK + ' bu araç çıktısı yukarıdaki' + (name ? " '" + name + "'" : '') +
133
+ ' sonucuyla BİREBİR AYNI — tekrar gönderilmedi]'
134
+ );
135
+ }
136
+ return MARK + ' bu içerik yukarıda TAM gönderildi — tekrar gönderilmedi]';
137
+ }
138
+
139
+ function _skillStub(raw) {
140
+ let name = '';
141
+ try {
142
+ const j = JSON.parse(raw);
143
+ name = String(j.name || j.skill || '');
144
+ } catch {}
145
+ return (
146
+ '{"ok":true,"name":"' + name.replace(/"/g, '') + '",' +
147
+ '"content":"' + MARK + ' bu skill gövdesi yukarıda tam yüklendi — tekrar gönderilmedi]"}'
148
+ );
149
+ }
150
+
151
+ function _compactPreview(content, cfg, label, hint) {
152
+ const len = content.length;
153
+ const headLen = Math.min(cfg.previewHead, Math.max(260, Math.floor(len * 0.4)));
154
+ const tailLen = len - headLen > 400 ? Math.min(cfg.previewTail, Math.floor(len * 0.15)) : 0;
155
+ const head = content.slice(0, headLen);
156
+ const tail = tailLen > 0 ? content.slice(-tailLen) : '';
157
+ const note = '\n' + MARK + ' ' + label + ' sıkıştırıldı: ' + len + ' karakter → önizleme. ' + hint + ']\n';
158
+ return tail ? head + note + tail : head + note;
159
+ }
160
+
161
+ /* messages: OpenAI-uyumlu mesaj dizisi. Dönüş: (gerekirse) yeni dizi.
162
+ Kapalıyken aynı referans döner; açıkken de değişiklik yoksa orijinal döner. */
163
+ function apply(messages, opts = {}) {
164
+ if (!state.enabled || !Array.isArray(messages) || messages.length < 2) return messages;
165
+
166
+ const cfg = { ...DEFAULTS, ...(opts.config || {}) };
167
+ const beforeTokens = messages.reduce((a, m) => a + estMsgTokens(m), 0);
168
+ state.stats.calls++;
169
+ state.stats.lastBeforeTokens = beforeTokens;
170
+ state.stats.lastAfterTokens = beforeTokens;
171
+ state.stats.lastSavedTokens = 0;
172
+ if (beforeTokens < cfg.minTotalTokens) return messages;
173
+
174
+ const out = messages.slice();
175
+ let touched = false;
176
+ const replace = (i, msg) => {
177
+ out[i] = msg;
178
+ touched = true;
179
+ };
180
+
181
+ const units = _indexUnits(messages);
182
+ const protectFrom = Math.max(1, units.length - cfg.keepRecentUnits);
183
+ const protectedIdx = new Set([0]);
184
+ for (let u = protectFrom; u < units.length; u++) {
185
+ for (const i of units[u]) protectedIdx.add(i);
186
+ }
187
+ /* son N araç sonucu birebir kalır */
188
+ const toolIdx = [];
189
+ for (let i = 0; i < messages.length; i++) {
190
+ if (messages[i] && messages[i].role === 'tool') toolIdx.push(i);
191
+ }
192
+ if (cfg.keepRecentTools > 0) {
193
+ for (const i of toolIdx.slice(-cfg.keepRecentTools)) protectedIdx.add(i);
194
+ }
195
+
196
+ const meta = _toolCallMap(messages);
197
+ const toolName = (i) => {
198
+ const m = messages[i];
199
+ const fromCall = m && m.tool_call_id ? meta.get(String(m.tool_call_id)) : null;
200
+ return String((m && m.name) || (fromCall && fromCall.name) || '');
201
+ };
202
+
203
+ /* her dosyanın EN SON okuması, yakın penceredeyse korunur */
204
+ const protectReadsFrom =
205
+ toolIdx.length > cfg.protectReadsWindow
206
+ ? toolIdx[toolIdx.length - cfg.protectReadsWindow]
207
+ : (toolIdx[0] ?? Infinity);
208
+ const latestRead = new Map();
209
+ for (const i of toolIdx) {
210
+ const m = messages[i];
211
+ if (!m || String(m.name || '') !== 'read_file') continue;
212
+ const info = m.tool_call_id ? meta.get(String(m.tool_call_id)) : null;
213
+ const target = _readTarget(info && info.args) || '__last__';
214
+ latestRead.set(target, i);
215
+ }
216
+
217
+ let deduped = 0;
218
+ let compactedTools = 0;
219
+ let compactedSkills = 0;
220
+ let compactedUsers = 0;
221
+
222
+ /* 1) SKILL gövde tekrarı: aynı skill'in eski kopyaları stub'a iner */
223
+ const skillSeen = new Set();
224
+ for (let i = 0; i < messages.length; i++) {
225
+ const m = messages[i];
226
+ if (!m || m.role !== 'tool' || toolName(i) !== 'skill') continue;
227
+ if (typeof m.content !== 'string' || _isSqueezed(m.content)) continue;
228
+ let key = '';
229
+ try {
230
+ const j = JSON.parse(m.content);
231
+ key = String(j.name || j.skill || '');
232
+ } catch {}
233
+ if (!key) key = _hash(m.content);
234
+ if (skillSeen.has(key)) {
235
+ replace(i, { ...m, content: _skillStub(m.content) });
236
+ compactedSkills++;
237
+ } else {
238
+ skillSeen.add(key);
239
+ }
240
+ }
241
+
242
+ /* 2) GENEL tekrar bloğu: ilk kopya tam, sonrakiler işaretçi (korumalılar hariç) */
243
+ const seen = new Map();
244
+ for (let i = 0; i < messages.length; i++) {
245
+ const m = messages[i];
246
+ if (!m || m.role === 'system' || m.role === 'assistant') continue;
247
+ if (typeof m.content !== 'string') continue;
248
+ if (m.content.length < cfg.minBlockChars || _isSqueezed(m.content)) continue;
249
+ const role = m.role === 'tool' ? 'tool' : 'user';
250
+ const key = role + ':' + _hash(m.content);
251
+ if (seen.has(key)) {
252
+ /* birebir aynı içerik: en son mesaj (güncel tur) hariç her yerde
253
+ işaretçiye inebilir — içerik zaten aynı bağlamda tam duruyor */
254
+ if (i === messages.length - 1) continue;
255
+ replace(i, { ...out[i], content: _pointerFor(role, toolName(i)) });
256
+ deduped++;
257
+ } else {
258
+ seen.set(key, i);
259
+ }
260
+ }
261
+
262
+ /* 3) ESKİ ARAÇ ÇIKTISI: başlık + baş/son önizleme */
263
+ for (const i of toolIdx) {
264
+ if (protectedIdx.has(i)) continue;
265
+ const m = out[i];
266
+ if (!m || typeof m.content !== 'string' || _isSqueezed(m.content)) continue;
267
+ if (m.content.length < cfg.minBlockChars) continue;
268
+ const name = toolName(i);
269
+ if (name === 'skill') continue;
270
+ if (name === 'read_file') {
271
+ const info = m.tool_call_id ? meta.get(String(m.tool_call_id)) : null;
272
+ const target = _readTarget(info && info.args) || '__last__';
273
+ if (latestRead.get(target) === i && i >= protectReadsFrom) continue;
274
+ }
275
+ const hint =
276
+ name === 'read_file'
277
+ ? 'Tam içerik diskte duruyor; gerekirse read_file ile tekrar oku'
278
+ : 'Gerekirse ' + (name || 'araç') + ' ile tekrar çalıştır';
279
+ replace(i, { ...m, content: _compactPreview(m.content, cfg, "'" + (name || 'araç') + "'", hint) });
280
+ compactedTools++;
281
+ }
282
+
283
+ /* 4) ESKİ BÜYÜK KULLANICI İÇERİĞİ (dosya eki vb.): baş/son önizleme */
284
+ for (let i = 0; i < messages.length; i++) {
285
+ const m = messages[i];
286
+ if (!m || m.role !== 'user' || protectedIdx.has(i) || i === 0) continue;
287
+ if (typeof m.content !== 'string' || _isSqueezed(m.content)) continue;
288
+ if (m.content.length < cfg.minUserCompactChars) continue;
289
+ if (m.content.startsWith('[ÖNCEKİ KONUŞMANIN ÖZETİ')) continue;
290
+ replace(i, {
291
+ ...m,
292
+ content: _compactPreview(m.content, cfg, 'kullanıcı içeriği', 'Tam içerik oturum kaydında duruyor'),
293
+ });
294
+ compactedUsers++;
295
+ }
296
+
297
+ if (!touched) return messages;
298
+
299
+ const afterTokens = out.reduce((a, m) => a + estMsgTokens(m), 0);
300
+ const saved = Math.max(0, beforeTokens - afterTokens);
301
+ state.stats.lastAfterTokens = afterTokens;
302
+ state.stats.lastSavedTokens = saved;
303
+ state.stats.savedTokens += saved;
304
+ state.stats.deduped += deduped;
305
+ state.stats.compactedTools += compactedTools;
306
+ state.stats.compactedSkills += compactedSkills;
307
+ state.stats.compactedUsers += compactedUsers;
308
+ if (saved > 0) state.stats.changedCalls++;
309
+ return out;
310
+ }
311
+
312
+ module.exports = { apply, setEnabled, isEnabled, getStats, resetStats, MARK };
package/src/main.js CHANGED
@@ -28,7 +28,7 @@ const bus = require('./agent/bus');
28
28
  const computeruse = require('./agent/computeruse');
29
29
  const fsguard = require('./agent/fsguard');
30
30
  const log = require('./agent/logger');
31
- const headroom = require('./agent/headroom');
31
+ const squeeze = require('./agent/squeeze');
32
32
  const QRCode = require('qrcode'); /* Expo Go QR (bc-expurl) — whatsapp ile aynı paket */
33
33
 
34
34
  /* Renderer'a sır gönderirken kullanılan maske; kaydederken aynen geri gelirse
@@ -317,6 +317,7 @@ let waHistory = new Map(); // jid -> [sid,...] bu sohbete ait tüm oturumlar
317
317
  let waBcMode = new Set(); // jid -> BeastCode modu AKTİF (WhatsApp'tan uzaktan kodlama)
318
318
  let waJidPn = new Map(); // jid -> gerçek telefon numarası (LID fallback için)
319
319
  let waLastActiveJid = ''; // en son mesaj gelen WA sohbeti — kanal tek oturumunda cevap/dosya hedefi
320
+ let desktopActiveSid = ''; // masaüstü UI'da AÇIK olan oturum — proaktif not buraya işlenir
320
321
  const WA_HISTORY_CAP = 20;
321
322
  let tg = null;
322
323
  let tgChats = new Map(); // telegram chatId -> aktif session id (hepsi KANAL TEK OTURUMUNA bağlı)
@@ -3465,29 +3466,9 @@ function reloadBackend() {
3465
3466
  süreç koşucusu (sandbox:run IPC ile aynı makine). */
3466
3467
  engine.sbRunHook = sbRunStartManaged;
3467
3468
 
3468
- /* HEADROOM (opsiyonel token sıkıştırma proxy'si): engine zincirinden provider
3469
- listesi alınır; durum değişince renderer'a 'headroom-status' düşer */
3470
- headroom.setProviderGetter(() => {
3471
- const seen = new Map();
3472
- for (const c of (engine && engine.cfg && engine.cfg.chain) || []) {
3473
- if (!c || !c.providerId || !c.url || seen.has(c.providerId)) continue;
3474
- seen.set(c.providerId, {
3475
- id: String(c.providerId),
3476
- name: String(c.providerName || c.providerId),
3477
- baseUrl: String(c.url).replace(/\/chat\/completions$/, ''),
3478
- });
3479
- }
3480
- return [...seen.values()];
3481
- });
3482
- headroom.setStatusSink((st) => {
3483
- try {
3484
- if (win && !win.isDestroyed()) win.webContents.send('agent:event', { type: 'headroom-status', status: st });
3485
- } catch {}
3486
- });
3487
- /* ayarlardan açık kaldıysa: uygulama açılışında arka planda başlat */
3488
- if (settings.headroom && settings.headroom.enabled) {
3489
- setTimeout(() => { headroom.setEnabled(true).catch(() => {}); }, 4000);
3490
- }
3469
+ /* SQUEEZE (opsiyonel token sıkıştırma): varsayılan KAPALI; ayar açıksa
3470
+ her LLM isteğinin kopyası gönderimden önce sıkıştırılır. */
3471
+ squeeze.setEnabled(!!(settings.squeeze && settings.squeeze.enabled));
3491
3472
  return engine.publicState();
3492
3473
  }
3493
3474
 
@@ -3718,7 +3699,6 @@ app.whenReady().then(() => {
3718
3699
  flushBrowserStorage(); // x.com/google oturumları (cookies) diske yazılsın
3719
3700
  try { toolsMod.disposeShellSessions(); } catch {} // kalıcı shell oturumlarını kapat
3720
3701
  try { require('./agent/mcp').stopAll(); } catch {} // MCP server süreçlerini kapat
3721
- try { headroom.stopAll(); } catch {} // headroom proxy süreçlerini kapat
3722
3702
  });
3723
3703
 
3724
3704
  if (process.argv.includes('--smoke')) {
@@ -5102,11 +5082,34 @@ ipcMain.handle('sessions:create', () => {
5102
5082
  return v;
5103
5083
  });
5104
5084
  ipcMain.handle('sessions:open', (_e, id) => engine.openSession(id));
5105
- ipcMain.handle('sessions:delete', (_e, id) => engine.deleteSession(id));
5085
+ ipcMain.handle('sessions:delete', (_e, id) => {
5086
+ const sid = String(id || '');
5087
+ if (sid && sid === desktopActiveSid) desktopActiveSid = '';
5088
+ return engine.deleteSession(id);
5089
+ });
5090
+ /* masaüstü UI hangi sohbeti AÇIK tutuyorsa main'e bildirir: empati loop
5091
+ proaktif notu rastgele bir oturuma değil, kullanıcının gördüğü sohbete
5092
+ işler; meşgulse enjeksiyon kuyruğa girer, tur bitince geçmişe düşer */
5093
+ ipcMain.handle('sessions:active', (_e, id) => {
5094
+ const sid = String(id || '');
5095
+ if (!sid) {
5096
+ desktopActiveSid = '';
5097
+ return true;
5098
+ }
5099
+ try {
5100
+ if (sessionFileAlive(sid)) desktopActiveSid = sid;
5101
+ } catch {}
5102
+ return true;
5103
+ });
5106
5104
 
5107
5105
  ipcMain.handle('agent:send', (_e, { sessionId, text }) => {
5108
5106
  const raw = text && typeof text === 'object' ? String(text.text || '') : String(text ?? '');
5109
5107
  const t = raw.trim();
5108
+ /* yazan sohbet = kullanıcının gördüğü sohbet: proaktif enjeksiyon hedefi */
5109
+ try {
5110
+ const asid = String(sessionId || '');
5111
+ if (asid && sessionFileAlive(asid)) desktopActiveSid = asid;
5112
+ } catch {}
5110
5113
  /* BOT HAFIZA GARANTİSİ: aktif bot bir MÜŞTERİ botuysa, botId'siz (eskiden
5111
5114
  kalma) oturumlar bu bota bağlanır — bot konuşması asla Beast'in global
5112
5115
  hafızasıyla (SOUL/USER/MEMORY) yürümez. Var olan botId asla üstüne yazılmaz. */
@@ -6673,44 +6676,24 @@ ipcMain.handle('install:status', async () => {
6673
6676
  /* 8) Edge TTS (bulut) */
6674
6677
  rows.push({ id: 'edge', name: 'Edge TTS — seslendirme', state: 'cloud', detail: 'bulut — kurulum gerekmez' });
6675
6678
 
6676
- /* 9) Headroom CLI — arka plan paket kurulumu burada görünür;
6677
- aç/kapat Token Sıkıştırma sekmesinde */
6678
- {
6679
- const st = headroom.probe();
6680
- let state = 'optional';
6681
- if (st.installed) state = 'ok';
6682
- else if (st.installing) state = 'loading';
6683
- if (st.failed) state = 'failed';
6684
- rows.push({
6685
- id: 'headroom',
6686
- name: 'Headroom CLI — token sıkıştırma motoru',
6687
- state,
6688
- detail: st.installing
6689
- ? 'arka planda kuruluyor — uv tool install headroom-ai[proxy]'
6690
- : st.failed
6691
- ? (st.error || 'kurulum başarısız')
6692
- : st.installed
6693
- ? 'kurulu — aç/kapat: Token Sıkıştırma sekmesi'
6694
- : 'kurulu değil — Token Sıkıştırma sekmesinden açınca otomatik kurulur',
6695
- ...(st.installing ? pctFields('headroom') : {}),
6696
- });
6697
- }
6698
-
6699
6679
  return rows;
6700
6680
  });
6701
6681
 
6702
6682
  /* cua-driver kurulumunu şimdi başlat (Kurulum sekmesi otomatiği + elle tetikleme) */
6703
6683
  ipcMain.handle('cua:install', () => require('./agent/computeruse').autoInstall());
6704
6684
 
6705
- /* HEADROOM (opsiyonel token sıkıştırma): aç/kapa + durum */
6706
- ipcMain.handle('headroom:toggle', async (_e, on) => {
6707
- settings.headroom = { ...(settings.headroom || {}), enabled: !!on };
6685
+ /* SQUEEZE (kendi token sıkıştırıcımız): aç/kapa + durum + istatistik */
6686
+ ipcMain.handle('squeeze:toggle', (_e, on) => {
6687
+ settings.squeeze = { ...(settings.squeeze || {}), enabled: !!on };
6708
6688
  saveSettings();
6709
- const r = await headroom.setEnabled(!!on).catch((e) => ({ ok: false, error: String((e && e.message) || e) }));
6710
- return r;
6689
+ squeeze.setEnabled(!!on);
6690
+ return { ok: true, enabled: !!on };
6691
+ });
6692
+ ipcMain.handle('squeeze:status', () => ({ enabled: squeeze.isEnabled(), stats: squeeze.getStats() }));
6693
+ ipcMain.handle('squeeze:reset', () => {
6694
+ squeeze.resetStats();
6695
+ return { ok: true, stats: squeeze.getStats() };
6711
6696
  });
6712
- ipcMain.handle('headroom:status', () => headroom.probe());
6713
- ipcMain.handle('headroom:stats', () => headroom.stats());
6714
6697
 
6715
6698
  /* ANDROID EMÜLATÖR: kaldırıldı — mobil önizleme tunnel (telefon QR) yoluyla yapılır */
6716
6699
 
@@ -7251,10 +7234,18 @@ function ensureDcSession(channelId) {
7251
7234
  return dcSingleSid;
7252
7235
  }
7253
7236
 
7254
- /* Masaüstü yedeği: en güncel (bg/bot-DM'siz, meşgul olmayan) sohbet — yoksa yeni */
7255
- function empatiDesktopSid() {
7237
+ /* Masaüstü hedefi: kullanıcının AÇIK sohbeti önceliklidir — proaktif not
7238
+ nereye bakıyorsa oraya işlenir; meşgulse empatiInjectToSession kuyruğa alır.
7239
+ Açık sohbet bilinmiyorsa en güncel meşgul olmayan sohbet (kanal oturumları
7240
+ hariç — onlara kanal yoluyla zaten enjekte edildi); o da yoksa yeni oturum. */
7241
+ function empatiDesktopSid(exclude) {
7242
+ const skip = exclude instanceof Set ? exclude : null;
7243
+ try {
7244
+ if (desktopActiveSid && sessionFileAlive(desktopActiveSid)) return String(desktopActiveSid);
7245
+ } catch {}
7256
7246
  try {
7257
7247
  for (const v of engine.listSessions()) {
7248
+ if (skip && skip.has(String(v.id))) continue;
7258
7249
  if (!engine.isBusy(v.id)) return String(v.id);
7259
7250
  }
7260
7251
  } catch {}
@@ -7275,14 +7266,17 @@ function empatiNotify(text, ev) {
7275
7266
  const outWa = PROACTIVE_MARK + '\n' + text + (srcWa ? '\n' + srcWa : '');
7276
7267
  const inject = empatiInjectText(ev, out);
7277
7268
  const senders = [];
7269
+ const channelSids = new Set(); /* bu tur enjeksiyon ALAN kanal oturumları */
7278
7270
  const tryWa = () => {
7279
7271
  try {
7280
7272
  const own = waOwnerNum();
7281
7273
  if (own && wa && wa.connected) {
7282
7274
  const jid = own + '@s.whatsapp.net';
7275
+ const sid = ensureWaSession(jid);
7276
+ channelSids.add(String(sid));
7283
7277
  senders.push(() =>
7284
7278
  Promise.resolve(sendWaSafe(jid, outWa))
7285
- .then(() => empatiInjectToSession(ensureWaSession(jid), inject))
7279
+ .then(() => empatiInjectToSession(sid, inject))
7286
7280
  .catch(() => {})
7287
7281
  );
7288
7282
  }
@@ -7292,9 +7286,11 @@ function empatiNotify(text, ev) {
7292
7286
  try {
7293
7287
  if (tg && tg.connected) {
7294
7288
  for (const id of tgOwnerIds()) {
7289
+ const sid = ensureTgSession(String(id));
7290
+ channelSids.add(String(sid));
7295
7291
  senders.push(() =>
7296
7292
  Promise.resolve(sendTgSafe(id, outPlain))
7297
- .then(() => empatiInjectToSession(ensureTgSession(String(id)), inject))
7293
+ .then(() => empatiInjectToSession(sid, inject))
7298
7294
  .catch(() => {})
7299
7295
  );
7300
7296
  }
@@ -7306,9 +7302,11 @@ function empatiNotify(text, ev) {
7306
7302
  if (dc && dc.connected) {
7307
7303
  const outDc = out.replace('🫡 *Beast proaktif:*', '🫡 **Beast proaktif:**');
7308
7304
  for (const id of dcOwnerIds()) {
7305
+ const sid = ensureDcSession(String(id));
7306
+ channelSids.add(String(sid));
7309
7307
  senders.push(() =>
7310
7308
  Promise.resolve(sendDcSafe(id, outDc))
7311
- .then(() => empatiInjectToSession(ensureDcSession(String(id)), inject))
7309
+ .then(() => empatiInjectToSession(sid, inject))
7312
7310
  .catch(() => {})
7313
7311
  );
7314
7312
  }
@@ -7330,8 +7328,9 @@ function empatiNotify(text, ev) {
7330
7328
  gider (kanal kullanıcısı ham BAĞLAM bloğunu görmez, ajan görür). */
7331
7329
  try {
7332
7330
  if (win && !win.isDestroyed()) {
7333
- const dsid = empatiDesktopSid();
7334
- empatiInjectToSession(dsid, out);
7331
+ const dsid = String(empatiDesktopSid(channelSids) || '');
7332
+ /* aynı oturuma kanal yoluyla zaten enjekte edildiyse TEKRAR yazma */
7333
+ if (dsid && !channelSids.has(dsid)) empatiInjectToSession(dsid, out);
7335
7334
  win.webContents.send('agent:event', { type: 'proactive', sessionId: dsid, id: ev.id, level: ev.level, title: ev.title, text: out });
7336
7335
  }
7337
7336
  } catch {}
@@ -11083,7 +11082,6 @@ ipcMain.handle('custom:set', (_e, list) => {
11083
11082
  });
11084
11083
  saveSettings();
11085
11084
  engine.setCustomProviders(settings.customProviders);
11086
- headroom.syncProviders(); /* headroom açıksa proxy setini yeni listeye uydur */
11087
11085
  return engine.publicState();
11088
11086
  });
11089
11087
 
package/src/preload.js CHANGED
@@ -10,6 +10,7 @@ contextBridge.exposeInMainWorld('beast', {
10
10
  listSessions: () => ipcRenderer.invoke('sessions:list'),
11
11
  createSession: () => ipcRenderer.invoke('sessions:create'),
12
12
  openSession: (id) => ipcRenderer.invoke('sessions:open', id),
13
+ setActiveSession: (id) => ipcRenderer.invoke('sessions:active', id),
13
14
  deleteSession: (id) => ipcRenderer.invoke('sessions:delete', id),
14
15
  send: (sessionId, text) => ipcRenderer.invoke('agent:send', { sessionId, text }),
15
16
  interrupt: (sessionId, reason) => ipcRenderer.invoke('agent:interrupt', sessionId, reason),
@@ -260,9 +261,9 @@ contextBridge.exposeInMainWorld('beast', {
260
261
  idePreviewUrl: (url) => ipcRenderer.invoke('ide:previewUrl', url),
261
262
  ideProjInfo: () => ipcRenderer.invoke('ide:projinfo'),
262
263
  ideExpQr: () => ipcRenderer.invoke('ide:expqr'),
263
- headroomToggle: (on) => ipcRenderer.invoke('headroom:toggle', !!on),
264
- headroomStatus: () => ipcRenderer.invoke('headroom:status'),
265
- headroomStats: () => ipcRenderer.invoke('headroom:stats'),
264
+ squeezeToggle: (on) => ipcRenderer.invoke('squeeze:toggle', !!on),
265
+ squeezeStatus: () => ipcRenderer.invoke('squeeze:status'),
266
+ squeezeReset: () => ipcRenderer.invoke('squeeze:reset'),
266
267
  agentsClearAll: () => ipcRenderer.invoke('agents:clearAll'),
267
268
  ideDelete: (rel) => ipcRenderer.invoke('ide:delete', rel),
268
269
  onWaEvent: (cb) => ipcRenderer.on('wa:event', (_e, ev) => cb(ev)),
@@ -66,7 +66,7 @@
66
66
  set_title: 'AYARLAR',
67
67
  tab_lang: 'Dil',
68
68
  lang_sub: 'Arayüz dili — uygulama dili ve sesli komut dili buradan değişir',
69
- tab_headroom: 'Token Sıkıştırma',
69
+ tab_squeeze: 'Squeeze',
70
70
  tab_provider: 'Provider',
71
71
  tab_fallout: 'Fallout',
72
72
  tab_memory: 'Memory',
@@ -787,7 +787,7 @@
787
787
  set_title: 'SETTINGS',
788
788
  tab_lang: 'Language',
789
789
  lang_sub: 'Interface language — app UI and voice command language',
790
- tab_headroom: 'Token Compression',
790
+ tab_squeeze: 'Squeeze',
791
791
  tab_provider: 'Provider',
792
792
  tab_fallout: 'Fallout',
793
793
  tab_memory: 'Memory',
@@ -511,7 +511,7 @@
511
511
  <button class="tab" data-tab="cron" data-i18n="tab_cron">Cron</button>
512
512
  <button class="tab" data-tab="agenda" data-i18n="tab_agenda">Ajanda</button>
513
513
  <button class="tab" data-tab="usage" data-i18n="tab_usage">Maliyet · Limit</button>
514
- <button class="tab" data-tab="headroom" data-i18n="tab_headroom">Token Sıkıştırma</button>
514
+ <button class="tab" data-tab="squeeze" data-i18n="tab_squeeze">Squeeze</button>
515
515
  <button class="tab" data-tab="logs" data-i18n="tab_logs">Log</button>
516
516
  <button class="tab" data-tab="dash" data-i18n="tab_dash">Dashboard</button>
517
517
  <button class="tab" data-tab="sec" data-i18n="tab_security">Güvenlik</button>
@@ -537,7 +537,7 @@
537
537
  <div id="tab-events" class="pane" hidden></div>
538
538
  <div id="tab-empati" class="pane" hidden></div>
539
539
  <div id="tab-usage" class="pane" hidden></div>
540
- <div id="tab-headroom" class="pane" hidden></div>
540
+ <div id="tab-squeeze" class="pane" hidden></div>
541
541
  <div id="tab-logs" class="pane" hidden></div>
542
542
  <div id="tab-dash" class="pane" hidden></div>
543
543
  <div id="tab-sec" class="pane" hidden></div>
@@ -1077,6 +1077,7 @@ setInterval(() => {
1077
1077
 
1078
1078
  async function openSession(id) {
1079
1079
  activeId = id;
1080
+ try { beast.setActiveSession(id); } catch {}
1080
1081
  streamEl = null;
1081
1082
  const s = await beast.openSession(id);
1082
1083
  els.msgs.innerHTML = '';
@@ -1213,7 +1214,6 @@ function addFiles(files, mode) {
1213
1214
  /* ---------------- settings ---------------- */
1214
1215
 
1215
1216
  let setTab = 'lang'; /* ayarlar artık DİL sekmesiyle açılır */
1216
- let hrPaneTimer = null; /* headroom-status → Kurulum sekmesi debounce tazeleme */
1217
1217
 
1218
1218
  /* DİL sekmesi: arayüz dili seçimi (eski sağ-alt dil ikonunun yerine) */
1219
1219
  function renderLangPane() {
@@ -1280,7 +1280,7 @@ async function renderActiveSettingsTab() {
1280
1280
  case 'cron': await openCron(); break;
1281
1281
  case 'agenda': await renderAgendaPane(); break;
1282
1282
  case 'usage': await renderUsagePane(); break;
1283
- case 'headroom': await renderHeadroomPane(); break;
1283
+ case 'squeeze': await renderSqueezePane(); break;
1284
1284
  case 'agents': await refreshAgentsPane(); break;
1285
1285
  case 'logs': await renderLogPane(); break;
1286
1286
  case 'dash': await renderDashboardPane(); break;
@@ -1364,7 +1364,7 @@ function switchTab(name) {
1364
1364
  document.querySelectorAll('#setTabs .tab').forEach((b) =>
1365
1365
  b.classList.toggle('active', b.dataset.tab === name)
1366
1366
  );
1367
- for (const p of ['lang', 'provider', 'fallout', 'skills', 'agents', 'tts', 'install', 'email', 'integrations', 'websearch', 'mcp', 'events', 'empati', 'cron', 'agenda', 'usage', 'headroom', 'logs', 'dash', 'sec', 'update']) {
1367
+ for (const p of ['lang', 'provider', 'fallout', 'skills', 'agents', 'tts', 'install', 'email', 'integrations', 'websearch', 'mcp', 'events', 'empati', 'cron', 'agenda', 'usage', 'squeeze', 'logs', 'dash', 'sec', 'update']) {
1368
1368
  const el = $('#tab-' + p);
1369
1369
  if (el) el.hidden = p !== name; // guard: eksik pane tüm sekmeleri kilitlemesin
1370
1370
  }
@@ -1374,7 +1374,7 @@ function switchTab(name) {
1374
1374
  if (name === 'email') renderEmailPane();
1375
1375
  if (name === 'usage') renderUsagePane();
1376
1376
  if (name === 'install') renderInstallPane();
1377
- if (name === 'headroom') renderHeadroomPane();
1377
+ if (name === 'squeeze') renderSqueezePane();
1378
1378
  if (name === 'events') renderEventsPane();
1379
1379
  if (name === 'empati') renderEmpatiPane();
1380
1380
  if (name === 'logs') renderLogPane();
@@ -2332,91 +2332,90 @@ function ttsFlushTail() {
2332
2332
  /* ---------------- KURULUM SEKMESİ: bileşenler OTOMATİK kurulur ----------------
2333
2333
  Eksik/kısmen inmiş bileşen sekme açılınca kendiliğinden indirilir; kullanıcı
2334
2334
  hiçbir düğmeye basmaz. Durum canlı güncellenir (4 sn'de bir). */
2335
- /* ---------- TOKEN SIKIŞTIRMA (Headroom) sekmesi ----------
2336
- Opsiyonel yerel sıkıştırma proxy'si: açıkken LLM girdisi %30-70 küçülür.
2337
- Aç/Kapat buradan; CLI kurulumu Kurulum sekmesinde görülür. */
2338
- let hrStatsTimer = null;
2339
-
2340
- async function renderHeadroomPane() {
2341
- const pane = $('#tab-headroom');
2335
+ /* ---------- SQUEEZE (kendi token sıkıştırıcımız) sekmesi ----------
2336
+ Varsayılan KAPALI. Açıkken her LLM isteğinin kopyası gönderimden önce
2337
+ sıkıştırılır: tekrar eden büyük bloklar bağlamda bir kez gider, eski araç
2338
+ çıktıları başlık + önizlemeye iner. Oturum kaydı değişmez. */
2339
+ let sqStatsTimer = null;
2340
+
2341
+ async function renderSqueezePane() {
2342
+ const pane = $('#tab-squeeze');
2342
2343
  if (!pane) return;
2343
- const st = await beast.headroomStatus().catch(() => null);
2344
-
2344
+ const st = await beast.squeezeStatus().catch(() => null);
2345
+ const on = !!(st && st.enabled);
2346
+ const s = (st && st.stats) || {};
2345
2347
  const fmtN = (n) => new Intl.NumberFormat('tr-TR').format(Math.round(Number(n) || 0));
2346
- let stHtml = '';
2347
- if (st) {
2348
- const readyN = (st.proxies || []).filter((p) => p.ready).length;
2349
- let stateTxt;
2350
- if (st.installing) stateTxt = '⏳ arka planda kuruluyor — uv tool install headroom-ai[proxy] (birkaç dakika)';
2351
- else if (st.enabled && readyN) stateTxt = '🟢 açık — proxy: ' + (st.proxies || []).filter((p) => p.ready).map((p) => '127.0.0.1:' + p.port).join(', ');
2352
- else if (st.enabled) stateTxt = '⏳ proxy başlatılıyor… (ilk açılış ~30 sn)';
2353
- else if (st.failed) stateTxt = '⛔ ' + (st.error || 'kurulum başarısız');
2354
- else if (st.installed) stateTxt = '⚪ kapalı — kurulu, aşağıdaki düğmeyle etkinleştir';
2355
- else stateTxt = '⚪ kapalı — açınca uv ile otomatik kurulur (birkaç dakika)';
2356
- stHtml = '<div class="sub" style="margin-top:10px;font-size:12px;text-align:center">' + escapeHtml(stateTxt) + '</div>';
2357
- }
2358
-
2359
- const stats = await beast.headroomStats().catch(() => null);
2348
+
2349
+ const stHtml =
2350
+ '<div class="sub" style="margin-top:10px;font-size:12px;text-align:center">' +
2351
+ (on
2352
+ ? '🟢 açık — her isteğin kopyası gönderimden önce sıkıştırılır; oturum kaydın değişmez'
2353
+ : '⚪ kapalı (varsayılan) — açınca yeni isteklerde uygulanır, geçmişe dokunmaz') +
2354
+ '</div>';
2355
+
2360
2356
  let statsHtml = '';
2361
- if (stats && stats.ok && (stats.input || stats.saved)) {
2357
+ if (s.calls) {
2362
2358
  statsHtml =
2363
2359
  '<div class="divider"></div>' +
2364
- '<h2 style="text-align:center">Tasarruf (proxy açıkken)</h2>' +
2360
+ '<h2 style="text-align:center">Tasarruf</h2>' +
2365
2361
  '<div style="display:flex;gap:24px;flex-wrap:wrap;margin-top:8px;justify-content:center">' +
2366
- '<div><div style="font-size:20px;font-weight:800;color:var(--ok)">%' + Math.round(stats.savingsPercent || 0) + '</div><div class="sub">sıkıştırma oranı</div></div>' +
2367
- '<div><div style="font-size:20px;font-weight:800">' + new Intl.NumberFormat('tr-TR').format(stats.proxyCompressionSaved || 0) + '</div><div class="sub">kaydedilen token</div></div>' +
2368
- '<div><div style="font-size:20px;font-weight:800">' + new Intl.NumberFormat('tr-TR').format(stats.input || 0) + '</div><div class="sub">toplam girdi token</div></div>' +
2369
- '</div>';
2362
+ '<div title="Son istekte tahmini token tasarrufu"><div style="font-size:20px;font-weight:800;color:var(--ok)">%' + (s.lastPercent || 0) + '</div><div class="sub">son istekte sıkıştırma</div></div>' +
2363
+ '<div><div style="font-size:20px;font-weight:800">' + fmtN(s.savedTokens) + '</div><div class="sub">toplam kaydedilen token (tahmini)</div></div>' +
2364
+ '<div><div style="font-size:20px;font-weight:800">' + fmtN(s.changedCalls) + '</div><div class="sub">sıkıştırılan istek</div></div>' +
2365
+ '</div>' +
2366
+ '<div class="sub" style="text-align:center;margin-top:8px">tekrar bloğu: ' + fmtN(s.deduped) +
2367
+ ' · eski araç çıktısı: ' + fmtN(s.compactedTools) +
2368
+ ' · skill: ' + fmtN(s.compactedSkills) +
2369
+ ' · kullanıcı içeriği: ' + fmtN(s.compactedUsers) + '</div>';
2370
2370
  }
2371
2371
 
2372
- /* AÇ/KAPAT düğmesi: ortalanmış + SVG ikonlu (play/pause — uygulama ikon stili) */
2373
- const svgPlay =
2374
- '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polygon points="6 3 20 12 6 21 6 3"/></svg>';
2375
- const svgPause =
2376
- '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="6" y="4" width="4" height="16" rx="1"/><rect x="14" y="4" width="4" height="16" rx="1"/></svg>';
2377
2372
  pane.innerHTML =
2378
- '<h2 style="text-align:center">' + _t('tab_headroom') + '</h2>' +
2379
- '<div class="sub" style="text-align:center;max-width:620px;margin:0 auto">Headroom yerel proxy\u2019si — araç çıktıları, loglar ve JSON yükleri modele gitmeden sıkıştırılır; orijinali yerelde saklanır. Token faturasını %30-70 düşürür. Yalnız OpenAI-uyumlu sağlayıcı trafiğini etkiler; kapalıyken hiçbir şey değişmez.</div>' +
2373
+ '<h2 style="text-align:center">' + _t('tab_squeeze') + '</h2>' +
2380
2374
  '<div style="display:flex;flex-direction:column;align-items:center;gap:10px;margin-top:18px">' +
2381
- '<button id="hrToggle" class="btn" style="width:auto;padding:11px 16px;display:inline-flex;align-items:center;gap:8px;justify-content:center" title=""></button>' +
2382
- '<span id="hrToggleMsg" class="sub" style="margin:0;text-align:center;max-width:560px"></span>' +
2375
+ '<button id="sqToggle" class="btn" style="width:auto;padding:11px 16px;display:inline-flex;align-items:center;gap:8px;justify-content:center" title=""></button>' +
2376
+ '<span id="sqToggleMsg" class="sub" style="margin:0;text-align:center;max-width:560px"></span>' +
2383
2377
  '</div>' +
2384
- stHtml + statsHtml;
2378
+ stHtml + statsHtml +
2379
+ (s.calls ? '<div style="text-align:center;margin-top:12px"><button id="sqReset" class="btn" style="width:auto;padding:6px 12px;font-size:12px">İstatistiği sıfırla</button></div>' : '');
2385
2380
 
2386
- const btn = $('#hrToggle');
2381
+ const svgPlay =
2382
+ '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polygon points="6 3 20 12 6 21 6 3"/></svg>';
2383
+ const svgPause =
2384
+ '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="6" y="4" width="4" height="16" rx="1"/><rect x="14" y="4" width="4" height="16" rx="1"/></svg>';
2385
+ const btn = $('#sqToggle');
2387
2386
  if (btn) {
2388
- const on = !!(st && st.enabled);
2389
2387
  /* yalnız SVG ikon — anlamı durum satırı + tooltip taşır */
2390
2388
  btn.innerHTML = on ? svgPause : svgPlay;
2391
- btn.title = on ? 'Token sıkıştırmayı kapat' : 'Token sıkıştırmayı aç';
2389
+ btn.title = on ? 'Squeeze\'i kapat' : 'Squeeze\'i aç';
2392
2390
  btn.setAttribute('aria-label', btn.title);
2393
2391
  btn.addEventListener('click', async () => {
2394
- const msg = $('#hrToggleMsg');
2392
+ const msg = $('#sqToggleMsg');
2395
2393
  btn.disabled = true;
2396
2394
  btn.innerHTML = '<span>…</span>';
2397
- const r = await beast.headroomToggle(!on).catch(() => null);
2395
+ const r = await beast.squeezeToggle(!on).catch(() => null);
2398
2396
  if (msg) {
2399
- if (r && r.installing) {
2400
- msg.textContent = 'Headroom arka planda kuruluyor — birkaç dakika sonra hazır olur, bu sekme açık kalabilir.';
2401
- } else if (r && r.error) {
2402
- msg.textContent = r.error;
2403
- } else {
2404
- msg.textContent = !on ? 'Açıldı — proxy hazır olunca trafiği yönlendirir' : 'Kapatıldı — trafik sağlayıcıya düz gidiyor';
2405
- }
2397
+ msg.textContent = r && r.ok
2398
+ ? (!on ? 'Açıldı — sonraki istekler sıkıştırılarak gönderilir' : 'Kapatıldı — istekler olduğu gibi gider')
2399
+ : 'Ayar kaydedilemedi';
2406
2400
  }
2407
- setTimeout(() => renderHeadroomPane(), 2500);
2401
+ setTimeout(() => renderSqueezePane(), 400);
2402
+ });
2403
+ }
2404
+ const rb = $('#sqReset');
2405
+ if (rb) {
2406
+ rb.addEventListener('click', async () => {
2407
+ rb.disabled = true;
2408
+ await beast.squeezeReset().catch(() => null);
2409
+ renderSqueezePane();
2408
2410
  });
2409
2411
  }
2410
2412
 
2411
2413
  /* açıkken istatistikler canlı aksın */
2412
- if (hrStatsTimer) clearInterval(hrStatsTimer);
2413
- hrStatsTimer = setInterval(async () => {
2414
- if (pane.hidden) { clearInterval(hrStatsTimer); return; }
2415
- const s2 = await beast.headroomStatus().catch(() => null);
2416
- const cur = $('#hrToggle');
2417
- if (s2 && cur && cur.disabled) return; /* toggle işlemi sürüyor */
2418
- await renderHeadroomPane();
2419
- }, 6000);
2414
+ if (sqStatsTimer) clearInterval(sqStatsTimer);
2415
+ sqStatsTimer = setInterval(() => {
2416
+ if (pane.hidden) { clearInterval(sqStatsTimer); return; }
2417
+ renderSqueezePane();
2418
+ }, 5000);
2420
2419
  }
2421
2420
 
2422
2421
  async function renderInstallPane() {
@@ -2486,24 +2485,10 @@ async function renderInstallPane() {
2486
2485
  '<div class="sub" style="font-size:11px">' + escapeHtml(r.detail || '') + (r.mb ? ' · ' + r.mb + ' MB' : '') + '</div>' +
2487
2486
  bar(r) +
2488
2487
  '</div>' +
2489
- (r.canToggle
2490
- ? '<button class="btn hr-tgl" data-on="' + (r.toggleOn ? '1' : '0') + '" style="width:auto;padding:2px 12px;flex:none">' + (r.toggleOn ? 'KAPAT' : 'AÇ') + '</button>'
2491
- : '') +
2492
2488
  badge(r.state) +
2493
2489
  pctLabel(r) +
2494
2490
  '</div>'
2495
2491
  )).join('');
2496
- /* Headroom aç/kapa: toggle kurulum + proxy başlatmayı arka planda tetikler */
2497
- el.querySelectorAll('.hr-tgl').forEach((b) => {
2498
- b.addEventListener('click', async () => {
2499
- const on = b.dataset.on !== '1';
2500
- b.disabled = true;
2501
- b.textContent = '…';
2502
- const r = await beast.headroomToggle(on).catch(() => null);
2503
- toast(r && r.error ? r.error : (r && r.installing ? 'Headroom kuruluyor — birkaç dakika sürer' : (on ? 'Headroom açıldı' : 'Headroom kapatıldı')));
2504
- refresh();
2505
- });
2506
- });
2507
2492
  };
2508
2493
 
2509
2494
  const refresh = async () => {
@@ -5745,19 +5730,6 @@ function onEvent(ev) {
5745
5730
  showPermissionCard(req, isBc);
5746
5731
  return;
5747
5732
  }
5748
- /* Headroom durum değişimi → açık olan sekmeyi tazele (Token Sıkıştırma / Kurulum) */
5749
- if (ev.type === 'headroom-status') {
5750
- try {
5751
- const iPane = document.getElementById('tab-install');
5752
- const hPane = document.getElementById('tab-headroom');
5753
- const open = (!iPane || iPane.hidden) && (!hPane || hPane.hidden) ? '' : (!hPane || hPane.hidden ? 'install' : 'headroom');
5754
- if (open) {
5755
- clearTimeout(hrPaneTimer);
5756
- hrPaneTimer = setTimeout(() => { (open === 'headroom' ? renderHeadroomPane() : renderInstallPane()).catch(() => {}); }, 1500);
5757
- }
5758
- } catch {}
5759
- return;
5760
- }
5761
5733
  /* EMPATİ LOOP: proaktif bildirim (masaüstü) + sekme canlı yenileme.
5762
5734
  Toast'ta yalnız haber başlığı — link/markdown kalabalığı chat mesajında. */
5763
5735
  if (ev.type === 'proactive') {
@@ -1,279 +0,0 @@
1
- 'use strict';
2
-
3
- /* ---------- BEAST × HEADROOM (OPSİYONEL token sıkıştırma) ----------
4
- Ayarlardan (Kurulum sekmesi) açılıp kapanır; kapalıyken hiçbir şey yapmaz.
5
- Açıkken: LLM istekleri yerel headroom proxy'sinden geçer — araç çıktıları,
6
- loglar ve JSON yükleri modele gitmeden sıkıştırılır (testte %39-71 tasarruf).
7
- Kurulum: uv tool install "headroom-ai[proxy]" (uv yoksa hata bildirir).
8
- Her provider için AYRI proxy portu (base URL farkları için); llm.js
9
- providerId → port haritasıyla yönlendirir, kapatınca düz bağlanır. */
10
-
11
- const { spawn } = require('child_process');
12
- const fs = require('fs');
13
- const path = require('path');
14
- const os = require('os');
15
- const { emitInstallProgress } = require('./progressbus');
16
-
17
- const PORT_BASE = 18787;
18
- const MAX_PROXIES = 8;
19
-
20
- const state = {
21
- enabled: false,
22
- installing: false,
23
- installFailed: false,
24
- installError: '',
25
- starting: false,
26
- proxies: new Map(), // providerId -> { proc, port, baseUrl, ready }
27
- providerGetter: null, // () => [{ id, name, baseUrl }]
28
- statusSink: null, // (probe) => void — main renderer'a akıtır
29
- };
30
-
31
- function _emit() {
32
- try { if (state.statusSink) state.statusSink(probe()); } catch {}
33
- }
34
-
35
- function headroomExe() {
36
- const local = path.join(os.homedir(), '.local', 'bin', 'headroom.exe');
37
- if (fs.existsSync(local)) return local;
38
- try {
39
- require('child_process').execSync('where headroom', { timeout: 5000, stdio: 'pipe' });
40
- return 'headroom';
41
- } catch {}
42
- return '';
43
- }
44
-
45
- function uvExe() {
46
- const local = path.join(os.homedir(), '.local', 'bin', 'uv.exe');
47
- if (fs.existsSync(local)) return local;
48
- try {
49
- require('child_process').execSync('where uv', { timeout: 5000, stdio: 'pipe' });
50
- return 'uv';
51
- } catch {}
52
- return '';
53
- }
54
-
55
- function probe() {
56
- return {
57
- installed: !!headroomExe(),
58
- installing: state.installing,
59
- failed: state.installFailed,
60
- error: state.installError,
61
- enabled: state.enabled,
62
- starting: state.starting,
63
- proxies: [...state.proxies.values()].map((p) => ({ port: p.port, baseUrl: p.baseUrl, ready: p.ready })),
64
- };
65
- }
66
-
67
- /* ---------- KURULUM (arka plan) ----------
68
- uv tool install headroom-ai[proxy] — kendi izole Python 3.13 ortamını kurar;
69
- sistemde Python gerekmez. uv yoksa pip fallback, o da yoksa elle yönlendirir. */
70
- function autoInstall() {
71
- if (state.installing || headroomExe()) return;
72
- const uv = uvExe();
73
- if (!uv) {
74
- state.installFailed = true;
75
- state.installError = 'uv bulunamadı — kur: winget install astral-sh.uv';
76
- _emit();
77
- return;
78
- }
79
- state.installing = true;
80
- state.installFailed = false;
81
- state.installError = '';
82
- _emit();
83
- emitInstallProgress('headroom', { pct: 4 });
84
- const proc = spawn(uv, ['tool', 'install', '--python', '3.13', 'headroom-ai[proxy]'], { windowsHide: true });
85
- let buf = '';
86
- let stage = 4;
87
- const bump = () => {
88
- stage = Math.min(92, stage + 7);
89
- emitInstallProgress('headroom', { pct: stage });
90
- };
91
- const onData = (c) => {
92
- const s = String(c);
93
- if (/Download|Resolve|Installed|Prepared/i.test(s)) bump();
94
- };
95
- proc.stdout.on('data', onData);
96
- proc.stderr.on('data', onData);
97
- proc.on('close', (code) => {
98
- state.installing = false;
99
- if (code === 0 && headroomExe()) {
100
- emitInstallProgress('headroom', { pct: 100 });
101
- } else {
102
- state.installFailed = true;
103
- state.installError = 'kurulum başarısız — elle: uv tool install "headroom-ai[proxy]"';
104
- }
105
- _emit();
106
- /* kurulum bitti ve açık kaldıysa proxy'leri hemen başlat */
107
- if (state.enabled && headroomExe()) start().catch(() => {});
108
- });
109
- proc.on('error', () => {
110
- state.installing = false;
111
- state.installFailed = true;
112
- state.installError = 'kurulum başlatılamadı (uv)';
113
- _emit();
114
- });
115
- }
116
-
117
- /* ---------- PROXY SÜREÇLERİ ----------
118
- Provider başına bir proxy; cache modunu BOZMAMAK için mode=token,
119
- Beast'in tanımadığı headroom_retrieve aracı enjekte EDİLMESİN diye
120
- no-ccr (geri-getirilemez sıkıştırma, en yüksek tasarruf) ve rate limit
121
- kapalı (Beast'in kendi istek patlamalarına takılmasın). Beacon kapalı. */
122
- function nextPort() {
123
- const used = new Set([...state.proxies.values()].map((p) => p.port));
124
- for (let i = 0; i < MAX_PROXIES; i++) {
125
- const port = PORT_BASE + i;
126
- if (!used.has(port)) return port;
127
- }
128
- return 0;
129
- }
130
-
131
- function spawnFor(provider) {
132
- return new Promise((resolve) => {
133
- const exe = headroomExe();
134
- if (!exe) return resolve({ ok: false, error: 'headroom kurulu değil' });
135
- const port = nextPort();
136
- if (!port) return resolve({ ok: false, error: 'port kalmadı' });
137
- let proc;
138
- try {
139
- proc = spawn(exe, [
140
- 'proxy', '--port', String(port), '--openai-api-url', provider.baseUrl,
141
- '--mode', 'token', '--no-ccr', '--no-rate-limit',
142
- ], {
143
- windowsHide: true,
144
- env: { ...process.env, HEADROOM_BEACON: 'off', DO_NOT_TRACK: '1' },
145
- });
146
- } catch (e) {
147
- return resolve({ ok: false, error: String((e && e.message) || e) });
148
- }
149
- const rec = { proc, port, baseUrl: provider.baseUrl, ready: false };
150
- state.proxies.set(provider.id, rec);
151
- _emit();
152
- proc.on('exit', () => {
153
- if (state.proxies.get(provider.id) === rec) state.proxies.delete(provider.id);
154
- _emit();
155
- });
156
- const t0 = Date.now();
157
- const poll = setInterval(async () => {
158
- let ready = false;
159
- try {
160
- const r = await fetch('http://127.0.0.1:' + port + '/health', { signal: AbortSignal.timeout(2000) });
161
- ready = !!r.ok;
162
- } catch {}
163
- if (ready) {
164
- clearInterval(poll);
165
- rec.ready = true;
166
- _emit();
167
- return resolve({ ok: true, port });
168
- }
169
- if (Date.now() - t0 > 90000) {
170
- clearInterval(poll);
171
- try { rec.proc.kill(); } catch {}
172
- state.proxies.delete(provider.id);
173
- _emit();
174
- resolve({ ok: false, error: 'proxy zaman aşımı (90sn)' });
175
- }
176
- }, 1200);
177
- });
178
- }
179
-
180
- async function start() {
181
- const exe = headroomExe();
182
- if (!exe) {
183
- autoInstall();
184
- return { ok: false, error: 'kuruluyor — birkaç dakika sonra hazır olur' };
185
- }
186
- state.starting = true;
187
- _emit();
188
- try {
189
- const providers = (state.providerGetter ? state.providerGetter() : []).slice(0, MAX_PROXIES);
190
- const routes = {};
191
- /* önce eskileri temizle: listeden çıkan provider'ın proxy'sini kapat */
192
- const keep = new Set(providers.map((p) => p.id));
193
- for (const [id, rec] of [...state.proxies.entries()]) {
194
- if (keep.has(id)) continue;
195
- try { rec.proc.kill(); } catch {}
196
- try { spawn('taskkill', ['/pid', String(rec.proc.pid), '/T', '/F'], { windowsHide: true }); } catch {}
197
- state.proxies.delete(id);
198
- }
199
- for (const p of providers) {
200
- if (!p.baseUrl || state.proxies.has(p.id)) continue;
201
- const r = await spawnFor(p);
202
- if (r.ok) routes[p.id] = r.port;
203
- }
204
- try { require('./llm').setHeadroomRoutes(routes); } catch {}
205
- state.enabled = true;
206
- return { ok: true, routes };
207
- } finally {
208
- state.starting = false;
209
- _emit();
210
- }
211
- }
212
-
213
- async function setEnabled(on) {
214
- if (on) {
215
- state.enabled = true;
216
- if (!headroomExe()) {
217
- autoInstall();
218
- return { ok: false, installing: true, error: 'headroom arka planda kuruluyor — birkaç dakika sonra hazır' };
219
- }
220
- return start();
221
- }
222
- state.enabled = false;
223
- stopAll();
224
- try { require('./llm').setHeadroomRoutes(null); } catch {}
225
- return { ok: true };
226
- }
227
-
228
- /* provider listesi değişti (eklendi/silindi) → proxy setini senkronla */
229
- function syncProviders() {
230
- if (!state.enabled || !headroomExe()) return;
231
- start().catch(() => {});
232
- }
233
-
234
- function stopAll() {
235
- for (const rec of state.proxies.values()) {
236
- try { rec.proc.kill(); } catch {}
237
- try { spawn('taskkill', ['/pid', String(rec.proc.pid), '/T', '/F'], { windowsHide: true }); } catch {}
238
- }
239
- state.proxies.clear();
240
- _emit();
241
- }
242
-
243
- function setProviderGetter(fn) { state.providerGetter = typeof fn === 'function' ? fn : null; }
244
- function setStatusSink(fn) { state.statusSink = typeof fn === 'function' ? fn : null; }
245
-
246
- /* proxy /stats özeti (sağlık duvarı yok — hata.yakalanır) */
247
- async function stats() {
248
- const rec = [...state.proxies.values()].find((p) => p.ready);
249
- if (!rec) return { ok: false, running: false };
250
- try {
251
- const r = await fetch('http://127.0.0.1:' + rec.port + '/stats', { signal: AbortSignal.timeout(4000) });
252
- const j = await r.json();
253
- const s = (j && (j.tokens || j.summary)) || {};
254
- return {
255
- ok: true,
256
- running: true,
257
- port: rec.port,
258
- input: Number(s.input) || 0,
259
- output: Number(s.output) || 0,
260
- saved: Number(s.saved) || 0,
261
- proxyCompressionSaved: Number(s.proxy_compression_saved) || 0,
262
- savingsPercent: Number(s.savings_percent) || 0,
263
- allLayersSaved: Number(s.all_layers_saved) || 0,
264
- };
265
- } catch {
266
- return { ok: false, running: true, port: rec.port };
267
- }
268
- }
269
-
270
- module.exports = {
271
- probe,
272
- autoInstall,
273
- setEnabled,
274
- syncProviders,
275
- stopAll,
276
- setProviderGetter,
277
- setStatusSink,
278
- stats,
279
- };