neoagent 3.1.1-beta.2 → 3.1.1-beta.3
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/flutter_app/lib/main_controller.dart +17 -0
- package/flutter_app/lib/main_models.dart +55 -0
- package/flutter_app/lib/main_operations.dart +530 -2
- package/flutter_app/lib/src/backend_client.dart +20 -0
- package/lib/schema_migrations.js +23 -0
- package/package.json +1 -1
- package/server/db/database.js +6 -0
- package/server/public/.last_build_id +1 -1
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/public/main.dart.js +63376 -62995
- package/server/routes/tasks.js +17 -0
- package/server/services/ai/tools.js +1 -1
- package/server/services/memory/consolidation.js +2 -0
- package/server/services/memory/manager.js +70 -8
- package/server/services/memory/retention.js +66 -0
- package/server/services/memory/routing.js +68 -0
- package/server/services/messaging/access_policy.js +39 -2
- package/server/services/messaging/manager.js +9 -4
- package/server/services/tasks/delivery_targets.js +260 -0
- package/server/utils/whatsapp.js +33 -1
package/server/routes/tasks.js
CHANGED
|
@@ -3,6 +3,7 @@ const router = express.Router();
|
|
|
3
3
|
const { requireAuth } = require('../middleware/auth');
|
|
4
4
|
const { sanitizeError } = require('../utils/security');
|
|
5
5
|
const { getAgentIdFromRequest, resolveAgentId } = require('../services/agents/manager');
|
|
6
|
+
const { TaskDeliveryTargetService } = require('../services/tasks/delivery_targets');
|
|
6
7
|
|
|
7
8
|
router.use(requireAuth);
|
|
8
9
|
|
|
@@ -28,6 +29,22 @@ router.get('/catalog', (req, res) => {
|
|
|
28
29
|
}
|
|
29
30
|
});
|
|
30
31
|
|
|
32
|
+
router.get('/delivery-targets', async (req, res) => {
|
|
33
|
+
try {
|
|
34
|
+
const agentId = resolveAgentId(req.session.userId, getAgentIdFromRequest(req));
|
|
35
|
+
const service = new TaskDeliveryTargetService({ app: req.app });
|
|
36
|
+
const targets = await service.listTargets(req.session.userId, {
|
|
37
|
+
agentId,
|
|
38
|
+
platform: req.query.platform,
|
|
39
|
+
q: req.query.q,
|
|
40
|
+
});
|
|
41
|
+
res.json(targets);
|
|
42
|
+
} catch (error) {
|
|
43
|
+
(req.app.locals.logger?.error || console.error)('[Tasks] Failed to discover delivery targets', error);
|
|
44
|
+
res.status(500).json({ error: sanitizeError(error) });
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
31
48
|
router.post('/', async (req, res) => {
|
|
32
49
|
try {
|
|
33
50
|
const tasks = req.app.locals.taskRuntime;
|
|
@@ -837,7 +837,7 @@ function getAvailableTools(app, options = {}) {
|
|
|
837
837
|
type: 'object',
|
|
838
838
|
properties: {
|
|
839
839
|
content: { type: 'string', description: 'The complete, self-contained fact. Must be readable standalone — no references to "above", "the dump", or "chat history". Write as a clear declarative sentence.' },
|
|
840
|
-
category: { type: 'string', enum: ['user_fact', 'preference', 'personality', 'episodic'], description: 'user_fact: facts about the user (job, location, hardware...), preference: likes/dislikes/settings, personality: how to interact with them, episodic: events/tasks/learnings' },
|
|
840
|
+
category: { type: 'string', enum: ['user_fact', 'preference', 'personality', 'episodic', 'procedural'], description: 'user_fact: facts about the user (job, location, hardware...), preference: likes/dislikes/settings, personality: how to interact with them, episodic: events/tasks/learnings, procedural: reusable workflows or repeatable tool-use procedures' },
|
|
841
841
|
importance: { type: 'number', description: 'Importance 1-10. 1=trivial, 5=default, 8+=critical. High-importance memories rank higher in recall.' }
|
|
842
842
|
},
|
|
843
843
|
required: ['content']
|
|
@@ -9,6 +9,7 @@ const MEMORY_CATEGORIES = new Set([
|
|
|
9
9
|
'events',
|
|
10
10
|
'tasks',
|
|
11
11
|
'episodic',
|
|
12
|
+
'procedural',
|
|
12
13
|
'assistant_self',
|
|
13
14
|
]);
|
|
14
15
|
|
|
@@ -99,6 +100,7 @@ function buildMemoryConsolidationInstructions(currentDateTime) {
|
|
|
99
100
|
'Use relation="extends" when it adds compatible detail without replacing the prior fact.',
|
|
100
101
|
'Use relation="derives" only for a strongly supported inference and lower its confidence.',
|
|
101
102
|
'Use is_static=true only for stable identity or durable preference facts.',
|
|
103
|
+
'Use category="procedural" only for reusable workflows or repeatable tool-use procedures, not one-off task status.',
|
|
102
104
|
'Set valid_from, valid_to, or forget_after as ISO-8601 timestamps when the thread provides temporal boundaries.',
|
|
103
105
|
'Return an empty memory_candidates array when nothing is worth retaining.',
|
|
104
106
|
].join(' ');
|
|
@@ -26,6 +26,12 @@ const {
|
|
|
26
26
|
findEmbeddingCandidates,
|
|
27
27
|
replaceMemoryEmbeddingIndex,
|
|
28
28
|
} = require('./embedding_index');
|
|
29
|
+
const { getRouteCategoryBoost, routeMemoryQuery } = require('./routing');
|
|
30
|
+
const {
|
|
31
|
+
getReinforcedStrength,
|
|
32
|
+
getRetentionScoreMultiplier,
|
|
33
|
+
isArchiveEligible,
|
|
34
|
+
} = require('./retention');
|
|
29
35
|
const { AGENT_DATA_DIR } = require('../../../runtime/paths');
|
|
30
36
|
const { isMainAgent, resolveAgentId } = require('../agents/manager');
|
|
31
37
|
const { buildFtsQuery } = require('../../db/ftsQuery');
|
|
@@ -72,6 +78,7 @@ const CATEGORIES = [
|
|
|
72
78
|
'events',
|
|
73
79
|
'tasks',
|
|
74
80
|
'episodic',
|
|
81
|
+
'procedural',
|
|
75
82
|
'assistant_self',
|
|
76
83
|
'user_fact',
|
|
77
84
|
'preference',
|
|
@@ -167,6 +174,9 @@ function serializeMemoryRow(row) {
|
|
|
167
174
|
importance: Number(row.importance || 0),
|
|
168
175
|
confidence: row.confidence == null ? 0.7 : Number(row.confidence),
|
|
169
176
|
access_count: Number(row.access_count || 0),
|
|
177
|
+
memoryStrength: row.memory_strength == null ? 1 : Number(row.memory_strength),
|
|
178
|
+
lastAccessedAt: row.last_accessed_at || null,
|
|
179
|
+
pinned: Number(row.pinned || 0) === 1,
|
|
170
180
|
archived: Number(row.archived || 0),
|
|
171
181
|
created_at: row.created_at,
|
|
172
182
|
updated_at: row.updated_at,
|
|
@@ -1729,9 +1739,9 @@ class MemoryManager {
|
|
|
1729
1739
|
`INSERT OR IGNORE INTO memories (
|
|
1730
1740
|
id, user_id, agent_id, category, scope_type, scope_id, source_type, source_id, source_label,
|
|
1731
1741
|
stale_after_days, metadata_json, content, summary, importance, confidence, memory_hash, embedding,
|
|
1732
|
-
embedding_provider, embedding_model, embedding_dimensions, embedded_at
|
|
1742
|
+
memory_strength, pinned, embedding_provider, embedding_model, embedding_dimensions, embedded_at
|
|
1733
1743
|
)
|
|
1734
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
1744
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
1735
1745
|
).run(
|
|
1736
1746
|
id,
|
|
1737
1747
|
userId,
|
|
@@ -1750,6 +1760,8 @@ class MemoryManager {
|
|
|
1750
1760
|
confidence,
|
|
1751
1761
|
memoryHash,
|
|
1752
1762
|
embedding ? serializeEmbedding(embedding) : null,
|
|
1763
|
+
1,
|
|
1764
|
+
0,
|
|
1753
1765
|
embeddingResult?.provider || null,
|
|
1754
1766
|
embeddingResult?.model || null,
|
|
1755
1767
|
embeddingResult?.dimensions || null,
|
|
@@ -1830,6 +1842,7 @@ class MemoryManager {
|
|
|
1830
1842
|
const agentId = this._agentId(userId, options);
|
|
1831
1843
|
const scope = normalizeScope(options.scope, agentId);
|
|
1832
1844
|
const limit = Math.max(1, Math.min(Number(topK) || 6, 50));
|
|
1845
|
+
const route = routeMemoryQuery(query);
|
|
1833
1846
|
|
|
1834
1847
|
const suppliedQueryEmbedding = options.queryEmbedding;
|
|
1835
1848
|
const queryVec = suppliedQueryEmbedding
|
|
@@ -1887,7 +1900,8 @@ class MemoryManager {
|
|
|
1887
1900
|
const candidateIdList = [...candidateIds];
|
|
1888
1901
|
const candidatePlaceholders = candidateIdList.map(() => '?').join(', ');
|
|
1889
1902
|
let all = db.prepare(
|
|
1890
|
-
`SELECT id, category, content, summary, importance, confidence, embedding, access_count,
|
|
1903
|
+
`SELECT id, category, content, summary, importance, confidence, embedding, access_count,
|
|
1904
|
+
memory_strength, last_accessed_at, pinned, created_at, updated_at,
|
|
1891
1905
|
scope_type, scope_id, source_type, source_id, source_label, stale_after_days, metadata_json
|
|
1892
1906
|
FROM memories
|
|
1893
1907
|
WHERE id IN (${candidatePlaceholders})
|
|
@@ -1987,12 +2001,17 @@ class MemoryManager {
|
|
|
1987
2001
|
const ftsScore = lexicalRanks.has(mem.id) ? 0.42 : 0;
|
|
1988
2002
|
const entityScore = entityRanks.has(mem.id) ? 0.48 : 0;
|
|
1989
2003
|
const relationScore = relationRanks.has(mem.id) ? 0.36 : 0;
|
|
2004
|
+
const directSignal = Math.max(mem.semanticScore, lexicalScore, ftsScore, entityScore, relationScore);
|
|
2005
|
+
const routeScore = directSignal > 0.08
|
|
2006
|
+
? getRouteCategoryBoost(route, normalizeMemoryCategory(mem.category))
|
|
2007
|
+
: 0;
|
|
1990
2008
|
const baseScore = Math.max(
|
|
1991
2009
|
mem.semanticScore * (0.65 + Number(mem.importance || 5) / 25),
|
|
1992
2010
|
lexicalScore,
|
|
1993
2011
|
ftsScore,
|
|
1994
2012
|
entityScore,
|
|
1995
2013
|
relationScore,
|
|
2014
|
+
routeScore,
|
|
1996
2015
|
);
|
|
1997
2016
|
const score = scoreMemoryCandidate({
|
|
1998
2017
|
semanticRank: semanticRanks.get(mem.id) ?? -1,
|
|
@@ -2003,7 +2022,7 @@ class MemoryManager {
|
|
|
2003
2022
|
confidence: mem.confidence,
|
|
2004
2023
|
accessCount: mem.access_count,
|
|
2005
2024
|
freshness: computeFreshnessMultiplier(mem),
|
|
2006
|
-
});
|
|
2025
|
+
}) * getRetentionScoreMultiplier(mem);
|
|
2007
2026
|
return {
|
|
2008
2027
|
...mem,
|
|
2009
2028
|
score,
|
|
@@ -2013,6 +2032,8 @@ class MemoryManager {
|
|
|
2013
2032
|
fullText: ftsScore,
|
|
2014
2033
|
entity: entityScore,
|
|
2015
2034
|
relation: relationScore,
|
|
2035
|
+
route: routeScore,
|
|
2036
|
+
routeIntent: route.intent,
|
|
2016
2037
|
vectorCandidateRank: vectorRanks.get(mem.id) ?? -1,
|
|
2017
2038
|
candidateCount: all.length,
|
|
2018
2039
|
},
|
|
@@ -2026,9 +2047,19 @@ class MemoryManager {
|
|
|
2026
2047
|
|
|
2027
2048
|
// Update access counts
|
|
2028
2049
|
if (results.length) {
|
|
2029
|
-
const
|
|
2030
|
-
|
|
2031
|
-
|
|
2050
|
+
const updates = db.prepare(
|
|
2051
|
+
`UPDATE memories
|
|
2052
|
+
SET access_count = access_count + 1,
|
|
2053
|
+
memory_strength = ?,
|
|
2054
|
+
last_accessed_at = datetime('now')
|
|
2055
|
+
WHERE id = ?`
|
|
2056
|
+
);
|
|
2057
|
+
const reinforce = db.transaction(() => {
|
|
2058
|
+
for (const result of results) {
|
|
2059
|
+
updates.run(getReinforcedStrength(result), result.id);
|
|
2060
|
+
}
|
|
2061
|
+
});
|
|
2062
|
+
reinforce();
|
|
2032
2063
|
}
|
|
2033
2064
|
|
|
2034
2065
|
const enrichedResults = this._attachSourceContext(
|
|
@@ -2061,7 +2092,8 @@ class MemoryManager {
|
|
|
2061
2092
|
*/
|
|
2062
2093
|
listMemories(userId, { category, limit = 50, offset = 0, includeArchived = false, agentId = null } = {}) {
|
|
2063
2094
|
const scopedAgentId = this._agentId(userId, { agentId });
|
|
2064
|
-
let sql = `SELECT id, category, content, summary, importance, confidence, access_count,
|
|
2095
|
+
let sql = `SELECT id, category, content, summary, importance, confidence, access_count,
|
|
2096
|
+
memory_strength, last_accessed_at, pinned, archived, created_at, updated_at,
|
|
2065
2097
|
scope_type, scope_id, source_type, source_id, source_label, stale_after_days, metadata_json
|
|
2066
2098
|
FROM memories WHERE user_id = ? AND agent_id = ? AND archived = ?`;
|
|
2067
2099
|
const params = [userId, scopedAgentId, includeArchived ? 1 : 0];
|
|
@@ -2197,6 +2229,27 @@ class MemoryManager {
|
|
|
2197
2229
|
return result.changes || 0;
|
|
2198
2230
|
}
|
|
2199
2231
|
|
|
2232
|
+
archiveWeakMemories(userId, options = {}) {
|
|
2233
|
+
const agentId = this._agentId(userId, options);
|
|
2234
|
+
const limit = Math.max(1, Math.min(Number(options.limit) || 100, 500));
|
|
2235
|
+
const rows = db.prepare(
|
|
2236
|
+
`SELECT id, category, importance, archived, memory_strength, last_accessed_at,
|
|
2237
|
+
pinned, stale_after_days, created_at, updated_at
|
|
2238
|
+
FROM memories
|
|
2239
|
+
WHERE user_id = ? AND agent_id = ? AND archived = 0
|
|
2240
|
+
ORDER BY updated_at ASC
|
|
2241
|
+
LIMIT ?`
|
|
2242
|
+
).all(userId, agentId, limit);
|
|
2243
|
+
const archiveIds = rows
|
|
2244
|
+
.filter((row) => isArchiveEligible(row, options))
|
|
2245
|
+
.map((row) => row.id);
|
|
2246
|
+
return {
|
|
2247
|
+
scanned: rows.length,
|
|
2248
|
+
archived: this.archiveMemories(archiveIds, true, userId),
|
|
2249
|
+
memoryIds: archiveIds,
|
|
2250
|
+
};
|
|
2251
|
+
}
|
|
2252
|
+
|
|
2200
2253
|
// ─────────────────────────────────────────────────────────────────────────
|
|
2201
2254
|
// Core Memory (always-injected key-value pairs)
|
|
2202
2255
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -2942,7 +2995,14 @@ class MemoryManager {
|
|
|
2942
2995
|
if (recalled.length) {
|
|
2943
2996
|
const memoryLines = [];
|
|
2944
2997
|
let memoryChars = 0;
|
|
2998
|
+
const seenMemoryText = new Set();
|
|
2999
|
+
const categoryCounts = new Map();
|
|
2945
3000
|
for (const m of recalled) {
|
|
3001
|
+
const category = normalizeMemoryCategory(m.category);
|
|
3002
|
+
const summaryKey = stableHash(summarizeForPrompt(m).slice(0, 220));
|
|
3003
|
+
if (seenMemoryText.has(summaryKey)) continue;
|
|
3004
|
+
const categoryCount = categoryCounts.get(category) || 0;
|
|
3005
|
+
if (memoryLines.length >= 3 && categoryCount >= 2) continue;
|
|
2946
3006
|
const badge = m.category !== 'episodic' ? ` [${m.category}]` : '';
|
|
2947
3007
|
const entities = Array.isArray(m.entities) && m.entities.length
|
|
2948
3008
|
? ` (entities: ${m.entities.slice(0, 4).map((entity) => entity.name).join(', ')})`
|
|
@@ -2963,6 +3023,8 @@ class MemoryManager {
|
|
|
2963
3023
|
const line = `- ${summarizeForPrompt(m)}${badge}${entities}${historySuffix}${sourceSuffix}`;
|
|
2964
3024
|
if (memoryLines.length && memoryChars + line.length > 1600) break;
|
|
2965
3025
|
memoryLines.push(line);
|
|
3026
|
+
seenMemoryText.add(summaryKey);
|
|
3027
|
+
categoryCounts.set(category, categoryCount + 1);
|
|
2966
3028
|
memoryChars += line.length;
|
|
2967
3029
|
}
|
|
2968
3030
|
sections.push(`Relevant memory:\n${memoryLines.join('\n')}`);
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const PROTECTED_CATEGORIES = new Set(['identity', 'preferences', 'assistant_self']);
|
|
4
|
+
|
|
5
|
+
function clamp(value, min, max, fallback) {
|
|
6
|
+
const number = Number(value);
|
|
7
|
+
return Number.isFinite(number) ? Math.max(min, Math.min(max, number)) : fallback;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function parseTime(value) {
|
|
11
|
+
const timestamp = Date.parse(String(value || ''));
|
|
12
|
+
return Number.isFinite(timestamp) ? timestamp : null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function ageDaysSince(value, now = Date.now()) {
|
|
16
|
+
const timestamp = parseTime(value);
|
|
17
|
+
if (!timestamp) return 0;
|
|
18
|
+
return Math.max(0, (now - timestamp) / (1000 * 60 * 60 * 24));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function isPinnedMemory(memory) {
|
|
22
|
+
if (Number(memory?.pinned || 0) === 1) return true;
|
|
23
|
+
if (PROTECTED_CATEGORIES.has(String(memory?.category || ''))) return true;
|
|
24
|
+
return Number(memory?.importance || 0) >= 9;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function getDecayedStrength(memory, now = Date.now()) {
|
|
28
|
+
const strength = clamp(memory?.memory_strength, 0.05, 2, 1);
|
|
29
|
+
if (isPinnedMemory(memory)) return strength;
|
|
30
|
+
const halfLifeDays = clamp(memory?.stale_after_days, 14, 365, 90);
|
|
31
|
+
const lastRelevantAt = memory?.last_accessed_at || memory?.updated_at || memory?.created_at;
|
|
32
|
+
const age = ageDaysSince(lastRelevantAt, now);
|
|
33
|
+
return clamp(strength * Math.pow(0.5, age / halfLifeDays), 0.01, 2, strength);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function getRetentionScoreMultiplier(memory, now = Date.now()) {
|
|
37
|
+
if (isPinnedMemory(memory)) return 1.04;
|
|
38
|
+
const decayed = getDecayedStrength(memory, now);
|
|
39
|
+
return clamp(0.82 + (decayed * 0.14), 0.65, 1.08, 1);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function getReinforcedStrength(memory) {
|
|
43
|
+
const current = clamp(memory?.memory_strength, 0.05, 2, 1);
|
|
44
|
+
const importanceBoost = clamp(memory?.importance, 1, 10, 5) / 250;
|
|
45
|
+
return clamp(current + 0.06 + importanceBoost, 0.05, 2, 1.08);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function isArchiveEligible(memory, options = {}) {
|
|
49
|
+
if (!memory || isPinnedMemory(memory)) return false;
|
|
50
|
+
if (Number(memory.archived || 0) === 1) return false;
|
|
51
|
+
if (Number(memory.importance || 0) >= 8) return false;
|
|
52
|
+
|
|
53
|
+
const now = options.now || Date.now();
|
|
54
|
+
const minAgeDays = clamp(options.minAgeDays, 1, 365, 45);
|
|
55
|
+
const threshold = clamp(options.strengthThreshold, 0.01, 1, 0.18);
|
|
56
|
+
const age = ageDaysSince(memory.updated_at || memory.created_at, now);
|
|
57
|
+
return age >= minAgeDays && getDecayedStrength(memory, now) < threshold;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
module.exports = {
|
|
61
|
+
getDecayedStrength,
|
|
62
|
+
getReinforcedStrength,
|
|
63
|
+
getRetentionScoreMultiplier,
|
|
64
|
+
isArchiveEligible,
|
|
65
|
+
isPinnedMemory,
|
|
66
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const INTENTS = new Set(['broad', 'procedural', 'episodic', 'profile']);
|
|
4
|
+
|
|
5
|
+
function hasAny(text, patterns) {
|
|
6
|
+
return patterns.some((pattern) => pattern.test(text));
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function normalizeIntent(intent) {
|
|
10
|
+
return INTENTS.has(intent) ? intent : 'broad';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function routeMemoryQuery(query) {
|
|
14
|
+
const text = String(query || '').toLowerCase();
|
|
15
|
+
if (!text.trim()) {
|
|
16
|
+
return { intent: 'broad', confidence: 0, categoryBoosts: {} };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const procedural = hasAny(text, [
|
|
20
|
+
/\bhow (?:do|did|should|can) (?:i|we|you)\b/,
|
|
21
|
+
/\b(?:workflow|procedure|runbook|playbook|steps?|checklist|process)\b/,
|
|
22
|
+
/\b(?:repeat|reusable|again|same way|tool[- ]use|command sequence)\b/,
|
|
23
|
+
]);
|
|
24
|
+
if (procedural) {
|
|
25
|
+
return {
|
|
26
|
+
intent: 'procedural',
|
|
27
|
+
confidence: 0.76,
|
|
28
|
+
categoryBoosts: { procedural: 0.26, tasks: 0.08, episodic: 0.04 },
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const episodic = hasAny(text, [
|
|
33
|
+
/\b(?:when|what happened|last time|previously|yesterday|today|last week|last month)\b/,
|
|
34
|
+
/\b(?:meeting|event|conversation|session|incident|task run)\b/,
|
|
35
|
+
]);
|
|
36
|
+
if (episodic) {
|
|
37
|
+
return {
|
|
38
|
+
intent: 'episodic',
|
|
39
|
+
confidence: 0.7,
|
|
40
|
+
categoryBoosts: { episodic: 0.18, events: 0.16, tasks: 0.1 },
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const profile = hasAny(text, [
|
|
45
|
+
/\b(?:prefer|preference|likes?|dislikes?|my name|who am i|about me)\b/,
|
|
46
|
+
/\b(?:profile|personality|standing instruction|how should you)\b/,
|
|
47
|
+
]);
|
|
48
|
+
if (profile) {
|
|
49
|
+
return {
|
|
50
|
+
intent: 'profile',
|
|
51
|
+
confidence: 0.72,
|
|
52
|
+
categoryBoosts: { preferences: 0.2, identity: 0.16, assistant_self: 0.12 },
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return { intent: 'broad', confidence: 0.35, categoryBoosts: {} };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function getRouteCategoryBoost(route, category) {
|
|
60
|
+
const intent = normalizeIntent(route?.intent);
|
|
61
|
+
if (intent === 'broad' || Number(route?.confidence || 0) < 0.6) return 0;
|
|
62
|
+
return Number(route?.categoryBoosts?.[category] || 0);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
module.exports = {
|
|
66
|
+
getRouteCategoryBoost,
|
|
67
|
+
routeMemoryQuery,
|
|
68
|
+
};
|
|
@@ -37,7 +37,7 @@ const SHARED_RAW_ID_PLATFORMS = new Set([
|
|
|
37
37
|
'webchat',
|
|
38
38
|
]);
|
|
39
39
|
|
|
40
|
-
const DIRECT_ONLY_PHONE_PLATFORMS = new Set(['telnyx'
|
|
40
|
+
const DIRECT_ONLY_PHONE_PLATFORMS = new Set(['telnyx']);
|
|
41
41
|
|
|
42
42
|
function capabilityTemplate(overrides = {}) {
|
|
43
43
|
return Object.freeze({
|
|
@@ -300,6 +300,17 @@ function addRule(bucket, rule, state) {
|
|
|
300
300
|
state[bucket].push(rule);
|
|
301
301
|
}
|
|
302
302
|
|
|
303
|
+
function isWhatsAppGroupId(value) {
|
|
304
|
+
return String(value || '').trim().toLowerCase().split(':')[0].endsWith('@g.us');
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function normalizeWhatsAppDirectId(value) {
|
|
308
|
+
const raw = String(value || '').trim().toLowerCase();
|
|
309
|
+
const base = raw.includes('@') ? raw.split('@')[0] : raw;
|
|
310
|
+
const primary = base.includes(':') ? base.split(':')[0] : base;
|
|
311
|
+
return primary.replace(/\D/g, '') || primary;
|
|
312
|
+
}
|
|
313
|
+
|
|
303
314
|
function migrateLegacyWhitelist(platform, entries) {
|
|
304
315
|
const capabilities = getPlatformAccessCapabilities(platform);
|
|
305
316
|
const policy = createDefaultAccessPolicy(platform);
|
|
@@ -328,6 +339,22 @@ function migrateLegacyWhitelist(platform, entries) {
|
|
|
328
339
|
const value = sanitizeValue(scope || 'chat', rawValue);
|
|
329
340
|
if (!value) continue;
|
|
330
341
|
|
|
342
|
+
if (platform === 'whatsapp') {
|
|
343
|
+
if (scope === 'group' || isWhatsAppGroupId(value)) {
|
|
344
|
+
addRule('sharedSpaceRules', normalizeRule({ scope: 'group', value }, new Set(capabilities.sharedSpaceRuleScopes)), state);
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
const directValue = scope === 'chat' ? value : normalizeWhatsAppDirectId(value);
|
|
348
|
+
if (!directValue) continue;
|
|
349
|
+
if (scope === 'chat') {
|
|
350
|
+
addRule('directRules', normalizeRule({ scope: 'chat', value: directValue }, new Set(capabilities.directRuleScopes)), state);
|
|
351
|
+
} else {
|
|
352
|
+
addRule('directRules', normalizeRule({ scope: 'phone_number', value: directValue }, new Set(capabilities.directRuleScopes)), state);
|
|
353
|
+
addRule('sharedActorRules', normalizeRule({ scope: 'phone_number', value: directValue }, new Set(capabilities.sharedActorRuleScopes)), state);
|
|
354
|
+
}
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
|
|
331
358
|
if (DIRECT_ONLY_PHONE_PLATFORMS.has(platform)) {
|
|
332
359
|
addRule('directRules', normalizeRule({ scope: 'phone_number', value }, new Set(capabilities.directRuleScopes)), state);
|
|
333
360
|
continue;
|
|
@@ -641,7 +668,17 @@ function classifyRecentTarget(platform, row) {
|
|
|
641
668
|
if (!chatId && !sender) return null;
|
|
642
669
|
|
|
643
670
|
const isDirect = !String(metadata.isGroup || '').match(/^(true|1)$/i) && (!chatId || chatId === sender || chatId === `dm_${sender}`);
|
|
644
|
-
if (
|
|
671
|
+
if (platform === 'whatsapp' && !isDirect && chatId) {
|
|
672
|
+
return {
|
|
673
|
+
source: 'recent',
|
|
674
|
+
bucket: 'sharedSpaceRules',
|
|
675
|
+
scope: 'group',
|
|
676
|
+
value: chatId,
|
|
677
|
+
label: groupName || chatId,
|
|
678
|
+
subtitle: 'Recent WhatsApp group',
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
if (DIRECT_ONLY_PHONE_PLATFORMS.has(platform) || platform === 'whatsapp') {
|
|
645
682
|
const value = normalizePhone(sender || chatId);
|
|
646
683
|
if (!value) return null;
|
|
647
684
|
return {
|
|
@@ -818,14 +818,19 @@ class MessagingManager extends EventEmitter {
|
|
|
818
818
|
);
|
|
819
819
|
}
|
|
820
820
|
|
|
821
|
-
async
|
|
821
|
+
async listAccessTargets(userId, platformName, options = {}) {
|
|
822
822
|
const agentId = this._agentId(userId, options);
|
|
823
823
|
const key = this._key(userId, agentId, platformName);
|
|
824
824
|
const platform = this.platforms.get(key);
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
discoveredTargets = await Promise.resolve(platform.listAccessTargets()).catch(() => []);
|
|
825
|
+
if (!platform || typeof platform.listAccessTargets !== 'function') {
|
|
826
|
+
return [];
|
|
828
827
|
}
|
|
828
|
+
return Promise.resolve(platform.listAccessTargets()).catch(() => []);
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
async getAccessCatalog(userId, platformName, options = {}) {
|
|
832
|
+
const agentId = this._agentId(userId, options);
|
|
833
|
+
const discoveredTargets = await this.listAccessTargets(userId, platformName, { agentId });
|
|
829
834
|
|
|
830
835
|
const recentRows = db.prepare(
|
|
831
836
|
`SELECT platform_chat_id, metadata
|