neoagent 2.4.4-beta.8 → 2.4.4-beta.9
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/.env.example +17 -0
- package/docs/configuration.md +1 -0
- package/docs/supermemory-memory-review.md +1 -1
- package/flutter_app/lib/main_account_settings.dart +79 -15
- package/flutter_app/lib/main_chat.dart +155 -8
- package/flutter_app/lib/main_controller.dart +38 -4
- package/flutter_app/lib/main_devices.dart +9 -3
- package/flutter_app/lib/main_models.dart +32 -0
- package/lib/manager.js +52 -0
- package/package.json +1 -1
- package/server/admin/admin.js +151 -0
- package/server/admin/index.html +55 -3
- package/server/public/.last_build_id +1 -1
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/public/main.dart.js +50995 -50850
- package/server/routes/account.js +2 -23
- package/server/routes/admin.js +18 -2
- package/server/routes/agents.js +5 -1
- package/server/routes/memory.js +2 -2
- package/server/services/account/service_email_settings.js +167 -0
- package/server/services/ai/engine.js +2 -17
- package/server/services/ai/rate_limits.js +150 -0
- package/server/services/ai/tools.js +11 -8
- package/server/services/memory/manager.js +0 -3
- package/server/services/websocket.js +5 -1
package/server/routes/account.js
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|
package/server/routes/admin.js
CHANGED
|
@@ -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:
|
|
675
|
-
rate_limit_weekly:
|
|
690
|
+
rate_limit_4h: defaults.fourHour,
|
|
691
|
+
rate_limit_weekly: defaults.weekly,
|
|
676
692
|
});
|
|
677
693
|
});
|
|
678
694
|
|
package/server/routes/agents.js
CHANGED
|
@@ -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({
|
|
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
|
|
package/server/routes/memory.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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'
|
|
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
|
|
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.
|
|
1932
|
-
return { error:
|
|
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
|
|
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', {
|
|
304
|
+
socket.emit('run:error', {
|
|
305
|
+
error: sanitizeError(err),
|
|
306
|
+
code: err?.code,
|
|
307
|
+
rateLimit: err?.rateLimit,
|
|
308
|
+
});
|
|
305
309
|
}
|
|
306
310
|
});
|
|
307
311
|
|