neoagent 2.4.4-beta.0 → 2.4.4-beta.11

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 (66) hide show
  1. package/.env.example +17 -0
  2. package/README.md +9 -3
  3. package/docs/capabilities.md +16 -7
  4. package/docs/configuration.md +1 -0
  5. package/docs/getting-started.md +6 -0
  6. package/docs/index.md +1 -0
  7. package/docs/security-boundaries.md +122 -0
  8. package/docs/supermemory-memory-review.md +852 -0
  9. package/flutter_app/lib/features/memory/views/retrieval_inspector_view.dart +128 -0
  10. package/flutter_app/lib/main.dart +3 -0
  11. package/flutter_app/lib/main_account_settings.dart +79 -15
  12. package/flutter_app/lib/main_app_shell.dart +22 -0
  13. package/flutter_app/lib/main_chat.dart +155 -8
  14. package/flutter_app/lib/main_controller.dart +74 -5
  15. package/flutter_app/lib/main_devices.dart +9 -3
  16. package/flutter_app/lib/main_models.dart +32 -0
  17. package/flutter_app/lib/main_operations.dart +13 -0
  18. package/flutter_app/lib/main_security.dart +967 -0
  19. package/flutter_app/lib/main_settings.dart +56 -0
  20. package/flutter_app/lib/src/backend_client.dart +60 -3
  21. package/flutter_app/macos/Flutter/GeneratedPluginRegistrant.swift +2 -0
  22. package/flutter_app/pubspec.lock +32 -0
  23. package/flutter_app/pubspec.yaml +1 -0
  24. package/lib/install_helpers.js +1 -0
  25. package/lib/manager.js +63 -1
  26. package/lib/schema_migrations.js +262 -0
  27. package/package.json +4 -2
  28. package/server/admin/admin.js +151 -0
  29. package/server/admin/index.html +55 -3
  30. package/server/db/database.js +3 -0
  31. package/server/http/routes.js +2 -1
  32. package/server/public/.last_build_id +1 -1
  33. package/server/public/assets/NOTICES +86 -0
  34. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  35. package/server/public/flutter_bootstrap.js +1 -1
  36. package/server/public/main.dart.js +82243 -80308
  37. package/server/routes/account.js +2 -23
  38. package/server/routes/admin.js +18 -2
  39. package/server/routes/agents.js +5 -1
  40. package/server/routes/memory.js +41 -4
  41. package/server/routes/security.js +112 -0
  42. package/server/services/account/service_email_settings.js +167 -0
  43. package/server/services/ai/engine.js +269 -27
  44. package/server/services/ai/rate_limits.js +150 -0
  45. package/server/services/ai/systemPrompt.js +26 -3
  46. package/server/services/ai/tools.js +11 -8
  47. package/server/services/cli/shell_worker.js +135 -0
  48. package/server/services/cli/shell_worker_pool.js +125 -0
  49. package/server/services/integrations/google/gmail.js +7 -7
  50. package/server/services/manager.js +20 -1
  51. package/server/services/memory/consolidation.js +111 -0
  52. package/server/services/memory/embedding_index.js +175 -0
  53. package/server/services/memory/embeddings.js +22 -2
  54. package/server/services/memory/evaluation.js +187 -0
  55. package/server/services/memory/ingestion_chunking.js +191 -0
  56. package/server/services/memory/ingestion_documents.js +96 -26
  57. package/server/services/memory/intelligence.js +3 -1
  58. package/server/services/memory/manager.js +855 -43
  59. package/server/services/memory/policy.js +0 -40
  60. package/server/services/memory/retrieval_reasoning.js +191 -0
  61. package/server/services/runtime/manager.js +7 -0
  62. package/server/services/security/approval_gate_service.js +93 -0
  63. package/server/services/security/tool_categories.js +105 -0
  64. package/server/services/security/tool_policy_service.js +92 -0
  65. package/server/services/security/tool_security_hook.js +77 -0
  66. package/server/services/websocket.js +5 -1
@@ -148,12 +148,24 @@ const _diagnosticsSettingsSection = _SettingsSection('diagnostics', <String>[
148
148
  'health',
149
149
  ]);
150
150
 
151
+ const _securitySettingsSection = _SettingsSection('security', <String>[
152
+ 'security',
153
+ 'tool',
154
+ 'permission',
155
+ 'allowlist',
156
+ 'shell',
157
+ 'android',
158
+ 'approval',
159
+ 'policy',
160
+ ]);
161
+
151
162
  const List<_SettingsSection> _settingsSearchSections = <_SettingsSection>[
152
163
  _overviewSettingsSection,
153
164
  _workspaceSettingsSection,
154
165
  _modelsSettingsSection,
155
166
  _voiceRecordingSettingsSection,
156
167
  _desktopSettingsSection,
168
+ _securitySettingsSection,
157
169
  _diagnosticsSettingsSection,
158
170
  ];
159
171
 
@@ -399,6 +411,13 @@ class _SettingsPanelState extends State<SettingsPanel> {
399
411
  _buildDesktopSection(controller),
400
412
  const SizedBox(height: 16),
401
413
  ],
414
+ if (_matchesSettingsSection(
415
+ searchQuery,
416
+ _securitySettingsSection,
417
+ )) ...<Widget>[
418
+ _buildSecuritySection(context, controller),
419
+ const SizedBox(height: 16),
420
+ ],
402
421
  if (_matchesSettingsSection(
403
422
  searchQuery,
404
423
  _diagnosticsSettingsSection,
@@ -1492,6 +1511,43 @@ class _SettingsPanelState extends State<SettingsPanel> {
1492
1511
  );
1493
1512
  }
1494
1513
 
1514
+ Widget _buildSecuritySection(BuildContext context, NeoAgentController controller) {
1515
+ return Card(
1516
+ child: Padding(
1517
+ padding: const EdgeInsets.all(20),
1518
+ child: Column(
1519
+ crossAxisAlignment: CrossAxisAlignment.start,
1520
+ children: <Widget>[
1521
+ const _SectionTitle('Security'),
1522
+ const SizedBox(height: 10),
1523
+ Text(
1524
+ 'Per-tool permission policies, approval gates, and process isolation for shell execution.',
1525
+ style: TextStyle(color: _textSecondary, height: 1.45),
1526
+ ),
1527
+ const SizedBox(height: 8),
1528
+ ListTile(
1529
+ contentPadding: EdgeInsets.zero,
1530
+ leading: Icon(Icons.checklist_outlined, color: _accentAlt),
1531
+ title: const Text('Tool Permissions'),
1532
+ subtitle: Text(
1533
+ 'Set block / ask / allow per tool category, or pick a global mode.',
1534
+ style: TextStyle(color: _textSecondary),
1535
+ ),
1536
+ trailing: const Icon(Icons.chevron_right),
1537
+ onTap: () {
1538
+ Navigator.of(context).push(
1539
+ MaterialPageRoute<void>(
1540
+ builder: (_) => MainSecurity(controller: controller),
1541
+ ),
1542
+ );
1543
+ },
1544
+ ),
1545
+ ],
1546
+ ),
1547
+ ),
1548
+ );
1549
+ }
1550
+
1495
1551
  Widget _buildDiagnosticsSection(NeoAgentController controller) {
1496
1552
  return Card(
1497
1553
  child: Padding(
@@ -1421,15 +1421,29 @@ class BackendClient {
1421
1421
  return getList(baseUrl, '/api/memory/memories$query');
1422
1422
  }
1423
1423
 
1424
- Future<List<Map<String, dynamic>>> recallMemories(
1424
+ Future<List<Map<String, dynamic>>> recallMemory(
1425
1425
  String baseUrl,
1426
1426
  String query, {
1427
+ int limit = 8,
1427
1428
  String? agentId,
1428
1429
  }) async {
1429
1430
  return postList(
1430
1431
  baseUrl,
1431
- '/api/memory/memories/recall',
1432
- _withAgentId(<String, dynamic>{'query': query, 'limit': 8}, agentId),
1432
+ _withAgentQuery('/api/memory/memories/recall', agentId),
1433
+ {'query': query, 'limit': limit},
1434
+ );
1435
+ }
1436
+
1437
+ Future<Map<String, dynamic>> inspectMemory(
1438
+ String baseUrl,
1439
+ String query, {
1440
+ int limit = 20,
1441
+ String? agentId,
1442
+ }) async {
1443
+ return postMap(
1444
+ baseUrl,
1445
+ _withAgentQuery('/api/memory/memories/inspect', agentId),
1446
+ {'query': query, 'limit': limit},
1433
1447
  );
1434
1448
  }
1435
1449
 
@@ -2004,6 +2018,49 @@ class BackendClient {
2004
2018
 
2005
2019
  throw BackendException(message, statusCode: response.statusCode);
2006
2020
  }
2021
+
2022
+ // ── Tool security ──────────────────────────────────────────────────────────
2023
+
2024
+ Future<Map<String, dynamic>> fetchSecurityPolicies(String baseUrl) async {
2025
+ return getMap(baseUrl, '/api/security/policies');
2026
+ }
2027
+
2028
+ Future<Map<String, dynamic>> fetchSecurityMode(String baseUrl) async {
2029
+ return getMap(baseUrl, '/api/security/mode');
2030
+ }
2031
+
2032
+ Future<Map<String, dynamic>> saveSecurityMode(String baseUrl, String mode) async {
2033
+ return putMap(baseUrl, '/api/security/mode', <String, dynamic>{'mode': mode});
2034
+ }
2035
+
2036
+ Future<Map<String, dynamic>> saveSecurityPolicy(
2037
+ String baseUrl, {
2038
+ required String category,
2039
+ required String policy,
2040
+ }) async {
2041
+ return putMap(baseUrl, '/api/security/policies', <String, dynamic>{
2042
+ 'category': category,
2043
+ 'policy': policy,
2044
+ });
2045
+ }
2046
+
2047
+ Future<Map<String, dynamic>> resolveToolApproval(
2048
+ String baseUrl, {
2049
+ required String approvalId,
2050
+ required String decision,
2051
+ required String scope,
2052
+ String? runId,
2053
+ String? toolName,
2054
+ Map<String, dynamic>? toolArgs,
2055
+ }) async {
2056
+ return postMap(baseUrl, '/api/security/approvals/$approvalId', <String, dynamic>{
2057
+ 'decision': decision,
2058
+ 'scope': scope,
2059
+ if (runId != null) 'runId': runId,
2060
+ if (toolName != null) 'toolName': toolName,
2061
+ if (toolArgs != null) 'toolArgs': toolArgs,
2062
+ });
2063
+ }
2007
2064
  }
2008
2065
 
2009
2066
  class BackendException implements Exception {
@@ -9,6 +9,7 @@ import audioplayers_darwin
9
9
  import connectivity_plus
10
10
  import desktop_audio_capture
11
11
  import file_picker
12
+ import flutter_local_notifications
12
13
  import flutter_secure_storage_macos
13
14
  import geolocator_apple
14
15
  import hotkey_manager_macos
@@ -29,6 +30,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
29
30
  ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin"))
30
31
  AudioCapturePlugin.register(with: registry.registrar(forPlugin: "AudioCapturePlugin"))
31
32
  FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
33
+ FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
32
34
  FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
33
35
  GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin"))
34
36
  HotkeyManagerMacosPlugin.register(with: registry.registrar(forPlugin: "HotkeyManagerMacosPlugin"))
@@ -277,6 +277,30 @@ packages:
277
277
  url: "https://pub.dev"
278
278
  source: hosted
279
279
  version: "5.0.0"
280
+ flutter_local_notifications:
281
+ dependency: "direct main"
282
+ description:
283
+ name: flutter_local_notifications
284
+ sha256: ef41ae901e7529e52934feba19ed82827b11baa67336829564aeab3129460610
285
+ url: "https://pub.dev"
286
+ source: hosted
287
+ version: "18.0.1"
288
+ flutter_local_notifications_linux:
289
+ dependency: transitive
290
+ description:
291
+ name: flutter_local_notifications_linux
292
+ sha256: "8f685642876742c941b29c32030f6f4f6dacd0e4eaecb3efbb187d6a3812ca01"
293
+ url: "https://pub.dev"
294
+ source: hosted
295
+ version: "5.0.0"
296
+ flutter_local_notifications_platform_interface:
297
+ dependency: transitive
298
+ description:
299
+ name: flutter_local_notifications_platform_interface
300
+ sha256: "6c5b83c86bf819cdb177a9247a3722067dd8cc6313827ce7c77a4b238a26fd52"
301
+ url: "https://pub.dev"
302
+ source: hosted
303
+ version: "8.0.0"
280
304
  flutter_markdown:
281
305
  dependency: "direct main"
282
306
  description:
@@ -1035,6 +1059,14 @@ packages:
1035
1059
  url: "https://pub.dev"
1036
1060
  source: hosted
1037
1061
  version: "0.7.11"
1062
+ timezone:
1063
+ dependency: transitive
1064
+ description:
1065
+ name: timezone
1066
+ sha256: dd14a3b83cfd7cb19e7888f1cbc20f258b8d71b54c06f79ac585f14093a287d1
1067
+ url: "https://pub.dev"
1068
+ source: hosted
1069
+ version: "0.10.1"
1038
1070
  tray_manager:
1039
1071
  dependency: "direct main"
1040
1072
  description:
@@ -39,6 +39,7 @@ dependencies:
39
39
  url_launcher: ^6.3.2
40
40
  video_player: ^2.9.2
41
41
  flutter_animate: ^4.5.2
42
+ flutter_local_notifications: ^18.0.0
42
43
 
43
44
  dependency_overrides:
44
45
  flutter_secure_storage_linux:
@@ -5,6 +5,7 @@ function withInstallEnv(extraEnv = {}) {
5
5
  return {
6
6
  ...process.env,
7
7
  PUPPETEER_SKIP_DOWNLOAD: process.env.PUPPETEER_SKIP_DOWNLOAD || 'true',
8
+ PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: process.env.PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD || '1',
8
9
  ...extraEnv,
9
10
  };
10
11
  }
package/lib/manager.js CHANGED
@@ -1593,7 +1593,12 @@ async function cmdInstall() {
1593
1593
  if (platform === 'macos' && commandExists('launchctl')) {
1594
1594
  installMacService();
1595
1595
  } else if (platform === 'linux' && commandExists('systemctl')) {
1596
- installLinuxService();
1596
+ try {
1597
+ installLinuxService();
1598
+ } catch (err) {
1599
+ logWarn(`systemd setup failed (${err.message}); falling back to detached process`);
1600
+ startFallback();
1601
+ }
1597
1602
  } else {
1598
1603
  startFallback();
1599
1604
  }
@@ -1693,6 +1698,59 @@ async function cmdRebuildWeb() {
1693
1698
  buildBundledWebClientIfPossible();
1694
1699
  }
1695
1700
 
1701
+ async function cmdFix() {
1702
+ cliBanner(`Fix ${APP_NAME}`, 'reset and recover');
1703
+ heading('Backup');
1704
+ backupRuntimeData();
1705
+ logOk(`Runtime data backed up to ${path.join(RUNTIME_HOME, 'backups')}`);
1706
+
1707
+ heading('Stop');
1708
+ cmdStop();
1709
+
1710
+ if (fs.existsSync(path.join(APP_DIR, '.git')) && commandExists('git')) {
1711
+ heading('Reset source files');
1712
+ const dirty = runQuiet('git', ['status', '--porcelain']);
1713
+ if (dirty.status === 0 && dirty.stdout.trim()) {
1714
+ runOrThrow('git', ['checkout', '--', '.']);
1715
+ logOk('Tracked source files reset to HEAD');
1716
+ } else {
1717
+ logOk('Working tree clean — nothing to reset');
1718
+ }
1719
+ }
1720
+
1721
+ heading('Dependencies');
1722
+ const nodeModulesDir = path.join(APP_DIR, 'node_modules');
1723
+ if (fs.existsSync(nodeModulesDir)) {
1724
+ logInfo('Removing node_modules…');
1725
+ fs.rmSync(nodeModulesDir, { recursive: true, force: true });
1726
+ logOk('node_modules removed');
1727
+ }
1728
+ installDependencies();
1729
+
1730
+ heading('Web Client');
1731
+ buildBundledWebClientIfPossible({ required: true });
1732
+
1733
+ heading('Start');
1734
+ const platform = detectPlatform();
1735
+ if (platform === 'macos' && commandExists('launchctl')) {
1736
+ installMacService();
1737
+ } else if (platform === 'linux' && commandExists('systemctl')) {
1738
+ try {
1739
+ installLinuxService();
1740
+ } catch (err) {
1741
+ logWarn(`systemd setup failed (${err.message}); falling back to detached process`);
1742
+ startFallback();
1743
+ }
1744
+ } else {
1745
+ startFallback();
1746
+ }
1747
+
1748
+ const port = loadEnvPort();
1749
+ logOk(`Running on http://localhost:${port}`);
1750
+ heading('Ready');
1751
+ logInfo('Fix complete. Run `neoagent status` to verify.');
1752
+ }
1753
+
1696
1754
  function cmdUninstall() {
1697
1755
  heading(`Uninstall ${APP_NAME}`);
1698
1756
  const platform = detectPlatform();
@@ -1962,6 +2020,7 @@ function printHelp() {
1962
2020
  row('restart', 'Stop, then start');
1963
2021
  row('status', 'Health overview (server, service, config)');
1964
2022
  row('logs', 'Tail server logs');
2023
+ row('fix', 'Backup, reset source, reinstall deps, restart');
1965
2024
  row('uninstall', 'Remove the system service');
1966
2025
  console.log('');
1967
2026
 
@@ -2035,6 +2094,9 @@ async function runCLI(argv) {
2035
2094
  case 'logs':
2036
2095
  cmdLogs();
2037
2096
  break;
2097
+ case 'fix':
2098
+ await cmdFix();
2099
+ break;
2038
2100
  case 'uninstall':
2039
2101
  cmdUninstall();
2040
2102
  break;
@@ -0,0 +1,262 @@
1
+ 'use strict';
2
+
3
+ function migrateMemoryEmbeddingIndex(db) {
4
+ db.exec(`
5
+ CREATE TABLE IF NOT EXISTS memory_embedding_bands (
6
+ memory_id TEXT NOT NULL,
7
+ user_id INTEGER NOT NULL,
8
+ agent_id TEXT NOT NULL,
9
+ dimension INTEGER NOT NULL,
10
+ index_version INTEGER NOT NULL DEFAULT 3,
11
+ band_index INTEGER NOT NULL,
12
+ band_value INTEGER NOT NULL,
13
+ updated_at TEXT DEFAULT (datetime('now')),
14
+ PRIMARY KEY (memory_id, band_index),
15
+ FOREIGN KEY (memory_id) REFERENCES memories(id) ON DELETE CASCADE,
16
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
17
+ );
18
+
19
+ CREATE INDEX IF NOT EXISTS idx_memory_embedding_bands_lookup
20
+ ON memory_embedding_bands(
21
+ user_id,
22
+ agent_id,
23
+ dimension,
24
+ band_index,
25
+ band_value
26
+ );
27
+ `);
28
+ const columns = new Set(
29
+ db.prepare('PRAGMA table_info(memory_embedding_bands)').all()
30
+ .map((column) => column.name),
31
+ );
32
+ if (!columns.has('index_version')) {
33
+ db.exec('ALTER TABLE memory_embedding_bands ADD COLUMN index_version INTEGER');
34
+ }
35
+ db.exec(`
36
+ DROP INDEX IF EXISTS idx_memory_embedding_bands_lookup;
37
+ CREATE INDEX idx_memory_embedding_bands_lookup
38
+ ON memory_embedding_bands(
39
+ user_id,
40
+ agent_id,
41
+ dimension,
42
+ index_version,
43
+ band_index,
44
+ band_value
45
+ );
46
+ `);
47
+ }
48
+
49
+ function migrateMemoryProvenance(db) {
50
+ db.exec(`
51
+ CREATE TABLE IF NOT EXISTS memory_source_chunks (
52
+ id TEXT PRIMARY KEY,
53
+ document_id TEXT NOT NULL,
54
+ user_id INTEGER NOT NULL,
55
+ agent_id TEXT NOT NULL,
56
+ chunk_index INTEGER NOT NULL,
57
+ char_start INTEGER NOT NULL,
58
+ char_end INTEGER NOT NULL,
59
+ content TEXT NOT NULL,
60
+ content_hash TEXT NOT NULL,
61
+ metadata_json TEXT DEFAULT '{}',
62
+ created_at TEXT DEFAULT (datetime('now')),
63
+ updated_at TEXT DEFAULT (datetime('now')),
64
+ FOREIGN KEY (document_id) REFERENCES memory_ingestion_documents(id) ON DELETE CASCADE,
65
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
66
+ UNIQUE(document_id, content_hash)
67
+ );
68
+
69
+ CREATE TABLE IF NOT EXISTS memory_source_links (
70
+ memory_id TEXT NOT NULL,
71
+ chunk_id TEXT NOT NULL,
72
+ source_document_id TEXT NOT NULL,
73
+ source_timestamp TEXT,
74
+ relevance_score REAL DEFAULT 1,
75
+ extraction_method TEXT DEFAULT 'source_chunk',
76
+ metadata_json TEXT DEFAULT '{}',
77
+ created_at TEXT DEFAULT (datetime('now')),
78
+ PRIMARY KEY (memory_id, chunk_id),
79
+ FOREIGN KEY (memory_id) REFERENCES memories(id) ON DELETE CASCADE,
80
+ FOREIGN KEY (chunk_id) REFERENCES memory_source_chunks(id) ON DELETE CASCADE,
81
+ FOREIGN KEY (source_document_id) REFERENCES memory_ingestion_documents(id) ON DELETE CASCADE
82
+ );
83
+
84
+ CREATE INDEX IF NOT EXISTS idx_memory_source_chunks_document
85
+ ON memory_source_chunks(document_id, chunk_index);
86
+ CREATE INDEX IF NOT EXISTS idx_memory_source_links_memory
87
+ ON memory_source_links(memory_id);
88
+ CREATE INDEX IF NOT EXISTS idx_memory_source_links_document
89
+ ON memory_source_links(source_document_id);
90
+ `);
91
+ }
92
+
93
+ function migrateMemoryRelations(db) {
94
+ db.exec(`
95
+ CREATE TABLE IF NOT EXISTS memory_relations (
96
+ id TEXT PRIMARY KEY,
97
+ user_id INTEGER NOT NULL,
98
+ agent_id TEXT,
99
+ from_memory_id TEXT NOT NULL,
100
+ to_memory_id TEXT NOT NULL,
101
+ relation_type TEXT NOT NULL CHECK(relation_type IN ('updates', 'extends', 'derives')),
102
+ confidence REAL DEFAULT 0.7,
103
+ metadata_json TEXT DEFAULT '{}',
104
+ created_at TEXT DEFAULT (datetime('now')),
105
+ UNIQUE(from_memory_id, to_memory_id, relation_type),
106
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
107
+ FOREIGN KEY (agent_id) REFERENCES agents(id) ON DELETE SET NULL,
108
+ FOREIGN KEY (from_memory_id) REFERENCES memories(id) ON DELETE CASCADE,
109
+ FOREIGN KEY (to_memory_id) REFERENCES memories(id) ON DELETE CASCADE
110
+ );
111
+
112
+ CREATE INDEX IF NOT EXISTS idx_memory_relations_from
113
+ ON memory_relations(user_id, agent_id, from_memory_id);
114
+ CREATE INDEX IF NOT EXISTS idx_memory_relations_to
115
+ ON memory_relations(user_id, agent_id, to_memory_id);
116
+ `);
117
+ }
118
+
119
+ function migrateMemoryRetrievalEvents(db) {
120
+ db.exec(`
121
+ CREATE TABLE IF NOT EXISTS memory_retrieval_events (
122
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
123
+ user_id INTEGER NOT NULL,
124
+ agent_id TEXT NOT NULL,
125
+ query_hash TEXT NOT NULL,
126
+ scope_type TEXT NOT NULL,
127
+ scope_id TEXT,
128
+ requested_k INTEGER NOT NULL,
129
+ candidate_count INTEGER DEFAULT 0,
130
+ semantic_candidate_count INTEGER DEFAULT 0,
131
+ lexical_candidate_count INTEGER DEFAULT 0,
132
+ entity_candidate_count INTEGER DEFAULT 0,
133
+ relation_candidate_count INTEGER DEFAULT 0,
134
+ result_count INTEGER DEFAULT 0,
135
+ result_ids_json TEXT DEFAULT '[]',
136
+ context_tokens_estimate INTEGER DEFAULT 0,
137
+ latency_ms REAL DEFAULT 0,
138
+ created_at TEXT DEFAULT (datetime('now')),
139
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
140
+ );
141
+
142
+ CREATE INDEX IF NOT EXISTS idx_memory_retrieval_events_user
143
+ ON memory_retrieval_events(user_id, agent_id, created_at DESC);
144
+
145
+ CREATE TABLE IF NOT EXISTS memory_retrieval_enhancements (
146
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
147
+ user_id INTEGER NOT NULL,
148
+ agent_id TEXT NOT NULL,
149
+ run_id TEXT,
150
+ query_hash TEXT NOT NULL,
151
+ trigger_reason TEXT NOT NULL,
152
+ plan_json TEXT,
153
+ initial_count INTEGER DEFAULT 0,
154
+ merged_count INTEGER DEFAULT 0,
155
+ result_ids_json TEXT DEFAULT '[]',
156
+ latency_ms REAL DEFAULT 0,
157
+ created_at TEXT DEFAULT (datetime('now')),
158
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
159
+ FOREIGN KEY (run_id) REFERENCES agent_runs(id) ON DELETE SET NULL
160
+ );
161
+
162
+ CREATE INDEX IF NOT EXISTS idx_memory_retrieval_enhancements_user
163
+ ON memory_retrieval_enhancements(user_id, agent_id, created_at DESC);
164
+ `);
165
+ }
166
+
167
+ function migrateMemoryEmbeddingMetadata(db) {
168
+ const columns = new Set(
169
+ db.prepare('PRAGMA table_info(memories)').all().map((column) => column.name),
170
+ );
171
+ const additions = [
172
+ ['embedding_provider', 'TEXT'],
173
+ ['embedding_model', 'TEXT'],
174
+ ['embedding_dimensions', 'INTEGER'],
175
+ ['embedded_at', 'TEXT'],
176
+ ];
177
+ for (const [name, type] of additions) {
178
+ if (!columns.has(name)) {
179
+ db.exec(`ALTER TABLE memories ADD COLUMN ${name} ${type}`);
180
+ }
181
+ }
182
+ try {
183
+ db.exec(`
184
+ UPDATE memories
185
+ SET embedding_dimensions = json_array_length(embedding),
186
+ embedding_provider = COALESCE(embedding_provider, 'legacy_unknown')
187
+ WHERE embedding IS NOT NULL AND embedding_dimensions IS NULL
188
+ `);
189
+ } catch {
190
+ // JSON1 may be unavailable. New writes still carry complete metadata.
191
+ }
192
+ }
193
+
194
+ function migrateToolPermissions(db) {
195
+ db.exec(`
196
+ CREATE TABLE IF NOT EXISTS tool_policies (
197
+ user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
198
+ category TEXT NOT NULL,
199
+ policy TEXT NOT NULL CHECK(policy IN ('allow','deny','require_approval','allow_always')),
200
+ updated_at TEXT DEFAULT (datetime('now')),
201
+ PRIMARY KEY (user_id, category)
202
+ );
203
+
204
+ CREATE TABLE IF NOT EXISTS tool_approval_log (
205
+ id TEXT PRIMARY KEY,
206
+ user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
207
+ run_id TEXT NOT NULL,
208
+ tool_name TEXT NOT NULL,
209
+ tool_args_json TEXT,
210
+ decision TEXT NOT NULL CHECK(decision IN ('approved','denied','timeout')),
211
+ scope TEXT NOT NULL CHECK(scope IN ('once','session','always')),
212
+ decided_at TEXT DEFAULT (datetime('now'))
213
+ );
214
+
215
+ CREATE INDEX IF NOT EXISTS idx_approval_log_user
216
+ ON tool_approval_log(user_id, decided_at DESC);
217
+ `);
218
+ }
219
+
220
+ function migrateToolPoliciesAllowAlways(db) {
221
+ // SQLite doesn't support ALTER COLUMN — recreate the table to add 'allow_always'
222
+ // to the CHECK constraint on existing installations.
223
+ const tableInfo = db.prepare("PRAGMA table_info(tool_policies)").all();
224
+ if (!tableInfo.length) return; // table doesn't exist yet; migrateToolPermissions will create it correctly
225
+ const checkRow = db.prepare(
226
+ "SELECT sql FROM sqlite_master WHERE type='table' AND name='tool_policies'",
227
+ ).get();
228
+ if (checkRow && checkRow.sql.includes("'allow_always'")) return; // already migrated
229
+ db.exec(`
230
+ CREATE TABLE IF NOT EXISTS tool_policies_v2 (
231
+ user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
232
+ category TEXT NOT NULL,
233
+ policy TEXT NOT NULL CHECK(policy IN ('allow','deny','require_approval','allow_always')),
234
+ updated_at TEXT DEFAULT (datetime('now')),
235
+ PRIMARY KEY (user_id, category)
236
+ );
237
+ INSERT OR IGNORE INTO tool_policies_v2 SELECT * FROM tool_policies;
238
+ DROP TABLE tool_policies;
239
+ ALTER TABLE tool_policies_v2 RENAME TO tool_policies;
240
+ `);
241
+ }
242
+
243
+ function runSchemaMigrations(db) {
244
+ migrateMemoryEmbeddingIndex(db);
245
+ migrateMemoryProvenance(db);
246
+ migrateMemoryRelations(db);
247
+ migrateMemoryRetrievalEvents(db);
248
+ migrateMemoryEmbeddingMetadata(db);
249
+ migrateToolPermissions(db);
250
+ migrateToolPoliciesAllowAlways(db);
251
+ }
252
+
253
+ module.exports = {
254
+ migrateMemoryEmbeddingIndex,
255
+ migrateMemoryProvenance,
256
+ migrateMemoryRelations,
257
+ migrateMemoryRetrievalEvents,
258
+ migrateMemoryEmbeddingMetadata,
259
+ migrateToolPermissions,
260
+ migrateToolPoliciesAllowAlways,
261
+ runSchemaMigrations,
262
+ };
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "neoagent",
3
- "version": "2.4.4-beta.0",
3
+ "version": "2.4.4-beta.11",
4
4
  "description": "Proactive personal AI agent with no limits",
5
5
  "license": "AGPL-3.0-only",
6
6
  "main": "server/index.js",
7
7
  "engines": {
8
- "node": ">=20"
8
+ "node": ">=18.0.0"
9
9
  },
10
10
  "bin": {
11
11
  "neoagent": "bin/neoagent.js"
@@ -52,6 +52,8 @@
52
52
  "flutter:test:unit": "cd flutter_app && flutter test ../test/flutter/unit",
53
53
  "flutter:test:widget": "cd flutter_app && flutter test ../test/flutter/widget",
54
54
  "benchmark:tokens": "node scripts/benchmark-token-cost.js",
55
+ "benchmark:memory": "node scripts/benchmark-memory.js",
56
+ "benchmark:memorybench:install": "node scripts/install-memorybench-provider.js",
55
57
  "release": "npx semantic-release"
56
58
  },
57
59
  "repository": {