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.
@@ -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, created_at, updated_at,
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 ids = results.map(r => r.id);
2030
- const placeholders = ids.map(() => '?').join(',');
2031
- db.prepare(`UPDATE memories SET access_count = access_count + 1 WHERE id IN (${placeholders})`).run(...ids);
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, archived, created_at, updated_at,
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', 'whatsapp']);
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 (DIRECT_ONLY_PHONE_PLATFORMS.has(platform)) {
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 getAccessCatalog(userId, platformName, options = {}) {
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
- let discoveredTargets = [];
826
- if (platform?.listAccessTargets) {
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