neoagent 2.4.4-beta.0 → 2.4.4-beta.5

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.
Files changed (47) hide show
  1. package/README.md +5 -3
  2. package/docs/capabilities.md +16 -7
  3. package/docs/index.md +1 -0
  4. package/docs/security-boundaries.md +122 -0
  5. package/docs/supermemory-memory-review.md +852 -0
  6. package/flutter_app/lib/features/memory/views/retrieval_inspector_view.dart +128 -0
  7. package/flutter_app/lib/main.dart +3 -0
  8. package/flutter_app/lib/main_app_shell.dart +22 -0
  9. package/flutter_app/lib/main_controller.dart +36 -1
  10. package/flutter_app/lib/main_operations.dart +13 -0
  11. package/flutter_app/lib/main_security.dart +971 -0
  12. package/flutter_app/lib/main_settings.dart +61 -0
  13. package/flutter_app/lib/src/backend_client.dart +60 -3
  14. package/flutter_app/macos/Flutter/GeneratedPluginRegistrant.swift +2 -0
  15. package/flutter_app/pubspec.lock +32 -0
  16. package/flutter_app/pubspec.yaml +1 -0
  17. package/lib/schema_migrations.js +237 -0
  18. package/package.json +4 -2
  19. package/server/db/database.js +3 -0
  20. package/server/http/routes.js +2 -1
  21. package/server/public/.last_build_id +1 -1
  22. package/server/public/assets/NOTICES +86 -0
  23. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  24. package/server/public/flutter_bootstrap.js +1 -1
  25. package/server/public/main.dart.js +80911 -79117
  26. package/server/routes/memory.js +39 -2
  27. package/server/routes/security.js +112 -0
  28. package/server/services/ai/engine.js +267 -10
  29. package/server/services/ai/systemPrompt.js +13 -2
  30. package/server/services/cli/shell_worker.js +135 -0
  31. package/server/services/cli/shell_worker_pool.js +125 -0
  32. package/server/services/manager.js +20 -1
  33. package/server/services/memory/consolidation.js +111 -0
  34. package/server/services/memory/embedding_index.js +175 -0
  35. package/server/services/memory/embeddings.js +22 -2
  36. package/server/services/memory/evaluation.js +187 -0
  37. package/server/services/memory/ingestion_chunking.js +191 -0
  38. package/server/services/memory/ingestion_documents.js +96 -26
  39. package/server/services/memory/intelligence.js +3 -1
  40. package/server/services/memory/manager.js +855 -40
  41. package/server/services/memory/policy.js +0 -40
  42. package/server/services/memory/retrieval_reasoning.js +191 -0
  43. package/server/services/runtime/manager.js +7 -0
  44. package/server/services/security/approval_gate_service.js +93 -0
  45. package/server/services/security/tool_categories.js +105 -0
  46. package/server/services/security/tool_policy_service.js +92 -0
  47. package/server/services/security/tool_security_hook.js +77 -0
@@ -28,6 +28,14 @@ const transferImportLimiter = rateLimit({
28
28
  legacyHeaders: false,
29
29
  });
30
30
 
31
+ const memorySearchLimiter = rateLimit({
32
+ windowMs: 1 * 60 * 1000,
33
+ max: 60,
34
+ message: { error: 'Too many memory search requests, try again later' },
35
+ standardHeaders: true,
36
+ legacyHeaders: false,
37
+ });
38
+
31
39
  const MAX_TRANSFER_TEXT_BYTES = 60 * 1024;
32
40
 
33
41
  function normalizeMemoryIds(value) {
@@ -378,20 +386,49 @@ router.post('/memories/bulk-archive', (req, res) => {
378
386
  });
379
387
 
380
388
  // Semantic recall (search)
381
- router.post('/memories/recall', async (req, res) => {
389
+ router.post('/memories/recall', memorySearchLimiter, async (req, res) => {
382
390
  const mm = req.app.locals.memoryManager;
383
391
  const userId = req.session.userId;
384
392
  const agentId = resolveAgentId(userId, getAgentIdFromRequest(req));
385
393
  const { query, limit = 8 } = req.body;
386
394
  if (!query) return res.status(400).json({ error: 'query is required' });
395
+
396
+ let parsedLimit = Number.parseInt(limit, 10);
397
+ if (!Number.isFinite(parsedLimit) || Number.isNaN(parsedLimit)) {
398
+ return res.status(400).json({ error: 'limit must be an integer between 1 and 100' });
399
+ }
400
+ parsedLimit = Math.max(1, Math.min(100, parsedLimit));
401
+
387
402
  try {
388
- const results = await mm.recallMemory(userId, query, parseInt(limit), { agentId });
403
+ const results = await mm.recallMemory(userId, query, parsedLimit, { agentId });
389
404
  res.json(results);
390
405
  } catch (err) {
391
406
  res.status(500).json({ error: sanitizeError(err) });
392
407
  }
393
408
  });
394
409
 
410
+ // Semantic recall inspection (UI debug)
411
+ router.post('/memories/inspect', memorySearchLimiter, async (req, res) => {
412
+ const mm = req.app.locals.memoryManager;
413
+ const userId = req.session.userId;
414
+ const agentId = resolveAgentId(userId, getAgentIdFromRequest(req));
415
+ const { query, limit = 20 } = req.body;
416
+ if (!query) return res.status(400).json({ error: 'query is required' });
417
+
418
+ let parsedLimit = Number.parseInt(limit, 10);
419
+ if (!Number.isFinite(parsedLimit) || Number.isNaN(parsedLimit)) {
420
+ return res.status(400).json({ error: 'limit must be an integer between 1 and 100' });
421
+ }
422
+ parsedLimit = Math.max(1, Math.min(100, parsedLimit));
423
+
424
+ try {
425
+ const results = await mm.recallMemory(userId, query, parsedLimit, { agentId });
426
+ res.json({ results });
427
+ } catch (err) {
428
+ res.status(500).json({ error: sanitizeError(err) });
429
+ }
430
+ });
431
+
395
432
  // ─────────────────────────────────────────────────────────────────────────────
396
433
  // Core Memory
397
434
  // ─────────────────────────────────────────────────────────────────────────────
@@ -0,0 +1,112 @@
1
+ 'use strict';
2
+
3
+ const express = require('express');
4
+ const router = express.Router();
5
+ const { requireAuth } = require('../middleware/auth');
6
+ const db = require('../db/database');
7
+ const { TOOL_CATEGORIES, getCategoryForTool } = require('../services/security/tool_categories');
8
+
9
+ router.use(requireAuth);
10
+
11
+ // ── Security mode ─────────────────────────────────────────────────────────────
12
+
13
+ // GET /api/security/mode — current global security mode
14
+ router.get('/mode', (req, res) => {
15
+ const mode = req.app.locals.toolPolicyService.getSecurityMode(req.session.userId);
16
+ res.json({ mode });
17
+ });
18
+
19
+ // PUT /api/security/mode — update global security mode
20
+ router.put('/mode', (req, res) => {
21
+ const { mode } = req.body;
22
+ if (!mode) return res.status(400).json({ error: 'mode is required' });
23
+ try {
24
+ req.app.locals.toolPolicyService.setSecurityMode(req.session.userId, mode);
25
+ res.json({ ok: true, mode });
26
+ } catch (err) {
27
+ res.status(400).json({ error: err.message });
28
+ }
29
+ });
30
+
31
+ // ── Per-category policies ─────────────────────────────────────────────────────
32
+
33
+ // GET /api/security/policies — all category policies + current mode
34
+ router.get('/policies', (req, res) => {
35
+ const { toolPolicyService } = req.app.locals;
36
+ const policies = toolPolicyService.getPolicies(req.session.userId);
37
+ const mode = toolPolicyService.getSecurityMode(req.session.userId);
38
+ res.json({ policies, mode, categories: Object.keys(TOOL_CATEGORIES) });
39
+ });
40
+
41
+ // PUT /api/security/policies — update a single category policy
42
+ router.put('/policies', (req, res) => {
43
+ const { category, policy } = req.body;
44
+ if (!category || !policy) {
45
+ return res.status(400).json({ error: 'category and policy are required' });
46
+ }
47
+ try {
48
+ req.app.locals.toolPolicyService.setPolicy(req.session.userId, category, policy);
49
+ res.json({ ok: true, category, policy });
50
+ } catch (err) {
51
+ res.status(400).json({ error: err.message });
52
+ }
53
+ });
54
+
55
+ // ── Approval decisions ────────────────────────────────────────────────────────
56
+
57
+ // POST /api/security/approvals/:approvalId — resolve a pending approval
58
+ router.post('/approvals/:approvalId', (req, res) => {
59
+ const { approvalId } = req.params;
60
+ const { decision, scope, runId, toolName, toolArgs } = req.body;
61
+
62
+ if (!decision || !['approved', 'denied'].includes(decision)) {
63
+ return res.status(400).json({ error: 'decision must be approved or denied' });
64
+ }
65
+ const normalizedScope = ['once', 'session', 'always'].includes(scope) ? scope : 'once';
66
+
67
+ // 'always' scope: also persist the policy so this category is allowed going forward
68
+ if (decision === 'approved' && normalizedScope === 'always' && toolName) {
69
+ try {
70
+ const category = getCategoryForTool(toolName, toolArgs ?? {});
71
+ if (category) {
72
+ req.app.locals.toolPolicyService.setPolicy(req.session.userId, category, 'allow');
73
+ }
74
+ } catch {}
75
+ }
76
+
77
+ const { approvalGateService } = req.app.locals;
78
+ const resolved = approvalGateService.resolve(
79
+ approvalId,
80
+ req.session.userId,
81
+ runId || null,
82
+ toolName || null,
83
+ toolArgs || {},
84
+ decision,
85
+ normalizedScope,
86
+ );
87
+ if (!resolved) {
88
+ return res.status(404).json({ error: 'Approval not found or already resolved' });
89
+ }
90
+ res.json({ ok: true, approvalId, decision, scope: normalizedScope });
91
+ });
92
+
93
+ // ── Audit log ─────────────────────────────────────────────────────────────────
94
+
95
+ // GET /api/security/approval-log — paginated audit log
96
+ router.get('/approval-log', (req, res) => {
97
+ const limit = Math.min(Number(req.query.limit) || 50, 200);
98
+ const offset = Number(req.query.offset) || 0;
99
+ const rows = db.prepare(`
100
+ SELECT id, run_id, tool_name, tool_args_json, decision, scope, decided_at
101
+ FROM tool_approval_log
102
+ WHERE user_id = ?
103
+ ORDER BY decided_at DESC
104
+ LIMIT ? OFFSET ?
105
+ `).all(req.session.userId, limit, offset);
106
+ const total = db.prepare(
107
+ 'SELECT COUNT(*) as count FROM tool_approval_log WHERE user_id = ?'
108
+ ).get(req.session.userId)?.count ?? 0;
109
+ res.json({ log: rows, total, limit, offset });
110
+ });
111
+
112
+ module.exports = router;
@@ -76,6 +76,18 @@ const {
76
76
  inferToolFailureMessage,
77
77
  buildAutonomousRecoveryContext,
78
78
  } = require('./toolEvidence');
79
+ const {
80
+ buildMemoryConsolidationInstructions,
81
+ normalizeMemoryCandidates,
82
+ } = require('../memory/consolidation');
83
+ const {
84
+ buildPlannerPrompt,
85
+ buildRerankerPrompt,
86
+ mergeRetrievalResults,
87
+ normalizeRerankResult,
88
+ normalizeRetrievalPlan,
89
+ shouldEnhanceRetrieval,
90
+ } = require('../memory/retrieval_reasoning');
79
91
 
80
92
  function generateTitle(task) {
81
93
  if (!task || typeof task !== 'string') return 'Untitled';
@@ -341,6 +353,198 @@ class AgentEngine {
341
353
  };
342
354
  }
343
355
 
356
+ async buildMemoryRecall({
357
+ memoryManager,
358
+ userId,
359
+ agentId,
360
+ query,
361
+ provider,
362
+ providerName,
363
+ model,
364
+ runId,
365
+ stepId = null,
366
+ options = {},
367
+ returnDetails = false,
368
+ }) {
369
+ const initial = await memoryManager.recallMemory(userId, query, 12, { agentId });
370
+
371
+ const pendingChunks = memoryManager.getPendingExtractionChunks?.(userId, agentId, 5) || [];
372
+ if (pendingChunks.length) {
373
+ this.extractPendingChunks(pendingChunks, {
374
+ userId,
375
+ agentId,
376
+ provider,
377
+ providerName,
378
+ model,
379
+ memoryManager,
380
+ }).catch((err) => console.warn('[Memory] Background chunk extraction failed:', err.message));
381
+ }
382
+
383
+ const decision = shouldEnhanceRetrieval(initial);
384
+ if (!decision.enhance) {
385
+ const message = await memoryManager.buildRecallMessage(userId, query, {
386
+ agentId,
387
+ recalled: initial.slice(0, 5),
388
+ });
389
+ return returnDetails
390
+ ? { message, results: initial.slice(0, 12), enhanced: false, reason: decision.reason }
391
+ : message;
392
+ }
393
+
394
+ const stats = memoryManager.getMemoryStats?.(userId, { agentId })
395
+ || { total: initial.length };
396
+ if (!Number(stats.total || 0)) {
397
+ return returnDetails
398
+ ? { message: null, results: [], enhanced: false, reason: 'empty_memory' }
399
+ : null;
400
+ }
401
+
402
+ const startedAt = Date.now();
403
+ let plan = null;
404
+ let merged = initial;
405
+ let reranked = initial;
406
+ try {
407
+ const planned = await this.requestStructuredJson({
408
+ provider,
409
+ providerName,
410
+ model,
411
+ messages: [],
412
+ prompt: buildPlannerPrompt(query, initial, new Date().toISOString()),
413
+ maxTokens: 650,
414
+ normalize: (raw) => normalizeRetrievalPlan(raw, query),
415
+ fallback: normalizeRetrievalPlan({}, query),
416
+ reasoningEffort: this.getReasoningEffort(providerName, options),
417
+ telemetry: { runId, stepId, userId, agentId },
418
+ phase: 'memory_retrieval_plan',
419
+ });
420
+ plan = planned.value;
421
+ const resultSets = [initial];
422
+ for (const variant of plan.queryVariants) {
423
+ if (variant === query && initial.length) continue;
424
+ resultSets.push(await memoryManager.recallMemory(userId, variant, 20, {
425
+ agentId,
426
+ validAt: plan.validAt,
427
+ includeHistory: plan.temporalMode === 'historical',
428
+ }));
429
+ }
430
+ merged = mergeRetrievalResults(resultSets, 30);
431
+ if (merged.length > 1) {
432
+ const rerankResponse = await this.requestStructuredJson({
433
+ provider,
434
+ providerName,
435
+ model,
436
+ messages: [],
437
+ prompt: buildRerankerPrompt(query, plan, merged.slice(0, 24)),
438
+ maxTokens: 1200,
439
+ normalize: (raw) => normalizeRerankResult(raw, merged),
440
+ fallback: merged,
441
+ reasoningEffort: this.getReasoningEffort(providerName, options),
442
+ telemetry: { runId, stepId, userId, agentId },
443
+ phase: 'memory_retrieval_rerank',
444
+ });
445
+ reranked = rerankResponse.value;
446
+ } else {
447
+ reranked = merged;
448
+ }
449
+ } catch (error) {
450
+ console.warn('[Memory] Retrieval enhancement failed:', error.message);
451
+ plan = null;
452
+ merged = initial;
453
+ reranked = initial;
454
+ }
455
+
456
+ memoryManager.recordRetrievalEnhancement?.(userId, {
457
+ query,
458
+ reason: decision.reason,
459
+ plan,
460
+ initialCount: initial.length,
461
+ mergedCount: merged.length,
462
+ resultIds: reranked.slice(0, 5).map((result) => result.id),
463
+ latencyMs: Date.now() - startedAt,
464
+ }, { agentId, runId });
465
+
466
+ const message = await memoryManager.buildRecallMessage(userId, query, {
467
+ agentId,
468
+ recalled: reranked.slice(0, 5),
469
+ });
470
+ return returnDetails
471
+ ? {
472
+ message,
473
+ results: reranked.slice(0, 12),
474
+ enhanced: plan !== null,
475
+ reason: decision.reason,
476
+ plan,
477
+ }
478
+ : message;
479
+ }
480
+
481
+ async extractPendingChunks(chunks, {
482
+ userId,
483
+ agentId,
484
+ provider,
485
+ providerName,
486
+ model,
487
+ memoryManager,
488
+ }) {
489
+ const ids = chunks.map((c) => c.id);
490
+ memoryManager.markChunksExtracted?.(ids, { success: true });
491
+
492
+ const consolidationSchema = JSON.stringify({
493
+ memory_candidates: [{
494
+ memory: 'Concise standalone fact.',
495
+ subject: 'Canonical entity or person.',
496
+ predicate: 'Normalized relationship or attribute.',
497
+ object: 'Current atomic value.',
498
+ relation: 'new | updates | extends | derives',
499
+ category: 'identity | preferences | projects | contacts | events | tasks | episodic | assistant_self',
500
+ confidence: 0.8,
501
+ importance: 5,
502
+ is_static: false,
503
+ valid_from: null,
504
+ valid_to: null,
505
+ forget_after: null,
506
+ evidence: 'Short source-grounded quote.',
507
+ }],
508
+ }, null, 2);
509
+
510
+ for (const chunk of chunks) {
511
+ try {
512
+ const result = await this.requestStructuredJson({
513
+ provider,
514
+ providerName,
515
+ model,
516
+ messages: [],
517
+ prompt: [
518
+ 'Return JSON only. Extract durable memory facts from the document chunk below.',
519
+ buildMemoryConsolidationInstructions(new Date().toISOString()),
520
+ `Source type: ${chunk.sourceType || 'document'}`,
521
+ chunk.title ? `Document title: ${chunk.title}` : '',
522
+ `Content:\n${String(chunk.content || '').slice(0, 2400)}`,
523
+ `Schema:\n${consolidationSchema}`,
524
+ ].filter(Boolean).join('\n\n'),
525
+ maxTokens: 800,
526
+ normalize: (raw) => normalizeMemoryCandidates(raw?.memory_candidates || []),
527
+ fallback: [],
528
+ phase: 'document_extraction',
529
+ });
530
+
531
+ const candidates = Array.isArray(result.value) ? result.value : [];
532
+ if (candidates.length) {
533
+ await memoryManager.consolidateMemoryCandidates(userId, candidates, {
534
+ agentId,
535
+ metadata: {
536
+ trustLevel: 'external_source',
537
+ sourceChunkMemoryId: chunk.id,
538
+ },
539
+ });
540
+ }
541
+ } catch (err) {
542
+ memoryManager.markChunksExtracted?.([chunk.id], { success: false });
543
+ console.warn('[Memory] Document chunk extraction failed:', err.message);
544
+ }
545
+ }
546
+ }
547
+
344
548
  persistRunMetadata(runId, patch = {}) {
345
549
  if (!runId || !patch || typeof patch !== 'object') return;
346
550
  const existing = db.prepare('SELECT metadata_json FROM agent_runs WHERE id = ?').get(runId);
@@ -888,6 +1092,7 @@ class AgentEngine {
888
1092
 
889
1093
  async refreshConversationState({
890
1094
  conversationId,
1095
+ runId,
891
1096
  provider,
892
1097
  providerName,
893
1098
  model,
@@ -905,7 +1110,34 @@ class AgentEngine {
905
1110
  const promptMessages = [
906
1111
  {
907
1112
  role: 'system',
908
- content: 'Return JSON only. Distill the current thread working state. Keep it concise and concrete. Track summary, open_commitments, unresolved_questions, referenced_entities, and last_verified_facts. Do not invent facts.'
1113
+ content: [
1114
+ 'Return JSON only. Distill the current thread working state. Keep it concise and concrete.',
1115
+ 'Track summary, open_commitments, unresolved_questions, referenced_entities, and last_verified_facts. Do not invent facts.',
1116
+ buildMemoryConsolidationInstructions(new Date().toISOString()),
1117
+ 'Schema:',
1118
+ JSON.stringify({
1119
+ summary: '',
1120
+ open_commitments: [],
1121
+ unresolved_questions: [],
1122
+ referenced_entities: [],
1123
+ last_verified_facts: [],
1124
+ memory_candidates: [{
1125
+ memory: 'Concise standalone fact for future context.',
1126
+ subject: 'Canonical entity or person.',
1127
+ predicate: 'Normalized relationship or attribute.',
1128
+ object: 'Current atomic value.',
1129
+ relation: 'new | updates | extends | derives',
1130
+ category: 'identity | preferences | projects | contacts | events | tasks | episodic | assistant_self',
1131
+ confidence: 0.9,
1132
+ importance: 7,
1133
+ is_static: false,
1134
+ valid_from: null,
1135
+ valid_to: null,
1136
+ forget_after: null,
1137
+ evidence: 'Short source-grounded evidence.',
1138
+ }],
1139
+ }, null, 2),
1140
+ ].join('\n\n')
909
1141
  },
910
1142
  {
911
1143
  role: 'user',
@@ -943,6 +1175,20 @@ class AgentEngine {
943
1175
  }
944
1176
 
945
1177
  memoryManager.updateConversationState(conversationId, nextState);
1178
+ const memoryCandidates = normalizeMemoryCandidates(parsed.memory_candidates);
1179
+ if (memoryCandidates.length) {
1180
+ await memoryManager.consolidateMemoryCandidates(
1181
+ options.userId,
1182
+ memoryCandidates,
1183
+ {
1184
+ agentId: options.agentId || null,
1185
+ conversationId,
1186
+ runId,
1187
+ },
1188
+ );
1189
+ const { invalidateSystemPromptCache } = require('./systemPrompt');
1190
+ invalidateSystemPromptCache(options.userId, options.agentId || null);
1191
+ }
946
1192
  return nextState;
947
1193
  }
948
1194
 
@@ -1107,7 +1353,7 @@ class AgentEngine {
1107
1353
  toolArgs,
1108
1354
  stepIndex: nextStepIndex,
1109
1355
  blocked: true,
1110
- result: { status: 'skipped', reason: 'Blocked by policy.' },
1356
+ result: { status: 'blocked', reason: hookResult.reason || 'Blocked by policy.', blocked_by: hookResult.blocked_by || 'policy' },
1111
1357
  });
1112
1358
  continue;
1113
1359
  }
@@ -1726,7 +1972,17 @@ class AgentEngine {
1726
1972
  const recallQuery = options.context?.rawUserMessage || userMessage;
1727
1973
  const recallMsg = options.skipGlobalRecall === true
1728
1974
  ? null
1729
- : await memoryManager.buildRecallMessage(userId, recallQuery, { agentId });
1975
+ : await this.buildMemoryRecall({
1976
+ memoryManager,
1977
+ userId,
1978
+ agentId,
1979
+ query: recallQuery,
1980
+ provider,
1981
+ providerName,
1982
+ model,
1983
+ runId,
1984
+ options,
1985
+ });
1730
1986
 
1731
1987
  let summaryMessage = null;
1732
1988
  let historyMessages = [];
@@ -2428,13 +2684,14 @@ class AgentEngine {
2428
2684
  const hookCtx = { toolName, toolArgs, runId, userId, agentId, iteration };
2429
2685
  const hookResult = await globalHooks.run('before_tool_call', hookCtx);
2430
2686
  if (hookResult.block) {
2431
- console.warn(`[Run ${shortenRunId(runId)}] before_tool_call hook blocked tool=${toolName}`);
2432
- // Treat as a soft skip — add a skipped tool message so the model knows
2687
+ const blockReason = hookResult.reason || 'Blocked by policy.';
2688
+ const blockedBy = hookResult.blocked_by || 'policy';
2689
+ console.warn(`[Run ${shortenRunId(runId)}] before_tool_call hook blocked tool=${toolName} reason="${blockReason}"`);
2433
2690
  messages.push({
2434
2691
  role: 'tool',
2435
2692
  name: toolName,
2436
2693
  tool_call_id: toolCall.id,
2437
- content: JSON.stringify({ tool: toolName, status: 'skipped', reason: 'Blocked by policy.' }),
2694
+ content: JSON.stringify({ tool: toolName, status: 'blocked', reason: blockReason, blocked_by: blockedBy }),
2438
2695
  });
2439
2696
  continue;
2440
2697
  }
@@ -2857,8 +3114,9 @@ class AgentEngine {
2857
3114
  refreshConversationSummary(conversationId, provider, model, historyWindow).catch((err) => {
2858
3115
  console.error('[AI] Conversation summary refresh failed:', err.message);
2859
3116
  });
2860
- this.refreshConversationState({
3117
+ await this.refreshConversationState({
2861
3118
  conversationId,
3119
+ runId,
2862
3120
  provider,
2863
3121
  providerName,
2864
3122
  model,
@@ -2866,7 +3124,7 @@ class AgentEngine {
2866
3124
  analysis,
2867
3125
  verification,
2868
3126
  historyWindow,
2869
- options,
3127
+ options: { ...options, userId, agentId },
2870
3128
  }).catch((err) => {
2871
3129
  console.error('[AI] Conversation working state refresh failed:', err.message);
2872
3130
  });
@@ -3166,9 +3424,8 @@ class AgentEngine {
3166
3424
  let relevantMemories = [];
3167
3425
  try {
3168
3426
  relevantMemories = this.memoryManager
3169
- ? await this.memoryManager.recallMemory(userId, task, {
3427
+ ? await this.memoryManager.recallMemory(userId, task, 4, {
3170
3428
  agentId: options.agentId || null,
3171
- limit: 4,
3172
3429
  })
3173
3430
  : [];
3174
3431
  } catch {}
@@ -3,6 +3,13 @@ const os = require('os');
3
3
  const PROMPT_CACHE_TTL = 30_000;
4
4
  const promptCache = new Map();
5
5
 
6
+ function invalidateSystemPromptCache(userId, agentId = null) {
7
+ const prefix = `${String(userId || 'global')}:${String(agentId || 'main')}:`;
8
+ for (const key of promptCache.keys()) {
9
+ if (key.startsWith(prefix)) promptCache.delete(key);
10
+ }
11
+ }
12
+
6
13
  function clampSection(text, maxChars) {
7
14
  const str = String(text || '').trim();
8
15
  if (!str) return '';
@@ -284,7 +291,7 @@ async function buildSystemPromptSections(userId, context = {}, memoryManager) {
284
291
  }
285
292
 
286
293
  const memCtx = await memoryManager.buildContext(userId, { agentId });
287
- const compactMemory = clampSection(memCtx, 3200);
294
+ const compactMemory = clampSection(memCtx, 1600);
288
295
  if (compactMemory) {
289
296
  dynamic.push(compactMemory);
290
297
  }
@@ -344,4 +351,8 @@ async function buildSystemPrompt(userId, context = {}, memoryManager) {
344
351
  return [sections.stable, sections.dynamic].filter(Boolean).join('\n\n');
345
352
  }
346
353
 
347
- module.exports = { buildSystemPrompt, buildSystemPromptSections };
354
+ module.exports = {
355
+ buildSystemPrompt,
356
+ buildSystemPromptSections,
357
+ invalidateSystemPromptCache,
358
+ };