neoagent 3.2.1-beta.9 → 3.3.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 (70) hide show
  1. package/flutter_app/lib/main_app_shell.dart +178 -34
  2. package/flutter_app/lib/main_chat.dart +542 -80
  3. package/flutter_app/lib/main_controller.dart +59 -4
  4. package/flutter_app/lib/main_models.dart +171 -22
  5. package/flutter_app/lib/main_operations.dart +0 -49
  6. package/flutter_app/lib/main_settings.dart +338 -0
  7. package/flutter_app/lib/src/backend_client.dart +19 -0
  8. package/package.json +1 -1
  9. package/server/db/database.js +2 -2
  10. package/server/http/routes.js +1 -0
  11. package/server/public/.last_build_id +1 -1
  12. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  13. package/server/public/flutter_bootstrap.js +1 -1
  14. package/server/public/main.dart.js +85079 -83904
  15. package/server/routes/behavior.js +80 -0
  16. package/server/routes/settings.js +4 -0
  17. package/server/services/agents/manager.js +1 -1
  18. package/server/services/ai/history.js +1 -1
  19. package/server/services/ai/loop/agent_engine_core.js +110 -17
  20. package/server/services/ai/loop/blank_recovery.js +5 -4
  21. package/server/services/ai/loop/completion_judge.js +226 -33
  22. package/server/services/ai/loop/conversation_loop.js +92 -34
  23. package/server/services/ai/loop/messaging_delivery.js +47 -0
  24. package/server/services/ai/loop/progress_classification.js +2 -0
  25. package/server/services/ai/loopPolicy.js +24 -2
  26. package/server/services/ai/messagingFallback.js +17 -17
  27. package/server/services/ai/model_failure_cache.js +7 -0
  28. package/server/services/ai/systemPrompt.js +31 -122
  29. package/server/services/ai/taskAnalysis.js +86 -12
  30. package/server/services/ai/toolEvidence.js +68 -224
  31. package/server/services/ai/tools.js +60 -19
  32. package/server/services/behavior/config.js +251 -0
  33. package/server/services/behavior/defaults.js +68 -0
  34. package/server/services/behavior/delivery.js +176 -0
  35. package/server/services/behavior/index.js +43 -0
  36. package/server/services/behavior/model_client.js +35 -0
  37. package/server/services/behavior/modules/agent_identity.js +37 -0
  38. package/server/services/behavior/modules/channel_style.js +28 -0
  39. package/server/services/behavior/modules/index.js +29 -0
  40. package/server/services/behavior/modules/norms.js +101 -0
  41. package/server/services/behavior/modules/persona.js +172 -0
  42. package/server/services/behavior/modules/persona_prompt.js +238 -0
  43. package/server/services/behavior/modules/social_memory.js +86 -0
  44. package/server/services/behavior/modules/social_observability.js +94 -0
  45. package/server/services/behavior/modules/social_signals.js +41 -0
  46. package/server/services/behavior/modules/theory_of_mind.js +118 -0
  47. package/server/services/behavior/modules/turn_taking.js +238 -0
  48. package/server/services/behavior/pipeline.js +341 -0
  49. package/server/services/behavior/registry.js +78 -0
  50. package/server/services/behavior/signals.js +107 -0
  51. package/server/services/behavior/state.js +99 -0
  52. package/server/services/behavior/system_prompt.js +75 -0
  53. package/server/services/memory/manager.js +14 -33
  54. package/server/services/messaging/access_policy.js +269 -74
  55. package/server/services/messaging/automation.js +158 -27
  56. package/server/services/messaging/discord.js +11 -1
  57. package/server/services/messaging/formatting_guides.js +5 -4
  58. package/server/services/messaging/http_platforms.js +73 -28
  59. package/server/services/messaging/inbound_queue.js +74 -13
  60. package/server/services/messaging/inbound_store.js +33 -0
  61. package/server/services/messaging/manager.js +57 -5
  62. package/server/services/messaging/telegram.js +10 -1
  63. package/server/services/messaging/whatsapp.js +11 -0
  64. package/server/services/social_reach/channels/social_video.js +1 -1
  65. package/server/services/social_video/captions.js +2 -2
  66. package/server/services/social_video/service.js +194 -29
  67. package/server/services/voice/message.js +1 -1
  68. package/server/services/voice/runtime.js +2 -2
  69. package/server/utils/logger.js +19 -0
  70. package/server/services/ai/terminal_reply.js +0 -57
@@ -1824,6 +1824,13 @@ class MemoryManager {
1824
1824
  const agentId = this._agentId(userId, options);
1825
1825
  const normalized = normalizeMemoryCandidates(candidates);
1826
1826
  const memoryIds = [];
1827
+ const scope = normalizeScope(options.scope, agentId);
1828
+ const sourceRef = normalizeSourceRef(options.sourceRef || {
1829
+ sourceType: 'conversation_consolidation',
1830
+ sourceId: options.runId || options.conversationId || null,
1831
+ sourceLabel: options.conversationId ? 'Conversation memory' : 'Agent memory',
1832
+ });
1833
+ const metadata = parseJsonObject(options.metadata, {});
1827
1834
 
1828
1835
  for (const candidate of normalized) {
1829
1836
  const memoryId = await this.saveMemory(
@@ -1835,22 +1842,16 @@ class MemoryManager {
1835
1842
  agentId,
1836
1843
  confidence: candidate.confidence,
1837
1844
  facts: [candidate],
1838
- sourceRef: {
1839
- sourceType: 'conversation_consolidation',
1840
- sourceId: options.runId || options.conversationId || null,
1841
- sourceLabel: options.conversationId ? 'Conversation memory' : 'Agent memory',
1842
- },
1843
- scope: {
1844
- scopeType: 'agent',
1845
- scopeId: agentId,
1846
- },
1845
+ sourceRef,
1846
+ scope,
1847
1847
  metadata: {
1848
1848
  relation: candidate.relation,
1849
1849
  isStatic: candidate.isStatic,
1850
1850
  evidence: candidate.evidence || null,
1851
- trustLevel: 'user_or_verified_conversation',
1851
+ trustLevel: metadata.trustLevel || 'user_or_verified_conversation',
1852
1852
  conversationId: options.conversationId || null,
1853
1853
  runId: options.runId || null,
1854
+ ...metadata,
1854
1855
  },
1855
1856
  signal: options.signal,
1856
1857
  },
@@ -2971,30 +2972,10 @@ class MemoryManager {
2971
2972
  async buildContext(userId = null, options = {}) {
2972
2973
  let ctx = '';
2973
2974
  const agentId = this._agentId(userId, options);
2974
-
2975
- const behaviorNotes = this.getAssistantBehaviorNotes(userId, { agentId });
2976
- if (behaviorNotes) {
2977
- ctx += `## Assistant Behavior Notes\n`;
2978
- ctx += `These are durable preferences for how the assistant should usually behave. Follow system rules and the active user request first.\n`;
2979
- ctx += `${behaviorNotes}\n\n`;
2980
- }
2981
-
2982
- if (userId != null) {
2983
- const selfState = this.getAssistantSelfState(userId, { agentId });
2984
- if (Object.keys(selfState.identity || {}).length || Object.keys(selfState.focus || {}).length) {
2985
- ctx += `## Assistant Self State\n`;
2986
- if (Object.keys(selfState.identity || {}).length) {
2987
- ctx += `Identity: ${JSON.stringify(selfState.identity)}\n`;
2988
- }
2989
- if (Object.keys(selfState.focus || {}).length) {
2990
- ctx += `Focus: ${JSON.stringify(selfState.focus)}\n`;
2991
- }
2992
- ctx += '\n';
2993
- }
2994
- }
2975
+ const sharedAudience = options.audience === 'shared';
2995
2976
 
2996
2977
  // 2. Core memory — always-relevant user facts
2997
- if (userId != null) {
2978
+ if (userId != null && !sharedAudience) {
2998
2979
  const core = this.getCoreMemory(userId, { agentId });
2999
2980
  const filteredCore = Object.fromEntries(
3000
2981
  Object.entries(core).filter(([key]) => key !== 'active_context')
@@ -3009,7 +2990,7 @@ class MemoryManager {
3009
2990
  }
3010
2991
  }
3011
2992
 
3012
- if (userId != null) {
2993
+ if (userId != null && !sharedAudience) {
3013
2994
  const profile = this.getUserProfile(userId, { agentId });
3014
2995
  if (profile.static.length || profile.dynamic.length) {
3015
2996
  ctx += `## Auto-Maintained User Profile\n`;
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const ACCESS_MODES = Object.freeze(['allowlist', 'open', 'disabled']);
4
+ const ACCESS_POLICY_SCHEMA_VERSION = 3;
4
5
  const ACCESS_MODE_SET = new Set(ACCESS_MODES);
5
6
  const RULE_SCOPES = Object.freeze([
6
7
  'user',
@@ -44,6 +45,7 @@ function capabilityTemplate(overrides = {}) {
44
45
  supportsDirectPolicy: true,
45
46
  supportsSharedPolicy: true,
46
47
  supportsMentionGate: false,
48
+ supportsUntaggedGroupToggle: true,
47
49
  supportsDiscovery: false,
48
50
  directRuleScopes: DIRECT_RULE_SCOPES,
49
51
  sharedSpaceRuleScopes: SHARED_SPACE_RULE_SCOPES,
@@ -55,7 +57,7 @@ function capabilityTemplate(overrides = {}) {
55
57
 
56
58
  const PLATFORM_CAPABILITIES = Object.freeze({
57
59
  whatsapp: capabilityTemplate({
58
- supportsMentionGate: false,
60
+ supportsMentionGate: true,
59
61
  supportsDiscovery: true,
60
62
  directRuleScopes: Object.freeze(['phone_number', 'user', 'chat']),
61
63
  sharedSpaceRuleScopes: Object.freeze(['group', 'chat']),
@@ -65,6 +67,7 @@ const PLATFORM_CAPABILITIES = Object.freeze({
65
67
  telnyx: capabilityTemplate({
66
68
  supportsSharedPolicy: false,
67
69
  supportsMentionGate: false,
70
+ supportsUntaggedGroupToggle: false,
68
71
  supportsDiscovery: false,
69
72
  directRuleScopes: Object.freeze(['phone_number']),
70
73
  sharedSpaceRuleScopes: Object.freeze([]),
@@ -96,14 +99,14 @@ const PLATFORM_CAPABILITIES = Object.freeze({
96
99
  manualEntryHint: 'Add a Slack user or channel id.',
97
100
  }),
98
101
  google_chat: capabilityTemplate({
99
- supportsMentionGate: false,
102
+ supportsMentionGate: true,
100
103
  supportsDiscovery: false,
101
104
  directRuleScopes: Object.freeze(['user', 'chat']),
102
105
  sharedSpaceRuleScopes: Object.freeze(['room', 'chat']),
103
106
  sharedActorRuleScopes: Object.freeze(['user']),
104
107
  }),
105
108
  teams: capabilityTemplate({
106
- supportsMentionGate: false,
109
+ supportsMentionGate: true,
107
110
  supportsDiscovery: false,
108
111
  directRuleScopes: Object.freeze(['user', 'chat']),
109
112
  sharedSpaceRuleScopes: Object.freeze(['channel', 'chat']),
@@ -117,7 +120,7 @@ const PLATFORM_CAPABILITIES = Object.freeze({
117
120
  sharedActorRuleScopes: Object.freeze(['user']),
118
121
  }),
119
122
  signal: capabilityTemplate({
120
- supportsMentionGate: false,
123
+ supportsMentionGate: true,
121
124
  supportsDiscovery: false,
122
125
  directRuleScopes: Object.freeze(['phone_number', 'user', 'chat']),
123
126
  sharedSpaceRuleScopes: Object.freeze(['group', 'chat']),
@@ -230,12 +233,15 @@ function defaultSharedPolicyForPlatform(platform) {
230
233
  function createDefaultAccessPolicy(platform) {
231
234
  const capabilities = getPlatformAccessCapabilities(platform);
232
235
  return {
236
+ schemaVersion: ACCESS_POLICY_SCHEMA_VERSION,
233
237
  directPolicy: 'allowlist',
234
238
  sharedPolicy: capabilities.supportsSharedPolicy ? 'allowlist' : 'disabled',
235
- requireMentionInShared: capabilities.supportsMentionGate,
239
+ defaultAllowUntaggedInShared: true,
236
240
  directRules: [],
237
241
  sharedSpaceRules: [],
238
242
  sharedActorRules: [],
243
+ sharedMemberRules: [],
244
+ sharedParticipationRules: [],
239
245
  };
240
246
  }
241
247
 
@@ -253,7 +259,12 @@ function dedupeRules(rules) {
253
259
  const seen = new Set();
254
260
  const result = [];
255
261
  for (const rule of rules) {
256
- const key = `${rule.scope}:${rule.value}`;
262
+ const key = [
263
+ rule.scope,
264
+ rule.value,
265
+ rule.spaceScope || '',
266
+ rule.spaceValue || '',
267
+ ].join(':');
257
268
  if (seen.has(key)) continue;
258
269
  seen.add(key);
259
270
  result.push(rule);
@@ -261,6 +272,31 @@ function dedupeRules(rules) {
261
272
  return result;
262
273
  }
263
274
 
275
+ function normalizeSharedMemberRule(rule, actorScopes, spaceScopes) {
276
+ const actorRule = normalizeRule(rule, actorScopes);
277
+ if (!actorRule) return null;
278
+ const spaceScope = normalizeScope(rule.spaceScope);
279
+ if (!RULE_SCOPE_SET.has(spaceScope) || !spaceScopes.has(spaceScope)) return null;
280
+ const spaceValue = sanitizeValue(spaceScope, rule.spaceValue);
281
+ if (!spaceValue) return null;
282
+ const spaceLabel = String(rule.spaceLabel || '').trim();
283
+ return {
284
+ ...actorRule,
285
+ spaceScope,
286
+ spaceValue,
287
+ ...(spaceLabel ? { spaceLabel } : {}),
288
+ };
289
+ }
290
+
291
+ function normalizeSharedParticipationRule(rule, spaceScopes) {
292
+ const normalized = normalizeRule(rule, spaceScopes);
293
+ if (!normalized) return null;
294
+ return {
295
+ ...normalized,
296
+ allowUntagged: rule.allowUntagged === true,
297
+ };
298
+ }
299
+
264
300
  function normalizeAccessPolicy(platform, value) {
265
301
  const capabilities = getPlatformAccessCapabilities(platform);
266
302
  const defaults = createDefaultAccessPolicy(platform);
@@ -268,28 +304,68 @@ function normalizeAccessPolicy(platform, value) {
268
304
  const sharedSpaceScopes = new Set(capabilities.sharedSpaceRuleScopes);
269
305
  const sharedActorScopes = new Set(capabilities.sharedActorRuleScopes);
270
306
  const raw = value && typeof value === 'object' ? value : {};
307
+ let rawSharedSpaceRules = Array.isArray(raw.sharedSpaceRules) ? raw.sharedSpaceRules : [];
308
+ let rawSharedActorRules = Array.isArray(raw.sharedActorRules) ? raw.sharedActorRules : [];
309
+ let rawSharedMemberRules = Array.isArray(raw.sharedMemberRules) ? raw.sharedMemberRules : [];
310
+ const rawSharedParticipationRules = Array.isArray(raw.sharedParticipationRules)
311
+ ? raw.sharedParticipationRules
312
+ : [];
313
+ let rawSharedPolicy = raw.sharedPolicy;
314
+ const isLegacyParticipationPolicy = Number(raw.schemaVersion || 0) < 3;
315
+
316
+ if (
317
+ Number(raw.schemaVersion || 0) < 2
318
+ && rawSharedMemberRules.length === 0
319
+ && rawSharedActorRules.length > 0
320
+ ) {
321
+ if (normalizeMode(rawSharedPolicy, defaults.sharedPolicy) === 'open') {
322
+ rawSharedPolicy = 'allowlist';
323
+ rawSharedSpaceRules = [];
324
+ } else if (rawSharedSpaceRules.length > 0) {
325
+ rawSharedMemberRules = rawSharedActorRules.flatMap((actorRule) =>
326
+ rawSharedSpaceRules.map((spaceRule) => ({
327
+ ...actorRule,
328
+ spaceScope: spaceRule.scope,
329
+ spaceValue: spaceRule.value,
330
+ spaceLabel: spaceRule.label,
331
+ })));
332
+ rawSharedSpaceRules = [];
333
+ rawSharedActorRules = [];
334
+ }
335
+ }
336
+
271
337
  const normalized = {
338
+ schemaVersion: ACCESS_POLICY_SCHEMA_VERSION,
272
339
  directPolicy: normalizeMode(raw.directPolicy, defaults.directPolicy),
273
- sharedPolicy: normalizeMode(raw.sharedPolicy, defaults.sharedPolicy),
274
- requireMentionInShared: capabilities.supportsMentionGate
275
- ? raw.requireMentionInShared !== false
276
- : false,
340
+ sharedPolicy: normalizeMode(rawSharedPolicy, defaults.sharedPolicy),
341
+ defaultAllowUntaggedInShared: capabilities.supportsUntaggedGroupToggle
342
+ ? (isLegacyParticipationPolicy
343
+ ? raw.requireMentionInShared !== true
344
+ : raw.defaultAllowUntaggedInShared !== false)
345
+ : true,
277
346
  directRules: dedupeRules((Array.isArray(raw.directRules) ? raw.directRules : [])
278
347
  .map((rule) => normalizeRule(rule, directScopes))
279
348
  .filter(Boolean)),
280
- sharedSpaceRules: dedupeRules((Array.isArray(raw.sharedSpaceRules) ? raw.sharedSpaceRules : [])
349
+ sharedSpaceRules: dedupeRules(rawSharedSpaceRules
281
350
  .map((rule) => normalizeRule(rule, sharedSpaceScopes))
282
351
  .filter(Boolean)),
283
- sharedActorRules: dedupeRules((Array.isArray(raw.sharedActorRules) ? raw.sharedActorRules : [])
352
+ sharedActorRules: dedupeRules(rawSharedActorRules
284
353
  .map((rule) => normalizeRule(rule, sharedActorScopes))
285
354
  .filter(Boolean)),
355
+ sharedMemberRules: dedupeRules(rawSharedMemberRules
356
+ .map((rule) => normalizeSharedMemberRule(rule, sharedActorScopes, sharedSpaceScopes))
357
+ .filter(Boolean)),
358
+ sharedParticipationRules: dedupeRules(rawSharedParticipationRules
359
+ .map((rule) => normalizeSharedParticipationRule(rule, sharedSpaceScopes))
360
+ .filter(Boolean)),
286
361
  };
287
-
288
362
  if (!capabilities.supportsSharedPolicy) {
289
363
  normalized.sharedPolicy = 'disabled';
290
- normalized.requireMentionInShared = false;
364
+ normalized.defaultAllowUntaggedInShared = true;
291
365
  normalized.sharedSpaceRules = [];
292
366
  normalized.sharedActorRules = [];
367
+ normalized.sharedMemberRules = [];
368
+ normalized.sharedParticipationRules = [];
293
369
  }
294
370
 
295
371
  return normalized;
@@ -318,6 +394,8 @@ function migrateLegacyWhitelist(platform, entries) {
318
394
  directRules: [],
319
395
  sharedSpaceRules: [],
320
396
  sharedActorRules: [],
397
+ sharedMemberRules: [],
398
+ sharedParticipationRules: [],
321
399
  };
322
400
  const list = Array.isArray(entries)
323
401
  ? entries.map((item) => String(item || '').trim()).filter(Boolean)
@@ -421,6 +499,8 @@ function migrateLegacyWhitelist(platform, entries) {
421
499
  directRules: state.directRules,
422
500
  sharedSpaceRules: state.sharedSpaceRules,
423
501
  sharedActorRules: state.sharedActorRules,
502
+ sharedMemberRules: state.sharedMemberRules,
503
+ sharedParticipationRules: state.sharedParticipationRules,
424
504
  });
425
505
  }
426
506
 
@@ -474,6 +554,29 @@ function contextMatchesRule(rule, context) {
474
554
  }
475
555
  }
476
556
 
557
+ function contextMatchesSharedMemberRule(rule, context) {
558
+ if (!contextMatchesRule(rule, context)) return false;
559
+ return contextMatchesRule({
560
+ scope: rule.spaceScope,
561
+ value: rule.spaceValue,
562
+ }, context);
563
+ }
564
+
565
+ function allowUntaggedForContext(policy, context) {
566
+ const scopePriority = {
567
+ group: 4,
568
+ channel: 4,
569
+ room: 4,
570
+ chat: 3,
571
+ server: 2,
572
+ };
573
+ const rule = policy.sharedParticipationRules
574
+ .filter((item) => contextMatchesRule(item, context))
575
+ .sort((left, right) =>
576
+ (scopePriority[right.scope] || 0) - (scopePriority[left.scope] || 0))[0];
577
+ return rule ? rule.allowUntagged : policy.defaultAllowUntaggedInShared;
578
+ }
579
+
477
580
  function evaluateAccessPolicy(policyInput, context, platform) {
478
581
  const capabilities = getPlatformAccessCapabilities(platform);
479
582
  const policy = normalizeAccessPolicy(platform, policyInput);
@@ -485,7 +588,8 @@ function evaluateAccessPolicy(policyInput, context, platform) {
485
588
  return { allowed: false, reason: 'direct_disabled', policy };
486
589
  }
487
590
  if (policy.directPolicy === 'allowlist') {
488
- const directMatch = policy.directRules.some((rule) => contextMatchesRule(rule, context));
591
+ const directMatch = policy.directRules.some((rule) => contextMatchesRule(rule, context))
592
+ || policy.sharedActorRules.some((rule) => contextMatchesRule(rule, context));
489
593
  if (!directMatch) {
490
594
  return { allowed: false, reason: 'direct_not_allowed', policy };
491
595
  }
@@ -499,20 +603,22 @@ function evaluateAccessPolicy(policyInput, context, platform) {
499
603
  }
500
604
  if (policy.sharedPolicy === 'allowlist') {
501
605
  const sharedSpaceMatch = policy.sharedSpaceRules.some((rule) => contextMatchesRule(rule, context));
502
- if (!sharedSpaceMatch) {
503
- return { allowed: false, reason: 'shared_space_not_allowed', policy };
504
- }
505
- }
506
- if (policy.sharedActorRules.length > 0) {
507
606
  const sharedActorMatch = policy.sharedActorRules.some((rule) => contextMatchesRule(rule, context));
508
- if (!sharedActorMatch) {
509
- return { allowed: false, reason: 'shared_actor_not_allowed', policy };
607
+ const sharedMemberMatch = policy.sharedMemberRules.some(
608
+ (rule) => contextMatchesSharedMemberRule(rule, context),
609
+ );
610
+ if (!sharedSpaceMatch && !sharedActorMatch && !sharedMemberMatch) {
611
+ return { allowed: false, reason: 'shared_not_allowed', policy };
510
612
  }
511
613
  }
512
- if (capabilities.supportsMentionGate && policy.requireMentionInShared && !context?.wasMentioned) {
513
- return { allowed: false, reason: 'mention_required', policy };
514
- }
515
- return { allowed: true, reason: 'allowed', policy };
614
+ const allowUntagged = allowUntaggedForContext(policy, context);
615
+ return {
616
+ allowed: true,
617
+ reason: 'allowed',
618
+ policy,
619
+ allowUntagged,
620
+ participationHint: allowUntagged ? 'automatic' : 'mention_only',
621
+ };
516
622
  }
517
623
 
518
624
  return { allowed: false, reason: 'unsupported_context', policy };
@@ -529,27 +635,85 @@ function labelForScope(scope) {
529
635
  }
530
636
  }
531
637
 
532
- function makeSuggestion({ scope, value, label, bucket }) {
638
+ function makeSuggestion({
639
+ scope,
640
+ value,
641
+ label,
642
+ bucket,
643
+ spaceScope = null,
644
+ spaceValue = null,
645
+ spaceLabel = null,
646
+ }) {
533
647
  if (!scope || !value || !bucket) return null;
648
+ const rule = {
649
+ scope,
650
+ value,
651
+ ...(spaceScope && spaceValue ? { spaceScope, spaceValue } : {}),
652
+ ...(spaceLabel ? { spaceLabel } : {}),
653
+ };
534
654
  return {
535
655
  label: label || `Allow ${labelForScope(scope)} ${value}`,
536
656
  prefixedId: scope === 'phone_number' ? value : `${scope}:${value}`,
537
657
  bucket,
538
- rule: { scope, value },
658
+ rule,
539
659
  };
540
660
  }
541
661
 
662
+ function sharedSpaceFromContext(context, options = {}, allowedScopes = []) {
663
+ const scopes = new Set(allowedScopes);
664
+ if (context.channelId && scopes.has('channel')) {
665
+ return {
666
+ scope: 'channel',
667
+ value: context.channelId,
668
+ label: options.channelLabel || context.channelId,
669
+ };
670
+ }
671
+ if (context.roomId && scopes.has('room')) {
672
+ return {
673
+ scope: 'room',
674
+ value: context.roomId,
675
+ label: options.roomLabel || context.roomId,
676
+ };
677
+ }
678
+ if (context.groupId && scopes.has('group')) {
679
+ return {
680
+ scope: 'group',
681
+ value: context.groupId,
682
+ label: options.groupLabel || context.groupId,
683
+ };
684
+ }
685
+ if (context.chatId && scopes.has('chat')) {
686
+ return {
687
+ scope: 'chat',
688
+ value: context.chatId,
689
+ label: context.chatId,
690
+ };
691
+ }
692
+ return null;
693
+ }
694
+
542
695
  function buildBlockedSenderSuggestions(platform, context, options = {}) {
543
696
  const suggestions = [];
544
697
  const senderLabel = String(options.senderName || context.senderId || '').trim();
545
698
  const chatId = String(context.chatId || '').trim();
699
+ const capabilities = getPlatformAccessCapabilities(platform);
700
+ const actorScope = context.phoneNumber ? 'phone_number' : 'user';
701
+ const actorValue = String(context.phoneNumber || context.senderId || '').trim();
702
+ const canUseSharedActor = capabilities.sharedActorRuleScopes.includes(actorScope);
546
703
 
547
704
  if (context.isDirect) {
548
- if (context.phoneNumber) {
705
+ if (actorValue && canUseSharedActor) {
706
+ suggestions.push(makeSuggestion({
707
+ scope: actorScope,
708
+ value: actorValue,
709
+ label: `Allow sender everywhere (${senderLabel || actorValue})`,
710
+ bucket: 'sharedActorRules',
711
+ }));
712
+ } else if (context.phoneNumber) {
549
713
  suggestions.push(makeSuggestion({
550
714
  scope: 'phone_number',
551
715
  value: context.phoneNumber,
552
- label: `Allow number (${senderLabel || context.phoneNumber})`,
716
+ label: `Allow sender (${senderLabel || context.phoneNumber})`,
553
717
  bucket: 'directRules',
554
718
  }));
555
719
  } else if (context.senderId) {
@@ -569,56 +733,53 @@ function buildBlockedSenderSuggestions(platform, context, options = {}) {
569
733
  }));
570
734
  }
571
735
  } else if (context.isShared) {
572
- if (context.senderId) {
736
+ const sharedSpace = sharedSpaceFromContext(
737
+ context,
738
+ options,
739
+ capabilities.sharedSpaceRuleScopes,
740
+ );
741
+ if (actorValue && canUseSharedActor && sharedSpace) {
573
742
  suggestions.push(makeSuggestion({
574
- scope: context.phoneNumber ? 'phone_number' : 'user',
575
- value: context.phoneNumber || context.senderId,
576
- label: `Allow sender (${senderLabel || context.senderId})`,
577
- bucket: 'sharedActorRules',
743
+ scope: actorScope,
744
+ value: actorValue,
745
+ label: `Allow sender in this ${labelForScope(sharedSpace.scope)} only (${senderLabel || actorValue})`,
746
+ bucket: 'sharedMemberRules',
747
+ spaceScope: sharedSpace.scope,
748
+ spaceValue: sharedSpace.value,
749
+ spaceLabel: sharedSpace.label,
578
750
  }));
579
- }
580
- if (context.serverId) {
581
751
  suggestions.push(makeSuggestion({
582
- scope: 'server',
583
- value: context.serverId,
584
- label: `Allow server (${options.serverLabel || context.serverId})`,
585
- bucket: 'sharedSpaceRules',
752
+ scope: actorScope,
753
+ value: actorValue,
754
+ label: `Allow sender everywhere (${senderLabel || actorValue})`,
755
+ bucket: 'sharedActorRules',
586
756
  }));
587
757
  }
588
- if (context.channelId) {
589
- suggestions.push(makeSuggestion({
590
- scope: 'channel',
591
- value: context.channelId,
592
- label: `Allow channel (${options.channelLabel || context.channelId})`,
593
- bucket: 'sharedSpaceRules',
594
- }));
595
- } else if (context.roomId) {
596
- suggestions.push(makeSuggestion({
597
- scope: 'room',
598
- value: context.roomId,
599
- label: `Allow room (${options.roomLabel || context.roomId})`,
600
- bucket: 'sharedSpaceRules',
601
- }));
602
- } else if (context.groupId) {
758
+ if (sharedSpace) {
603
759
  suggestions.push(makeSuggestion({
604
- scope: 'group',
605
- value: context.groupId,
606
- label: `Allow group (${options.groupLabel || context.groupId})`,
607
- bucket: 'sharedSpaceRules',
608
- }));
609
- } else if (chatId) {
610
- suggestions.push(makeSuggestion({
611
- scope: 'chat',
612
- value: chatId,
613
- label: `Allow chat (${chatId})`,
760
+ scope: sharedSpace.scope,
761
+ value: sharedSpace.value,
762
+ label: `Allow everyone in this ${labelForScope(sharedSpace.scope)} (${sharedSpace.label})`,
614
763
  bucket: 'sharedSpaceRules',
615
764
  }));
616
765
  }
617
766
  }
618
767
 
619
768
  return suggestions.filter(Boolean).filter((item, index, list) => {
620
- const key = `${item.bucket}:${item.rule.scope}:${item.rule.value}`;
621
- return list.findIndex((entry) => `${entry.bucket}:${entry.rule.scope}:${entry.rule.value}` === key) === index;
769
+ const key = [
770
+ item.bucket,
771
+ item.rule.scope,
772
+ item.rule.value,
773
+ item.rule.spaceScope || '',
774
+ item.rule.spaceValue || '',
775
+ ].join(':');
776
+ return list.findIndex((entry) => [
777
+ entry.bucket,
778
+ entry.rule.scope,
779
+ entry.rule.value,
780
+ entry.rule.spaceScope || '',
781
+ entry.rule.spaceValue || '',
782
+ ].join(':') === key) === index;
622
783
  });
623
784
  }
624
785
 
@@ -648,11 +809,22 @@ function summarizeAccessPolicy(platform, policyInput) {
648
809
  ];
649
810
  if (capabilities.supportsSharedPolicy) {
650
811
  parts.push(`shared spaces ${policy.sharedPolicy}`);
651
- if (capabilities.supportsMentionGate) {
652
- parts.push(policy.requireMentionInShared ? 'mentions required' : 'mentions optional');
812
+ if (capabilities.supportsUntaggedGroupToggle) {
813
+ const mentionOnlyCount = policy.sharedParticipationRules
814
+ .filter((rule) => !rule.allowUntagged).length;
815
+ if (!policy.defaultAllowUntaggedInShared) {
816
+ parts.push('untagged responses off by default');
817
+ } else {
818
+ parts.push(mentionOnlyCount > 0
819
+ ? `${mentionOnlyCount} tagged-only shared space${mentionOnlyCount === 1 ? '' : 's'}`
820
+ : 'social intelligence for untagged messages');
821
+ }
653
822
  }
654
823
  }
655
- const ruleCount = policy.directRules.length + policy.sharedSpaceRules.length + policy.sharedActorRules.length;
824
+ const ruleCount = policy.directRules.length
825
+ + policy.sharedSpaceRules.length
826
+ + policy.sharedActorRules.length
827
+ + policy.sharedMemberRules.length;
656
828
  if (ruleCount > 0) {
657
829
  parts.push(`${ruleCount} rule${ruleCount == 1 ? '' : 's'}`);
658
830
  }
@@ -661,10 +833,29 @@ function summarizeAccessPolicy(platform, policyInput) {
661
833
 
662
834
  function classifyRecentTarget(platform, row) {
663
835
  const chatId = String(row.platform_chat_id || '').trim();
664
- const sender = String(row.sender || row.sender_id || '').trim();
665
836
  const metadata = row.metadata && typeof row.metadata === 'object' ? row.metadata : {};
837
+ const sender = String(
838
+ row.sender
839
+ || row.sender_id
840
+ || metadata.sender
841
+ || metadata.senderId
842
+ || metadata.sender_id
843
+ || '',
844
+ ).trim();
666
845
  const senderName = String(metadata.senderName || metadata.sender_name || row.sender_name || '').trim();
667
- const groupName = String(metadata.groupName || metadata.group_name || metadata.guildName || metadata.guild_name || '').trim();
846
+ const groupName = String(
847
+ metadata.groupName
848
+ || metadata.group_name
849
+ || metadata.channelName
850
+ || metadata.channel_name
851
+ || metadata.roomName
852
+ || metadata.room_name
853
+ || metadata.serverName
854
+ || metadata.server_name
855
+ || metadata.guildName
856
+ || metadata.guild_name
857
+ || '',
858
+ ).trim();
668
859
  if (!chatId && !sender) return null;
669
860
 
670
861
  const isDirect = !String(metadata.isGroup || '').match(/^(true|1)$/i) && (!chatId || chatId === sender || chatId === `dm_${sender}`);
@@ -714,6 +905,10 @@ function contextFromMessage(msg) {
714
905
  const chatId = String(msg.chatId || '').trim();
715
906
  const senderId = String(msg.sender || '').trim();
716
907
  const normalizedChatId = chatId.startsWith('dm_') ? chatId.slice(3) : chatId;
908
+ const capabilities = getPlatformAccessCapabilities(msg.platform);
909
+ const phoneNumber = capabilities.directRuleScopes.includes('phone_number')
910
+ ? normalizePhone(msg.phoneNumber || senderId)
911
+ : '';
717
912
  return {
718
913
  platform: msg.platform,
719
914
  senderId,
@@ -725,7 +920,7 @@ function contextFromMessage(msg) {
725
920
  serverId: String(msg.guildId || msg.serverId || '').trim(),
726
921
  roomId: String(msg.roomId || '').trim(),
727
922
  roleIds: Array.isArray(msg.roleIds) ? msg.roleIds.map(String) : [],
728
- phoneNumber: normalizePhone(msg.phoneNumber || senderId),
923
+ phoneNumber,
729
924
  wasMentioned: msg.wasMentioned === true,
730
925
  normalizedChatId,
731
926
  };