@cccarv82/freya 3.5.1 → 3.6.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/cli/web.js CHANGED
@@ -98,6 +98,283 @@ function newestFile(dir, prefix) {
98
98
  return files[0]?.p || null;
99
99
  }
100
100
 
101
+ // ---------------------------------------------------------------------------
102
+ // Daily-logs ↔ SQLite sync: keeps the daily_logs table in sync with .md files
103
+ // ---------------------------------------------------------------------------
104
+ function syncDailyLogs(workspaceDir) {
105
+ try {
106
+ const logsDir = path.join(workspaceDir, 'logs', 'daily');
107
+ if (!exists(logsDir)) return { synced: 0, toEmbed: [] };
108
+ const files = fs.readdirSync(logsDir).filter(f => /^\d{4}-\d{2}-\d{2}\.md$/.test(f));
109
+ if (!files.length) return { synced: 0, toEmbed: [] };
110
+
111
+ const upsert = dl.db.prepare(`
112
+ INSERT INTO daily_logs (date, raw_markdown) VALUES (?, ?)
113
+ ON CONFLICT(date) DO UPDATE SET raw_markdown = excluded.raw_markdown
114
+ `);
115
+
116
+ let synced = 0;
117
+ const toEmbed = []; // collect logs that need embedding
118
+ const tx = dl.db.transaction((fileList) => {
119
+ for (const file of fileList) {
120
+ const date = file.replace('.md', '');
121
+ const content = fs.readFileSync(path.join(logsDir, file), 'utf8');
122
+ upsert.run(date, content);
123
+ toEmbed.push({ date, content });
124
+ synced++;
125
+ }
126
+ });
127
+ tx(files);
128
+ return { synced, toEmbed };
129
+ } catch (e) {
130
+ console.error('[sync] Daily-logs sync failed:', e.message);
131
+ return { synced: 0, toEmbed: [] };
132
+ }
133
+ }
134
+
135
+ // ---------------------------------------------------------------------------
136
+ // Background embedding generation — runs async, never blocks
137
+ // ---------------------------------------------------------------------------
138
+ async function generateEmbeddingsBackground(workspaceDir, items) {
139
+ // items = [{ type: 'daily_log'|'task'|'blocker', id: string, text: string }]
140
+ if (!items || !items.length) return;
141
+ try {
142
+ const dm = new DataManager(workspaceDir, path.join(workspaceDir, 'logs'));
143
+ let generated = 0;
144
+ for (const item of items) {
145
+ try {
146
+ // Skip if embeddings already exist AND item is not a daily_log
147
+ // (daily logs get updated frequently, so always regenerate)
148
+ if (item.type !== 'daily_log' && dm.hasEmbeddings(item.type, item.id)) continue;
149
+ const count = await dm.generateEmbeddings(item.type, item.id, item.text);
150
+ generated += count;
151
+ } catch (err) {
152
+ console.error(`[embeddings] Failed for ${item.type}/${item.id}:`, err.message);
153
+ }
154
+ }
155
+ if (generated > 0) console.log(`[embeddings] Generated ${generated} embedding chunks`);
156
+ } catch (err) {
157
+ console.error('[embeddings] Background generation failed:', err.message);
158
+ }
159
+ }
160
+
161
+ // ---------------------------------------------------------------------------
162
+ // Build structured data context (tasks, blockers, projects) — always compact
163
+ // ---------------------------------------------------------------------------
164
+ function buildStructuredContext() {
165
+ const parts = [];
166
+
167
+ // Pending tasks
168
+ try {
169
+ const tasks = dl.db.prepare("SELECT id, description, category, status, project_slug, created_at, due_date FROM tasks WHERE status = 'PENDING' ORDER BY created_at DESC LIMIT 50").all();
170
+ if (tasks.length) {
171
+ parts.push('\n[TASKS PENDENTES (' + tasks.length + ')]');
172
+ for (const t of tasks) {
173
+ parts.push(`• [${t.category}] ${t.description} (projeto: ${t.project_slug || 'N/A'}${t.due_date ? ', prazo: ' + t.due_date : ''})`);
174
+ }
175
+ } else {
176
+ parts.push('\n[TASKS: nenhuma task pendente registrada no sistema]');
177
+ }
178
+ } catch { /* ignore */ }
179
+
180
+ // Open blockers
181
+ try {
182
+ const blockers = dl.db.prepare("SELECT id, title, severity, status, project_slug, owner, next_action, created_at FROM blockers WHERE status IN ('OPEN','MITIGATING') ORDER BY created_at DESC LIMIT 30").all();
183
+ if (blockers.length) {
184
+ parts.push('\n[BLOCKERS ABERTOS (' + blockers.length + ')]');
185
+ for (const b of blockers) {
186
+ parts.push(`• [${b.severity}] ${b.title} (projeto: ${b.project_slug || 'N/A'}, owner: ${b.owner || '?'})`);
187
+ }
188
+ } else {
189
+ parts.push('\n[BLOCKERS: nenhum blocker aberto registrado no sistema]');
190
+ }
191
+ } catch { /* ignore */ }
192
+
193
+ // Active projects
194
+ try {
195
+ const projects = dl.db.prepare("SELECT slug, client, name FROM projects WHERE is_active = 1 ORDER BY slug").all();
196
+ if (projects.length) {
197
+ parts.push('\n[PROJETOS ATIVOS (' + projects.length + ')]');
198
+ for (const p of projects) {
199
+ parts.push(`• ${p.slug} — ${p.name || p.client || 'sem nome'}`);
200
+ }
201
+ }
202
+ } catch { /* ignore */ }
203
+
204
+ return parts.join('\n');
205
+ }
206
+
207
+ // ---------------------------------------------------------------------------
208
+ // Smart context builder: uses RAG when available, falls back to raw logs
209
+ // ---------------------------------------------------------------------------
210
+ async function buildSmartContext(workspaceDir, query) {
211
+ const parts = [];
212
+ const dm = new DataManager(workspaceDir, path.join(workspaceDir, 'logs'));
213
+ const TOKEN_BUDGET = 12000; // chars budget for log/RAG context
214
+ let usedBudget = 0;
215
+
216
+ // 1. Try semantic search first (if embeddings exist)
217
+ let ragUsed = false;
218
+ try {
219
+ const embCount = dm.getEmbeddingCount();
220
+ if (embCount > 0) {
221
+ const ragResults = await dm.semanticSearch(query, 15);
222
+ const relevant = ragResults.filter(r => r.score > 0.25);
223
+ if (relevant.length > 0) {
224
+ ragUsed = true;
225
+ parts.push('\n[CONTEXTO RELEVANTE — Busca Semântica]');
226
+ for (const r of relevant) {
227
+ const chunk = `\n--- ${r.reference_type} (${r.reference_id}) [relevância: ${Math.round(r.score * 100)}%] ---\n${r.text_chunk}`;
228
+ if (usedBudget + chunk.length > TOKEN_BUDGET) break;
229
+ parts.push(chunk);
230
+ usedBudget += chunk.length;
231
+ }
232
+ }
233
+ }
234
+ } catch (ragErr) {
235
+ console.error('[context] RAG search failed:', ragErr.message);
236
+ }
237
+
238
+ // 2. Fallback: if RAG not available or returned few results, include recent daily logs
239
+ if (!ragUsed || usedBudget < TOKEN_BUDGET / 3) {
240
+ try {
241
+ const logsDir = path.join(workspaceDir, 'logs', 'daily');
242
+ if (exists(logsDir)) {
243
+ const maxDays = ragUsed ? 3 : 5; // fewer if RAG already provided some context
244
+ const files = fs.readdirSync(logsDir)
245
+ .filter(f => /^\d{4}-\d{2}-\d{2}\.md$/.test(f))
246
+ .sort()
247
+ .slice(-maxDays);
248
+ if (files.length) {
249
+ parts.push('\n[DAILY LOGS — ÚLTIMOS ' + files.length + ' DIAS]');
250
+ for (const file of files) {
251
+ const date = file.replace('.md', '');
252
+ const content = fs.readFileSync(path.join(logsDir, file), 'utf8');
253
+ const maxPerLog = Math.floor((TOKEN_BUDGET - usedBudget) / files.length);
254
+ const trimmed = content.length > maxPerLog ? content.slice(0, maxPerLog) + '\n...(truncado)' : content;
255
+ parts.push(`\n--- LOG ${date} ---\n${trimmed}`);
256
+ usedBudget += trimmed.length;
257
+ if (usedBudget >= TOKEN_BUDGET) break;
258
+ }
259
+ }
260
+ }
261
+ } catch (e) {
262
+ console.error('[context] Failed to read daily logs:', e.message);
263
+ }
264
+ }
265
+
266
+ // 3. Always include structured data (compact, always useful)
267
+ parts.push(buildStructuredContext());
268
+
269
+ return parts.join('\n');
270
+ }
271
+
272
+ // ---------------------------------------------------------------------------
273
+ // Background auto-ingest from chat: extracts tasks/blockers from conversation
274
+ // ---------------------------------------------------------------------------
275
+ const INGEST_SIGNALS = /\b(criar|crie|registr|task|tarefa|blocker|impediment|problem|urgente|preciso|agendar|schedule|delegat|prioriz|adicionar?|anotar?|lembr|reminder|todo|pendente|pendência)\b/i;
276
+ const QUERY_ONLY = /^(o que|como|quando|qual|quais|quem|onde|por que|porque|quantos?|existe|tem |show|list|status|resumo|report|relatório|buscar?|search|find)/i;
277
+
278
+ async function backgroundIngestFromChat(workspaceDir, userQuery) {
279
+ // Skip pure queries — only ingest actionable messages
280
+ if (!userQuery || userQuery.length < 25) return;
281
+ if (QUERY_ONLY.test(userQuery.trim()) && !INGEST_SIGNALS.test(userQuery)) return;
282
+ if (!INGEST_SIGNALS.test(userQuery)) return;
283
+
284
+ try {
285
+ const cmd = process.env.COPILOT_CMD || 'copilot';
286
+
287
+ // Build a minimal planner prompt
288
+ const schema = {
289
+ actions: [
290
+ { type: 'create_task', description: '<string>', priority: 'HIGH|MEDIUM|LOW', category: 'DO_NOW|SCHEDULE|DELEGATE|IGNORE', projectSlug: '<string optional>' },
291
+ { type: 'create_blocker', title: '<string>', severity: 'CRITICAL|HIGH|MEDIUM|LOW', notes: '<string>', projectSlug: '<string optional>' }
292
+ ]
293
+ };
294
+ const prompt = `Você é o planner do sistema F.R.E.Y.A.\n\nAnalise o texto abaixo e extraia APENAS tarefas e blockers explícitos.\nSe NÃO houver tarefas ou blockers claros, retorne: {"actions":[]}\nRetorne APENAS JSON válido no formato: ${JSON.stringify(schema)}\nNÃO use code fences. NÃO inclua texto extra.\n\nTEXTO:\n${userQuery}\n`;
295
+
296
+ const agentEnv = { FREYA_WORKSPACE_DIR: workspaceDir };
297
+ const baseArgs = ['-s', '--no-color', '--stream', 'off', '-p', prompt];
298
+
299
+ const r = await run(cmd, baseArgs, workspaceDir, agentEnv);
300
+ const out = (r.stdout + r.stderr).trim();
301
+ if (r.code !== 0 || !out) return;
302
+
303
+ // Try to parse JSON plan
304
+ const jsonText = extractFirstJsonObject(out) || out;
305
+ let plan;
306
+ try {
307
+ plan = JSON.parse(jsonText);
308
+ } catch {
309
+ try { plan = JSON.parse(escapeJsonControlChars(jsonText)); } catch { return; }
310
+ }
311
+
312
+ const actions = Array.isArray(plan.actions) ? plan.actions : [];
313
+ const taskActions = actions.filter(a => a && a.type === 'create_task' && a.description);
314
+ const blockerActions = actions.filter(a => a && a.type === 'create_blocker' && a.title);
315
+
316
+ if (!taskActions.length && !blockerActions.length) return;
317
+
318
+ // Apply actions directly to SQLite
319
+ const slugMap = readProjectSlugMap(workspaceDir);
320
+ const validTaskCats = new Set(['DO_NOW', 'SCHEDULE', 'DELEGATE', 'IGNORE']);
321
+ const insertTask = dl.db.prepare(`INSERT INTO tasks (id, project_slug, description, category, status, metadata) VALUES (?, ?, ?, ?, ?, ?)`);
322
+ const insertBlocker = dl.db.prepare(`INSERT INTO blockers (id, project_slug, title, severity, status, owner, next_action, metadata) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`);
323
+
324
+ let tasksCreated = 0, blockersCreated = 0;
325
+ const embeddingQueue = [];
326
+
327
+ const ingestTx = dl.db.transaction(() => {
328
+ // Dedup check
329
+ const recentTasks = dl.db.prepare("SELECT description FROM tasks WHERE created_at >= datetime('now', '-1 day')").all();
330
+ const existingKeys = new Set(recentTasks.map(t => sha1(normalizeTextForKey(t.description))));
331
+ const recentBlockers = dl.db.prepare("SELECT title FROM blockers WHERE created_at >= datetime('now', '-1 day')").all();
332
+ const existingBKeys = new Set(recentBlockers.map(b => sha1(normalizeTextForKey(b.title))));
333
+
334
+ for (const a of taskActions) {
335
+ const desc = normalizeWhitespace(a.description);
336
+ if (!desc) continue;
337
+ const projectSlug = String(a.projectSlug || '').trim() || inferProjectSlug(desc, slugMap);
338
+ const key = sha1(normalizeTextForKey((projectSlug ? projectSlug + ' ' : '') + desc));
339
+ if (existingKeys.has(key)) continue;
340
+
341
+ const id = `t-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`;
342
+ const category = validTaskCats.has(String(a.category || '').trim()) ? String(a.category).trim() : 'DO_NOW';
343
+ const metadata = JSON.stringify({ priority: a.priority || 'medium' });
344
+ insertTask.run(id, projectSlug || null, desc, category, 'PENDING', metadata);
345
+ existingKeys.add(key);
346
+ tasksCreated++;
347
+ embeddingQueue.push({ type: 'task', id, text: desc });
348
+ }
349
+
350
+ for (const a of blockerActions) {
351
+ const title = normalizeWhitespace(a.title);
352
+ if (!title) continue;
353
+ const projectSlug = String(a.projectSlug || '').trim() || inferProjectSlug(title, slugMap);
354
+ const key = sha1(normalizeTextForKey((projectSlug ? projectSlug + ' ' : '') + title));
355
+ if (existingBKeys.has(key)) continue;
356
+
357
+ const id = `b-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`;
358
+ const severity = String(a.severity || 'MEDIUM').toUpperCase();
359
+ const metadata = JSON.stringify({ description: a.notes || title });
360
+ insertBlocker.run(id, projectSlug || null, title, severity, 'OPEN', null, null, metadata);
361
+ existingBKeys.add(key);
362
+ blockersCreated++;
363
+ embeddingQueue.push({ type: 'blocker', id, text: title + ' ' + (a.notes || '') });
364
+ }
365
+ });
366
+ ingestTx();
367
+
368
+ if (tasksCreated || blockersCreated) {
369
+ console.log(`[chat-ingest] Auto-ingested ${tasksCreated} tasks, ${blockersCreated} blockers from chat`);
370
+ // Generate embeddings in background
371
+ generateEmbeddingsBackground(workspaceDir, embeddingQueue).catch(() => {});
372
+ }
373
+ } catch (err) {
374
+ console.error('[chat-ingest] Background ingestion failed:', err.message);
375
+ }
376
+ }
377
+
101
378
  function settingsPath(workspaceDir) {
102
379
  return path.join(workspaceDir, 'data', 'settings', 'settings.json');
103
380
  }
@@ -2755,6 +3032,21 @@ async function cmdWeb({ port, dir, open, dev }) {
2755
3032
  await autoUpdate(wsDir);
2756
3033
  } catch { /* non-fatal */ }
2757
3034
 
3035
+ // Sync daily log .md files → SQLite daily_logs table on startup
3036
+ try {
3037
+ const { synced, toEmbed } = syncDailyLogs(wsDir);
3038
+ if (synced > 0) console.log(`[FREYA] Synced ${synced} daily logs to SQLite`);
3039
+ // Generate embeddings in background (non-blocking, last 30 days max)
3040
+ if (toEmbed.length > 0) {
3041
+ const recentLogs = toEmbed.slice(-30).map(l => ({ type: 'daily_log', id: l.date, text: l.content }));
3042
+ generateEmbeddingsBackground(wsDir, recentLogs).catch(err => {
3043
+ console.error('[FREYA] Embedding generation failed (non-fatal):', err.message);
3044
+ });
3045
+ }
3046
+ } catch (e) {
3047
+ console.error('[FREYA] Warning: daily-logs sync failed:', e.message || String(e));
3048
+ }
3049
+
2758
3050
  const host = '127.0.0.1';
2759
3051
 
2760
3052
  const server = http.createServer(async (req, res) => {
@@ -3736,6 +4028,17 @@ async function cmdWeb({ port, dir, open, dev }) {
3736
4028
  }
3737
4029
  }
3738
4030
 
4031
+ // Sync this daily log file to SQLite so chat/RAG can find it
4032
+ try {
4033
+ const logContent = fs.readFileSync(file, 'utf8');
4034
+ const upsert = dl.db.prepare(`INSERT INTO daily_logs (date, raw_markdown) VALUES (?, ?) ON CONFLICT(date) DO UPDATE SET raw_markdown = excluded.raw_markdown`);
4035
+ upsert.run(d, logContent);
4036
+ // Regenerate embeddings for this log in background
4037
+ generateEmbeddingsBackground(workspaceDir, [{ type: 'daily_log', id: d, text: logContent }]).catch(() => {});
4038
+ } catch (syncErr) {
4039
+ console.error('[inbox] Failed to sync daily log to SQLite:', syncErr.message);
4040
+ }
4041
+
3739
4042
  return safeJson(res, 200, { ok: true, file: path.relative(workspaceDir, file).replace(/\\/g, '/'), appended: true });
3740
4043
  }
3741
4044
 
@@ -4097,6 +4400,8 @@ async function cmdWeb({ port, dir, open, dev }) {
4097
4400
  insertTask.run(id, projectSlug || null, description, category, 'PENDING', metadata);
4098
4401
 
4099
4402
  applied.tasks++;
4403
+ if (!applied._embedQueue) applied._embedQueue = [];
4404
+ applied._embedQueue.push({ type: 'task', id, text: description });
4100
4405
  existingTaskKeys24h.add(key); // prevent duplicates within same batch
4101
4406
  continue;
4102
4407
  }
@@ -4117,6 +4422,8 @@ async function cmdWeb({ port, dir, open, dev }) {
4117
4422
  insertBlocker.run(id, projectSlug || null, title, severity, 'OPEN', null, null, metadata);
4118
4423
 
4119
4424
  applied.blockers++;
4425
+ if (!applied._embedQueue) applied._embedQueue = [];
4426
+ applied._embedQueue.push({ type: 'blocker', id, text: title + ' ' + (notes || '') });
4120
4427
  existingBlockerKeys24h.add(key); // prevent duplicates within same batch
4121
4428
  continue;
4122
4429
  }
@@ -4137,6 +4444,12 @@ async function cmdWeb({ port, dir, open, dev }) {
4137
4444
 
4138
4445
  applyTx(actions);
4139
4446
 
4447
+ // Generate embeddings for newly created tasks/blockers (background, non-blocking)
4448
+ if (applied._embedQueue && applied._embedQueue.length > 0) {
4449
+ generateEmbeddingsBackground(workspaceDir, applied._embedQueue).catch(() => {});
4450
+ delete applied._embedQueue; // don't send internal queue in response
4451
+ }
4452
+
4140
4453
  // Auto-suggest reports when planner didn't include any
4141
4454
  if (!applied.reportsSuggested.length) {
4142
4455
  const sug = [];
@@ -4277,20 +4590,11 @@ async function cmdWeb({ port, dir, open, dev }) {
4277
4590
  return `\n\n---\nFILE: ${rel}\n---\n` + fs.readFileSync(p, 'utf8');
4278
4591
  }).join('');
4279
4592
 
4280
- // V2 RAG Context (graceful fallback if embedder/sharp not available)
4281
- const dm = new DataManager(workspaceDir, path.join(workspaceDir, 'logs'));
4282
- let ragContext = '';
4283
- try {
4284
- const ragResults = await dm.semanticSearch(query, 12);
4285
- if (ragResults.length > 0) {
4286
- ragContext = '\n\n[MEMÓRIA DE LONGO PRAZO RECUPERADA (RAG VIA SQLITE)]\n';
4287
- for (const r of ragResults) {
4288
- ragContext += `\n---\nFONTE: ${r.reference_type} -> ID: ${r.reference_id} (Score: ${r.score.toFixed(3)})\nCONTEÚDO:\n${r.text_chunk}\n`;
4289
- }
4290
- }
4291
- } catch (ragErr) {
4292
- console.error('[oracle] RAG search failed (embedder/sharp unavailable), continuing without context:', ragErr.message);
4293
- }
4593
+ // Ensure daily logs are synced to SQLite before querying
4594
+ try { syncDailyLogs(workspaceDir); } catch { /* non-fatal */ }
4595
+
4596
+ // Build smart context: RAG (if embeddings exist) + fallback to raw logs + structured data
4597
+ const dataContext = await buildSmartContext(workspaceDir, query);
4294
4598
 
4295
4599
  // Build image context for the prompt (Copilot reads files via --allow-all-tools)
4296
4600
  let imageContext = '';
@@ -4301,8 +4605,23 @@ async function cmdWeb({ port, dir, open, dev }) {
4301
4605
  }
4302
4606
  }
4303
4607
 
4304
- // System instructions (small, always fits in -p)
4305
- const oracleSysInstr = `Você é o Orchestrator do sistema F.R.E.Y.A.\n\nVocê NÃO é o Oracle. Você é o agente principal que COORDENA todos os sub-agentes.\nSiga o fluxo definido no master.mdc: analise o intent do usuário, execute o plano internamente (Oracle para buscar dados, SM Agent para sintetizar), e SEMPRE retorne uma resposta FINAL em linguagem natural, estruturada e consultiva para o usuário.\n\nNUNCA exponha JSONs brutos, hierarquia interna de agentes, ou peça ao usuário para "chamar outro agente".\nVocê DEVE sintetizar os dados recuperados em uma resposta clara, organizada e útil.\n\nIdioma: Português do Brasil.\n${ragContext}${imageContext}`;
4608
+ // System instructions includes REAL data context so the Orchestrator
4609
+ // can synthesize answers without needing to "call" sub-agents at runtime
4610
+ const oracleSysInstr = `Você é FREYA — Assistente Responsiva com Otimização Aprimorada.
4611
+
4612
+ PAPEL: Você é o agente principal do sistema. Responda SEMPRE em linguagem natural, estruturada e consultiva.
4613
+
4614
+ REGRAS ABSOLUTAS:
4615
+ - NUNCA exponha JSONs brutos, nomes de agentes internos (Oracle, SM Agent, Ingestor), ou hierarquia de roteamento.
4616
+ - NUNCA peça ao usuário para "chamar outro agente" ou "invocar o Orchestrator".
4617
+ - NUNCA diga "como agente X, não posso...". Você é FREYA, um sistema único e coeso.
4618
+ - SEMPRE sintetize os dados abaixo em respostas úteis, organizadas e em português brasileiro.
4619
+ - Use a estrutura: Contexto → Análise → Recomendações → Próximos passos.
4620
+ - Termine com: — FREYA\\nAssistente Responsiva com Otimização Aprimorada
4621
+
4622
+ DADOS REAIS DO WORKSPACE (use estes dados para responder):
4623
+ ${dataContext}
4624
+ ${imageContext}`;
4306
4625
 
4307
4626
  const cmd = process.env.COPILOT_CMD || 'copilot';
4308
4627
 
@@ -4318,15 +4637,15 @@ async function cmdWeb({ port, dir, open, dev }) {
4318
4637
  }
4319
4638
  }
4320
4639
 
4321
- // ENAMETOOLONG fix: when prompt is large, write user query to temp file
4640
+ // ENAMETOOLONG fix: when prompt is large, write full prompt to temp file
4322
4641
  const fullOraclePrompt = `${oracleSysInstr}\n\nREGRAS:${rulesText}\n\nCONSULTA DO USUÁRIO:\n${query}\n`;
4323
4642
  const SAFE_ARG_LEN = 24000;
4324
4643
  let oracleTmpFile = null;
4325
4644
  let r;
4326
4645
  if (fullOraclePrompt.length > SAFE_ARG_LEN) {
4327
- oracleTmpFile = path.join(os.tmpdir(), `freya-oracle-input-${Date.now()}.txt`);
4328
- fs.writeFileSync(oracleTmpFile, query, 'utf8');
4329
- const filePrompt = `${oracleSysInstr}\n\nREGRAS:${rulesText}\n\nCONSULTA DO USUÁRIO:\nA consulta do usuário é grande e foi salva no arquivo abaixo. LEIA o conteúdo completo do arquivo e responda com base nele.\nARQUIVO: ${oracleTmpFile}\n\nIMPORTANTE: NÃO descreva o arquivo. LEIA e RESPONDA à consulta.\n`;
4646
+ oracleTmpFile = path.join(os.tmpdir(), `freya-orchestrator-${Date.now()}.txt`);
4647
+ fs.writeFileSync(oracleTmpFile, fullOraclePrompt, 'utf8');
4648
+ const filePrompt = `Leia o arquivo abaixo que contém suas instruções completas, regras, dados do workspace e a consulta do usuário. Siga TODAS as instruções contidas nele.\nARQUIVO: ${oracleTmpFile}\n\nIMPORTANTE: Leia o arquivo INTEIRO e responda à consulta do usuário que está no final do arquivo.`;
4330
4649
  copilotArgs.push('--add-dir', os.tmpdir());
4331
4650
  copilotArgs.push('--allow-all-tools', '-p', filePrompt);
4332
4651
  r = await run(cmd, copilotArgs, workspaceDir, oracleEnv);
@@ -4339,7 +4658,13 @@ async function cmdWeb({ port, dir, open, dev }) {
4339
4658
  if (r.code !== 0) {
4340
4659
  return safeJson(res, 200, { ok: false, answer: 'Falha no processamento do agente FREYA:\n' + (out || 'Exit code != 0'), sessionId });
4341
4660
  }
4342
- return safeJson(res, 200, { ok: true, answer: out, sessionId });
4661
+ // Send response immediately
4662
+ safeJson(res, 200, { ok: true, answer: out, sessionId });
4663
+ // Fire-and-forget: auto-ingest tasks/blockers from user message
4664
+ backgroundIngestFromChat(workspaceDir, query).catch(err => {
4665
+ console.error('[chat-ingest] Background failed:', err.message);
4666
+ });
4667
+ return;
4343
4668
  } catch (e) {
4344
4669
  return safeJson(res, 200, {
4345
4670
  ok: false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cccarv82/freya",
3
- "version": "3.5.1",
3
+ "version": "3.6.0",
4
4
  "description": "Personal AI Assistant with local-first persistence",
5
5
  "scripts": {
6
6
  "health": "node scripts/validate-data.js && node scripts/validate-structure.js",
@@ -325,6 +325,12 @@ class DataLayer {
325
325
  embedding BLOB NOT NULL, /* Stored as Buffer of Float32Array */
326
326
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP
327
327
  );
328
+
329
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_doc_emb_ref
330
+ ON document_embeddings(reference_type, reference_id, chunk_index);
331
+
332
+ CREATE INDEX IF NOT EXISTS idx_doc_emb_type
333
+ ON document_embeddings(reference_type);
328
334
  `);
329
335
 
330
336
  // --- Migrations for existing databases ---
@@ -229,6 +229,95 @@ class DataManager {
229
229
  return NaN;
230
230
  }
231
231
 
232
+ // --- Embedding Generation ---
233
+
234
+ /**
235
+ * Split text into chunks suitable for embedding (~400-600 chars each).
236
+ * Splits on markdown headings, then paragraphs, then sentences.
237
+ */
238
+ chunkText(text, maxChunkSize = 500) {
239
+ if (!text || text.length <= maxChunkSize) return [text].filter(Boolean);
240
+
241
+ const chunks = [];
242
+ // First split on markdown ## headings
243
+ const sections = text.split(/(?=^## )/m).filter(s => s.trim());
244
+
245
+ for (const section of sections) {
246
+ if (section.length <= maxChunkSize) {
247
+ chunks.push(section.trim());
248
+ continue;
249
+ }
250
+ // Split long sections on double newlines (paragraphs)
251
+ const paragraphs = section.split(/\n\n+/).filter(p => p.trim());
252
+ let buffer = '';
253
+ for (const para of paragraphs) {
254
+ if (buffer.length + para.length + 2 > maxChunkSize && buffer) {
255
+ chunks.push(buffer.trim());
256
+ buffer = '';
257
+ }
258
+ buffer += (buffer ? '\n\n' : '') + para;
259
+ }
260
+ if (buffer.trim()) chunks.push(buffer.trim());
261
+ }
262
+
263
+ return chunks.filter(c => c.length > 10); // skip tiny fragments
264
+ }
265
+
266
+ /**
267
+ * Generate embeddings for a piece of content and store in document_embeddings.
268
+ * Deletes existing embeddings for (referenceType, referenceId) first to avoid stale data.
269
+ * @param {string} referenceType - 'daily_log', 'task', or 'blocker'
270
+ * @param {string} referenceId - unique ID (date for logs, task/blocker id)
271
+ * @param {string} text - content to embed
272
+ */
273
+ async generateEmbeddings(referenceType, referenceId, text) {
274
+ if (!text || !text.trim()) return 0;
275
+
276
+ const chunks = this.chunkText(text);
277
+ if (!chunks.length) return 0;
278
+
279
+ // Delete existing embeddings for this reference
280
+ dl.db.prepare('DELETE FROM document_embeddings WHERE reference_type = ? AND reference_id = ?')
281
+ .run(referenceType, referenceId);
282
+
283
+ const insert = dl.db.prepare(`
284
+ INSERT INTO document_embeddings (reference_type, reference_id, chunk_index, text_chunk, embedding)
285
+ VALUES (?, ?, ?, ?, ?)
286
+ `);
287
+
288
+ let count = 0;
289
+ for (let i = 0; i < chunks.length; i++) {
290
+ try {
291
+ const vector = await defaultEmbedder.embedText(chunks[i]);
292
+ const buffer = defaultEmbedder.vectorToBuffer(vector);
293
+ insert.run(referenceType, referenceId, i, chunks[i], buffer);
294
+ count++;
295
+ } catch (err) {
296
+ console.error(`[embeddings] Failed to embed chunk ${i} of ${referenceType}/${referenceId}:`, err.message);
297
+ }
298
+ }
299
+ return count;
300
+ }
301
+
302
+ /**
303
+ * Check if embeddings exist and are up-to-date for a reference.
304
+ * @returns {boolean} true if embeddings exist
305
+ */
306
+ hasEmbeddings(referenceType, referenceId) {
307
+ const row = dl.db.prepare(
308
+ 'SELECT COUNT(*) as c FROM document_embeddings WHERE reference_type = ? AND reference_id = ?'
309
+ ).get(referenceType, referenceId);
310
+ return row && row.c > 0;
311
+ }
312
+
313
+ /**
314
+ * Get total embedding count (for checking if RAG is available).
315
+ */
316
+ getEmbeddingCount() {
317
+ const row = dl.db.prepare('SELECT COUNT(*) as c FROM document_embeddings').get();
318
+ return row ? row.c : 0;
319
+ }
320
+
232
321
  // --- RAG (Vector Search) ---
233
322
  async semanticSearch(query, topK = 10) {
234
323
  const queryVector = await defaultEmbedder.embedText(query);