neoagent 2.4.4-beta.8 → 2.5.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.
Files changed (52) hide show
  1. package/.env.example +17 -0
  2. package/README.md +64 -57
  3. package/docs/agent-run-lifecycle.md +65 -0
  4. package/docs/agents-and-users.md +48 -0
  5. package/docs/architecture.md +59 -0
  6. package/docs/automation-architecture.md +50 -0
  7. package/docs/automation.md +54 -70
  8. package/docs/capabilities.md +16 -118
  9. package/docs/clients-and-devices.md +43 -0
  10. package/docs/configuration.md +104 -116
  11. package/docs/development.md +82 -0
  12. package/docs/devices.md +62 -0
  13. package/docs/getting-started.md +47 -57
  14. package/docs/index.md +41 -30
  15. package/docs/integrations-architecture.md +48 -0
  16. package/docs/integrations.md +56 -51
  17. package/docs/memory-architecture.md +90 -0
  18. package/docs/memory.md +78 -0
  19. package/docs/migration.md +12 -5
  20. package/docs/models.md +64 -0
  21. package/docs/operations.md +46 -30
  22. package/docs/persistence.md +49 -0
  23. package/docs/recordings-and-health.md +41 -0
  24. package/docs/runtime-and-tools.md +52 -0
  25. package/docs/security-boundaries.md +65 -111
  26. package/docs/skills.md +32 -45
  27. package/docs/why-neoagent.md +47 -16
  28. package/flutter_app/lib/main_account_settings.dart +79 -15
  29. package/flutter_app/lib/main_chat.dart +155 -8
  30. package/flutter_app/lib/main_controller.dart +38 -4
  31. package/flutter_app/lib/main_devices.dart +9 -3
  32. package/flutter_app/lib/main_models.dart +32 -0
  33. package/landing/index.html +0 -4
  34. package/lib/install_helpers.js +1 -0
  35. package/lib/manager.js +63 -1
  36. package/package.json +2 -2
  37. package/server/admin/admin.js +151 -0
  38. package/server/admin/index.html +55 -3
  39. package/server/public/.last_build_id +1 -1
  40. package/server/public/flutter_bootstrap.js +1 -1
  41. package/server/public/main.dart.js +50995 -50850
  42. package/server/routes/account.js +2 -23
  43. package/server/routes/admin.js +18 -2
  44. package/server/routes/agents.js +5 -1
  45. package/server/routes/memory.js +2 -2
  46. package/server/services/account/service_email_settings.js +167 -0
  47. package/server/services/ai/engine.js +2 -17
  48. package/server/services/ai/rate_limits.js +150 -0
  49. package/server/services/ai/tools.js +11 -8
  50. package/server/services/memory/manager.js +0 -3
  51. package/server/services/websocket.js +5 -1
  52. package/docs/supermemory-memory-review.md +0 -852
@@ -38,6 +38,7 @@ const {
38
38
  approveChallenge,
39
39
  resolveChallengeForApproval,
40
40
  } = require('../services/account/qr_login');
41
+ const { getRateLimitSnapshot } = require('../services/ai/rate_limits');
41
42
 
42
43
  const accountLimiter = rateLimit({
43
44
  windowMs: 15 * 60 * 1000,
@@ -112,29 +113,7 @@ router.get('/', (req, res) => {
112
113
 
113
114
  router.get('/usage', (req, res) => {
114
115
  try {
115
- const userId = req.session.userId;
116
- const userLimits = db.prepare('SELECT rate_limit_4h, rate_limit_weekly FROM users WHERE id = ?').get(userId);
117
- const h4Tokens = db.prepare("SELECT COALESCE(SUM(total_tokens), 0) as t FROM agent_runs WHERE user_id = ? AND created_at > datetime('now', '-4 hours')").get(userId).t;
118
- const weeklyTokens = db.prepare("SELECT COALESCE(SUM(total_tokens), 0) as t FROM agent_runs WHERE user_id = ? AND created_at > datetime('now', '-7 days')").get(userId).t;
119
-
120
- const globalLimit4h = process.env.NEOAGENT_RATE_LIMIT_4H ? parseInt(process.env.NEOAGENT_RATE_LIMIT_4H, 10) : null;
121
- const globalLimitWeekly = process.env.NEOAGENT_RATE_LIMIT_WEEKLY ? parseInt(process.env.NEOAGENT_RATE_LIMIT_WEEKLY, 10) : null;
122
-
123
- const custom4h = userLimits?.rate_limit_4h ?? null;
124
- const customWeekly = userLimits?.rate_limit_weekly ?? null;
125
-
126
- res.json({
127
- limits: {
128
- fourHour: custom4h ?? globalLimit4h,
129
- weekly: customWeekly ?? globalLimitWeekly,
130
- fourHourIsCustom: custom4h !== null,
131
- weeklyIsCustom: customWeekly !== null,
132
- },
133
- usage: {
134
- fourHour: h4Tokens,
135
- weekly: weeklyTokens,
136
- }
137
- });
116
+ res.json(getRateLimitSnapshot(req.session.userId));
138
117
  } catch (err) {
139
118
  sendRouteError(res, err);
140
119
  }
@@ -18,6 +18,7 @@ const {
18
18
  const { APP_DIR, ENV_FILE, upsertEnvValue } = require('../../runtime/paths');
19
19
  const { isManagedDeployment } = require('../utils/deployment');
20
20
  const rateLimit = require('express-rate-limit');
21
+ const { configuredDefaultLimits } = require('../services/ai/rate_limits');
21
22
 
22
23
  const router = express.Router();
23
24
  const ADMIN_DIR = path.join(__dirname, '..', 'admin');
@@ -324,6 +325,20 @@ const sqlLimiter = rateLimit({
324
325
  message: { error: 'Too many SQL queries, slow down' },
325
326
  });
326
327
 
328
+ router.get('/api/config/email', requireAdminAuth, (req, res) => {
329
+ const { getAdminEmailSettings } = require('../services/account/service_email_settings');
330
+ res.json(getAdminEmailSettings());
331
+ });
332
+
333
+ router.put('/api/config/email', requireAdminAuth, settingsLimiter, express.json(), (req, res) => {
334
+ try {
335
+ const { updateAdminEmailSettings } = require('../services/account/service_email_settings');
336
+ res.json({ ok: true, ...updateAdminEmailSettings(req.body) });
337
+ } catch (err) {
338
+ res.status(err.statusCode || 500).json({ error: err.message });
339
+ }
340
+ });
341
+
327
342
  // --- Access settings (signup toggle + API key) ---
328
343
 
329
344
  router.get('/api/settings', requireAdminAuth, (req, res) => {
@@ -670,9 +685,10 @@ router.put('/api/providers', requireAdminAuth, express.json(), (req, res) => {
670
685
  // --- Global default rate limits ---
671
686
 
672
687
  router.get('/api/config/rate-limits', requireAdminAuth, (req, res) => {
688
+ const defaults = configuredDefaultLimits();
673
689
  res.json({
674
- rate_limit_4h: process.env.NEOAGENT_RATE_LIMIT_4H ? parseInt(process.env.NEOAGENT_RATE_LIMIT_4H, 10) : null,
675
- rate_limit_weekly: process.env.NEOAGENT_RATE_LIMIT_WEEKLY ? parseInt(process.env.NEOAGENT_RATE_LIMIT_WEEKLY, 10) : null,
690
+ rate_limit_4h: defaults.fourHour,
691
+ rate_limit_weekly: defaults.weekly,
676
692
  });
677
693
  });
678
694
 
@@ -130,7 +130,11 @@ router.post('/', async (req, res) => {
130
130
 
131
131
  res.json(result);
132
132
  } catch (err) {
133
- res.status(500).json({ error: sanitizeError(err) });
133
+ res.status(err?.statusCode || err?.status || 500).json({
134
+ error: sanitizeError(err),
135
+ code: err?.code,
136
+ rateLimit: err?.rateLimit,
137
+ });
134
138
  }
135
139
  });
136
140
 
@@ -494,7 +494,7 @@ router.post('/transfer-import', transferImportLimiter, async (req, res) => {
494
494
  if (applyCoreMemory && parsed.coreEntries) {
495
495
  for (const [key, value] of Object.entries(parsed.coreEntries)) {
496
496
  if (!key || key === 'active_context') continue;
497
- mm.updateCore(userId, key, value, { agentId, confirmed: true });
497
+ mm.updateCore(userId, key, value, { agentId });
498
498
  coreUpdatedCount += 1;
499
499
  }
500
500
  }
@@ -526,7 +526,7 @@ router.put('/core/:key', (req, res) => {
526
526
  if (req.params.key === 'active_context') {
527
527
  return res.status(400).json({ error: 'active_context is no longer a supported core memory key' });
528
528
  }
529
- mm.updateCore(userId, req.params.key, value, { agentId, confirmed: true });
529
+ mm.updateCore(userId, req.params.key, value, { agentId });
530
530
  res.json({ success: true });
531
531
  });
532
532
 
@@ -0,0 +1,167 @@
1
+ 'use strict';
2
+
3
+ const { ENV_FILE, upsertEnvValue } = require('../../../runtime/paths');
4
+
5
+ const STRING_FIELDS = Object.freeze({
6
+ from: 'NEOAGENT_EMAIL_FROM',
7
+ smtpHost: 'NEOAGENT_EMAIL_SMTP_HOST',
8
+ smtpUser: 'NEOAGENT_EMAIL_SMTP_USER',
9
+ replyTo: 'NEOAGENT_EMAIL_REPLY_TO',
10
+ brandName: 'NEOAGENT_EMAIL_BRAND_NAME',
11
+ supportUrl: 'NEOAGENT_EMAIL_SUPPORT_URL',
12
+ publicUrl: 'NEOAGENT_EMAIL_PUBLIC_URL',
13
+ });
14
+
15
+ const BOOLEAN_FIELDS = Object.freeze({
16
+ smtpSecure: ['NEOAGENT_EMAIL_SMTP_SECURE', false],
17
+ smtpRequireTls: ['NEOAGENT_EMAIL_SMTP_REQUIRE_TLS', true],
18
+ smtpRejectUnauthorized: ['NEOAGENT_EMAIL_SMTP_REJECT_UNAUTHORIZED', true],
19
+ requireSignupConfirmation: ['NEOAGENT_EMAIL_REQUIRE_SIGNUP_CONFIRMATION', true],
20
+ requireEmailChangeConfirmation: ['NEOAGENT_EMAIL_REQUIRE_EMAIL_CHANGE_CONFIRMATION', true],
21
+ notifyUnusualLogin: ['NEOAGENT_EMAIL_NOTIFY_UNUSUAL_LOGIN', true],
22
+ notifyAccountChanges: ['NEOAGENT_EMAIL_NOTIFY_ACCOUNT_CHANGES', true],
23
+ });
24
+
25
+ function envString(env, key) {
26
+ return String(env[key] || '').trim();
27
+ }
28
+
29
+ function envBoolean(env, key, defaultValue) {
30
+ const value = envString(env, key).toLowerCase();
31
+ if (!value) return defaultValue;
32
+ return ['1', 'true', 'yes', 'on'].includes(value);
33
+ }
34
+
35
+ function envPositiveInteger(env, key, defaultValue) {
36
+ const value = Number(envString(env, key));
37
+ return Number.isInteger(value) && value > 0 ? value : defaultValue;
38
+ }
39
+
40
+ function cleanSingleLine(value, label) {
41
+ const cleaned = String(value ?? '').trim();
42
+ if (/[\r\n]/.test(cleaned)) {
43
+ const error = new Error(`${label} must be a single line.`);
44
+ error.statusCode = 400;
45
+ throw error;
46
+ }
47
+ return cleaned;
48
+ }
49
+
50
+ function requireBoolean(value, label) {
51
+ if (typeof value !== 'boolean') {
52
+ const error = new Error(`${label} must be true or false.`);
53
+ error.statusCode = 400;
54
+ throw error;
55
+ }
56
+ return value;
57
+ }
58
+
59
+ function requireInteger(value, label, { min, max }) {
60
+ const number = Number(value);
61
+ if (!Number.isInteger(number) || number < min || number > max) {
62
+ const error = new Error(`${label} must be an integer from ${min} to ${max}.`);
63
+ error.statusCode = 400;
64
+ throw error;
65
+ }
66
+ return number;
67
+ }
68
+
69
+ function requireHttpUrl(value, label) {
70
+ const cleaned = cleanSingleLine(value, label);
71
+ if (!cleaned) return '';
72
+ try {
73
+ const url = new URL(cleaned);
74
+ if (url.protocol !== 'http:' && url.protocol !== 'https:') throw new Error('invalid protocol');
75
+ } catch {
76
+ const error = new Error(`${label} must be an HTTP or HTTPS URL.`);
77
+ error.statusCode = 400;
78
+ throw error;
79
+ }
80
+ return cleaned.replace(/\/$/, '');
81
+ }
82
+
83
+ function encodeEnvValue(value) {
84
+ return JSON.stringify(String(value));
85
+ }
86
+
87
+ function persistValue(envFile, env, key, value) {
88
+ upsertEnvValue(envFile, key, encodeEnvValue(value));
89
+ if (value === '') {
90
+ delete env[key];
91
+ } else {
92
+ env[key] = String(value);
93
+ }
94
+ }
95
+
96
+ function getAdminEmailSettings({ env = process.env } = {}) {
97
+ const smtpPort = envPositiveInteger(env, 'NEOAGENT_EMAIL_SMTP_PORT', 587);
98
+ const settings = {
99
+ smtpPort,
100
+ tokenTtlHours: envPositiveInteger(env, 'NEOAGENT_EMAIL_TOKEN_TTL_HOURS', 24),
101
+ smtpPasswordConfigured: Boolean(envString(env, 'NEOAGENT_EMAIL_SMTP_PASS')),
102
+ };
103
+
104
+ for (const [name, key] of Object.entries(STRING_FIELDS)) {
105
+ settings[name] = envString(env, key);
106
+ }
107
+ for (const [name, [key, defaultValue]] of Object.entries(BOOLEAN_FIELDS)) {
108
+ let effectiveDefault = defaultValue;
109
+ if (name === 'smtpSecure') effectiveDefault = smtpPort === 465;
110
+ if (name === 'smtpRequireTls') effectiveDefault = !settings.smtpSecure;
111
+ settings[name] = envBoolean(env, key, effectiveDefault);
112
+ }
113
+
114
+ const missing = [];
115
+ if (!settings.from) missing.push('Sender address');
116
+ if (!settings.smtpHost) missing.push('SMTP host');
117
+
118
+ return {
119
+ configured: missing.length === 0,
120
+ missing,
121
+ settings,
122
+ };
123
+ }
124
+
125
+ function updateAdminEmailSettings(body, { env = process.env, envFile = ENV_FILE } = {}) {
126
+ if (!body || typeof body !== 'object' || Array.isArray(body)) {
127
+ const error = new Error('Email settings must be an object.');
128
+ error.statusCode = 400;
129
+ throw error;
130
+ }
131
+
132
+ const values = {};
133
+ for (const name of Object.keys(STRING_FIELDS)) {
134
+ values[name] = name === 'supportUrl' || name === 'publicUrl'
135
+ ? requireHttpUrl(body[name], name === 'supportUrl' ? 'Support URL' : 'Public URL')
136
+ : cleanSingleLine(body[name], name);
137
+ }
138
+
139
+ values.smtpPort = requireInteger(body.smtpPort, 'SMTP port', { min: 1, max: 65535 });
140
+ values.tokenTtlHours = requireInteger(body.tokenTtlHours, 'Token lifetime', { min: 1, max: 8760 });
141
+
142
+ for (const name of Object.keys(BOOLEAN_FIELDS)) {
143
+ values[name] = requireBoolean(body[name], name);
144
+ }
145
+
146
+ const smtpPassword = cleanSingleLine(body.smtpPassword, 'SMTP password');
147
+ const clearSmtpPassword = body.clearSmtpPassword === true;
148
+
149
+ for (const [name, key] of Object.entries(STRING_FIELDS)) {
150
+ persistValue(envFile, env, key, values[name]);
151
+ }
152
+ persistValue(envFile, env, 'NEOAGENT_EMAIL_SMTP_PORT', values.smtpPort);
153
+ persistValue(envFile, env, 'NEOAGENT_EMAIL_TOKEN_TTL_HOURS', values.tokenTtlHours);
154
+ for (const [name, [key]] of Object.entries(BOOLEAN_FIELDS)) {
155
+ persistValue(envFile, env, key, values[name]);
156
+ }
157
+ if (smtpPassword || clearSmtpPassword) {
158
+ persistValue(envFile, env, 'NEOAGENT_EMAIL_SMTP_PASS', clearSmtpPassword ? '' : smtpPassword);
159
+ }
160
+
161
+ return getAdminEmailSettings({ env });
162
+ }
163
+
164
+ module.exports = {
165
+ getAdminEmailSettings,
166
+ updateAdminEmailSettings,
167
+ };
@@ -56,6 +56,7 @@ const { globalHooks } = require('./hooks');
56
56
  const { withProviderRetry, isTransientError } = require('./providerRetry');
57
57
  const { normalizeCompletionConfidence, shouldAcceptTaskComplete } = require('./completion');
58
58
  const { normalizeUsage, recordModelUsage } = require('./usage');
59
+ const { enforceRateLimits } = require('./rate_limits');
59
60
  const { ToolRepetitionGuard } = require('./repetitionGuard');
60
61
  const { shortenRunId, summarizeForLog, parseMaybeJson } = require('./logFormat');
61
62
  const {
@@ -1786,23 +1787,7 @@ class AgentEngine {
1786
1787
  ensureDefaultAiSettings(userId, agentId);
1787
1788
  const aiSettings = getAiSettings(userId, agentId);
1788
1789
 
1789
- const userLimits = db.prepare('SELECT rate_limit_4h, rate_limit_weekly FROM users WHERE id = ?').get(userId);
1790
- const globalLimit4h = process.env.NEOAGENT_RATE_LIMIT_4H ? parseInt(process.env.NEOAGENT_RATE_LIMIT_4H, 10) : null;
1791
- const globalLimitWeekly = process.env.NEOAGENT_RATE_LIMIT_WEEKLY ? parseInt(process.env.NEOAGENT_RATE_LIMIT_WEEKLY, 10) : null;
1792
- const effective4h = userLimits?.rate_limit_4h ?? globalLimit4h;
1793
- const effectiveWeekly = userLimits?.rate_limit_weekly ?? globalLimitWeekly;
1794
- if (effective4h) {
1795
- const h4Tokens = db.prepare("SELECT COALESCE(SUM(total_tokens), 0) as t FROM agent_runs WHERE user_id = ? AND created_at > datetime('now', '-4 hours')").get(userId).t;
1796
- if (h4Tokens >= effective4h) {
1797
- throw new Error(`Rate limit exceeded: You have used ${h4Tokens} tokens in the last 4 hours (limit: ${effective4h}).`);
1798
- }
1799
- }
1800
- if (effectiveWeekly) {
1801
- const weeklyTokens = db.prepare("SELECT COALESCE(SUM(total_tokens), 0) as t FROM agent_runs WHERE user_id = ? AND created_at > datetime('now', '-7 days')").get(userId).t;
1802
- if (weeklyTokens >= effectiveWeekly) {
1803
- throw new Error(`Rate limit exceeded: You have used ${weeklyTokens} tokens in the last 7 days (limit: ${effectiveWeekly}).`);
1804
- }
1805
- }
1790
+ enforceRateLimits(userId);
1806
1791
 
1807
1792
  const runId = options.runId || uuidv4();
1808
1793
  const conversationId = options.conversationId;
@@ -0,0 +1,150 @@
1
+ 'use strict';
2
+
3
+ const db = require('../../db/database');
4
+
5
+ const DEFAULT_RATE_LIMIT_4H = 2_500_000;
6
+ const DEFAULT_RATE_LIMIT_WEEKLY = 10_000_000;
7
+
8
+ const WINDOWS = {
9
+ fourHour: {
10
+ durationMs: 4 * 60 * 60 * 1000,
11
+ },
12
+ weekly: {
13
+ durationMs: 7 * 24 * 60 * 60 * 1000,
14
+ },
15
+ };
16
+
17
+ class RateLimitExceededError extends Error {
18
+ constructor(windowKey, snapshot) {
19
+ const label = windowKey === 'fourHour' ? 'the last 4 hours' : 'the last 7 days';
20
+ const usage = snapshot.usage[windowKey];
21
+ const limit = snapshot.limits[windowKey];
22
+ super(`Rate limit exceeded: You have used ${usage} tokens in ${label} (limit: ${limit}).`);
23
+ this.name = 'RateLimitExceededError';
24
+ this.statusCode = 429;
25
+ this.code = 'RATE_LIMIT_EXCEEDED';
26
+ this.rateLimit = {
27
+ window: windowKey,
28
+ ...snapshot,
29
+ };
30
+ }
31
+ }
32
+
33
+ function parsePositiveInteger(value, fallback) {
34
+ const parsed = Number.parseInt(String(value || ''), 10);
35
+ return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
36
+ }
37
+
38
+ function configuredDefaultLimits() {
39
+ return {
40
+ fourHour: parsePositiveInteger(
41
+ process.env.NEOAGENT_RATE_LIMIT_4H,
42
+ DEFAULT_RATE_LIMIT_4H,
43
+ ),
44
+ weekly: parsePositiveInteger(
45
+ process.env.NEOAGENT_RATE_LIMIT_WEEKLY,
46
+ DEFAULT_RATE_LIMIT_WEEKLY,
47
+ ),
48
+ };
49
+ }
50
+
51
+ function parseSqliteDate(value) {
52
+ const text = String(value || '').trim();
53
+ if (!text) return null;
54
+ const normalized = text.includes('T') ? text : `${text.replace(' ', 'T')}Z`;
55
+ const date = new Date(normalized);
56
+ return Number.isNaN(date.getTime()) ? null : date;
57
+ }
58
+
59
+ function usageRows(userId, durationMs) {
60
+ const modifier = `-${Math.floor(durationMs / 1000)} seconds`;
61
+ return db.prepare(
62
+ `SELECT COALESCE(total_tokens, 0) AS tokens, created_at
63
+ FROM agent_runs
64
+ WHERE user_id = ? AND created_at > datetime('now', ?)
65
+ ORDER BY datetime(created_at) ASC`,
66
+ ).all(userId, modifier);
67
+ }
68
+
69
+ function nextDecreaseAt(rows, durationMs, usage, limit) {
70
+ const positiveRows = rows.filter((row) => Number(row.tokens) > 0);
71
+ if (positiveRows.length === 0) return null;
72
+
73
+ let tokensToExpire = 0;
74
+ const requiredExpiry = usage >= limit ? usage - limit + 1 : 1;
75
+ for (const row of positiveRows) {
76
+ tokensToExpire += Number(row.tokens);
77
+ if (tokensToExpire < requiredExpiry) continue;
78
+ const createdAt = parseSqliteDate(row.created_at);
79
+ return createdAt
80
+ ? new Date(createdAt.getTime() + durationMs).toISOString()
81
+ : null;
82
+ }
83
+ return null;
84
+ }
85
+
86
+ function getRateLimitSnapshot(userId) {
87
+ const userLimits = db.prepare(
88
+ 'SELECT rate_limit_4h, rate_limit_weekly FROM users WHERE id = ?',
89
+ ).get(userId);
90
+ const defaults = configuredDefaultLimits();
91
+ const customFourHour = userLimits?.rate_limit_4h;
92
+ const customWeekly = userLimits?.rate_limit_weekly;
93
+ const limits = {
94
+ fourHour: customFourHour == null
95
+ ? defaults.fourHour
96
+ : (customFourHour > 0 ? customFourHour : null),
97
+ weekly: customWeekly == null
98
+ ? defaults.weekly
99
+ : (customWeekly > 0 ? customWeekly : null),
100
+ fourHourIsCustom: customFourHour != null,
101
+ weeklyIsCustom: customWeekly != null,
102
+ };
103
+ const usage = {};
104
+ const remaining = {};
105
+ const reached = {};
106
+ const nextDecreaseAtByWindow = {};
107
+
108
+ for (const [windowKey, config] of Object.entries(WINDOWS)) {
109
+ const rows = usageRows(userId, config.durationMs);
110
+ const used = rows.reduce((total, row) => total + Number(row.tokens || 0), 0);
111
+ const limit = limits[windowKey];
112
+ usage[windowKey] = used;
113
+ remaining[windowKey] = limit == null ? null : Math.max(0, limit - used);
114
+ reached[windowKey] = limit != null && used >= limit;
115
+ nextDecreaseAtByWindow[windowKey] = limit == null
116
+ ? null
117
+ : nextDecreaseAt(rows, config.durationMs, used, limit);
118
+ }
119
+
120
+ return {
121
+ limits,
122
+ usage,
123
+ remaining,
124
+ reached: {
125
+ ...reached,
126
+ any: reached.fourHour || reached.weekly,
127
+ },
128
+ nextDecreaseAt: nextDecreaseAtByWindow,
129
+ };
130
+ }
131
+
132
+ function enforceRateLimits(userId) {
133
+ const snapshot = getRateLimitSnapshot(userId);
134
+ if (snapshot.reached.fourHour) {
135
+ throw new RateLimitExceededError('fourHour', snapshot);
136
+ }
137
+ if (snapshot.reached.weekly) {
138
+ throw new RateLimitExceededError('weekly', snapshot);
139
+ }
140
+ return snapshot;
141
+ }
142
+
143
+ module.exports = {
144
+ DEFAULT_RATE_LIMIT_4H,
145
+ DEFAULT_RATE_LIMIT_WEEKLY,
146
+ RateLimitExceededError,
147
+ configuredDefaultLimits,
148
+ enforceRateLimits,
149
+ getRateLimitSnapshot,
150
+ };
@@ -786,10 +786,9 @@ function getAvailableTools(app, options = {}) {
786
786
  type: 'object',
787
787
  properties: {
788
788
  key: { type: 'string', enum: ['user_profile', 'preferences', 'ai_personality'], description: 'user_profile: who the user is, preferences: standing likes/dislikes, ai_personality: concise durable notes for how the agent should behave for this user' },
789
- value: { type: 'string', description: 'Value to set. Keep it concise — this is injected into every single prompt.' },
790
- confirmed: { type: 'boolean', description: 'Must be true only when the user explicitly requested this core-memory change in the current conversation.' }
789
+ value: { type: 'string', description: 'Value to set. Keep it concise — this is injected into every single prompt.' }
791
790
  },
792
- required: ['key', 'value', 'confirmed']
791
+ required: ['key', 'value']
793
792
  }
794
793
  },
795
794
  {
@@ -1895,7 +1894,8 @@ async function executeTool(toolName, args, context, engine) {
1895
1894
  case 'memory_save': {
1896
1895
  const { MemoryManager } = require('../memory/manager');
1897
1896
  const mm = new MemoryManager();
1898
- const id = await mm.saveMemory(userId, args.content, args.category || 'episodic', args.importance || 5, { agentId });
1897
+ const content = typeof args.content === 'string' ? args.content : args.value;
1898
+ const id = await mm.saveMemory(userId, content, args.category || 'episodic', args.importance || 5, { agentId });
1899
1899
  if (!id) {
1900
1900
  return {
1901
1901
  success: true,
@@ -1926,12 +1926,15 @@ async function executeTool(toolName, args, context, engine) {
1926
1926
  }
1927
1927
 
1928
1928
  case 'memory_update_core': {
1929
- const { MemoryManager } = require('../memory/manager');
1929
+ const { MemoryManager, CORE_KEYS } = require('../memory/manager');
1930
1930
  const mm = new MemoryManager();
1931
- if (args.confirmed !== true) {
1932
- return { error: 'Core memory updates require explicit current-session user confirmation.' };
1931
+ if (!CORE_KEYS.includes(args.key)) {
1932
+ return { error: `Core memory key must be one of: ${CORE_KEYS.join(', ')}.` };
1933
+ }
1934
+ if (typeof args.value !== 'string' || !args.value.trim()) {
1935
+ return { error: 'Core memory value must be a non-empty string.' };
1933
1936
  }
1934
- mm.updateCore(userId, args.key, args.value, { agentId, confirmed: true });
1937
+ mm.updateCore(userId, args.key, args.value, { agentId });
1935
1938
  return { success: true, key: args.key, message: 'Core memory updated' };
1936
1939
  }
1937
1940
 
@@ -2233,9 +2233,6 @@ class MemoryManager {
2233
2233
  }
2234
2234
 
2235
2235
  updateCore(userId, key, value, options = {}) {
2236
- if (options.confirmed !== true) {
2237
- throw new Error('Core memory updates require explicit confirmation.');
2238
- }
2239
2236
  const agentId = this._agentId(userId, options);
2240
2237
  const strVal = typeof value === 'object' ? JSON.stringify(value) : String(value);
2241
2238
  db.prepare(
@@ -301,7 +301,11 @@ function setupWebSocket(io, services) {
301
301
  }
302
302
  } catch (err) {
303
303
  console.error(`[WS] agent:run failed for user ${userId}:`, err);
304
- socket.emit('run:error', { error: sanitizeError(err) });
304
+ socket.emit('run:error', {
305
+ error: sanitizeError(err),
306
+ code: err?.code,
307
+ rateLimit: err?.rateLimit,
308
+ });
305
309
  }
306
310
  });
307
311