@swarmclawai/swarmclaw 0.9.3 → 0.9.4

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 (50) hide show
  1. package/README.md +12 -10
  2. package/bundled-skills/google-workspace/SKILL.md +2 -0
  3. package/package.json +1 -1
  4. package/src/app/api/chatrooms/[id]/chat/route.ts +1 -1
  5. package/src/app/api/clawhub/install/route.ts +2 -0
  6. package/src/app/api/skills/[id]/route.ts +4 -0
  7. package/src/app/api/skills/route.ts +4 -0
  8. package/src/components/agents/agent-sheet.tsx +5 -5
  9. package/src/lib/server/agents/agent-thread-session.test.ts +64 -0
  10. package/src/lib/server/agents/agent-thread-session.ts +1 -1
  11. package/src/lib/server/agents/main-agent-loop-advanced.test.ts +77 -0
  12. package/src/lib/server/agents/main-agent-loop.ts +259 -0
  13. package/src/lib/server/agents/orchestrator-lg.ts +12 -8
  14. package/src/lib/server/agents/orchestrator.ts +11 -7
  15. package/src/lib/server/chat-execution/chat-execution-advanced.test.ts +11 -10
  16. package/src/lib/server/chat-execution/chat-execution-session-sync.test.ts +116 -3
  17. package/src/lib/server/chat-execution/chat-execution.ts +74 -26
  18. package/src/lib/server/chat-execution/stream-agent-chat.test.ts +65 -30
  19. package/src/lib/server/chat-execution/stream-agent-chat.ts +69 -25
  20. package/src/lib/server/chatrooms/chatroom-helpers.test.ts +26 -0
  21. package/src/lib/server/chatrooms/chatroom-helpers.ts +11 -8
  22. package/src/lib/server/connectors/contact-boundaries.ts +101 -0
  23. package/src/lib/server/connectors/manager.test.ts +504 -73
  24. package/src/lib/server/connectors/manager.ts +40 -9
  25. package/src/lib/server/connectors/session-consolidation.ts +2 -0
  26. package/src/lib/server/connectors/session-kind.ts +7 -0
  27. package/src/lib/server/connectors/session.test.ts +104 -0
  28. package/src/lib/server/connectors/session.ts +5 -2
  29. package/src/lib/server/identity-continuity.test.ts +4 -3
  30. package/src/lib/server/identity-continuity.ts +8 -4
  31. package/src/lib/server/memory/session-archive-memory.ts +2 -1
  32. package/src/lib/server/session-reset-policy.test.ts +17 -3
  33. package/src/lib/server/session-reset-policy.ts +4 -2
  34. package/src/lib/server/session-tools/connector.ts +11 -10
  35. package/src/lib/server/session-tools/crud.ts +41 -7
  36. package/src/lib/server/session-tools/index.ts +2 -0
  37. package/src/lib/server/session-tools/manage-skills.test.ts +194 -0
  38. package/src/lib/server/session-tools/memory.ts +12 -23
  39. package/src/lib/server/session-tools/skill-runtime.test.ts +175 -0
  40. package/src/lib/server/session-tools/skill-runtime.ts +382 -0
  41. package/src/lib/server/session-tools/skills.ts +575 -0
  42. package/src/lib/server/skills/runtime-skill-resolver.test.ts +162 -0
  43. package/src/lib/server/skills/runtime-skill-resolver.ts +750 -0
  44. package/src/lib/server/skills/skill-discovery.ts +4 -0
  45. package/src/lib/server/skills/skills-normalize.test.ts +28 -0
  46. package/src/lib/server/skills/skills-normalize.ts +93 -1
  47. package/src/lib/server/storage.ts +1 -1
  48. package/src/lib/server/tasks/task-followups.test.ts +124 -0
  49. package/src/lib/server/tasks/task-followups.ts +88 -13
  50. package/src/types/index.ts +26 -2
@@ -592,9 +592,9 @@ describe('sanitizeConnectorOutboundContent', () => {
592
592
  storage.saveSessions({
593
593
  session_1: {
594
594
  id: 'session_1',
595
- name: 'Molly',
595
+ name: 'connector:whatsapp:gran',
596
596
  cwd: process.env.WORKSPACE_DIR,
597
- user: 'default',
597
+ user: 'connector',
598
598
  provider: 'anthropic',
599
599
  model: 'claude-test',
600
600
  claudeSessionId: null,
@@ -720,7 +720,357 @@ describe('sanitizeConnectorOutboundContent', () => {
720
720
  assert.equal(output.health.some((entry: { event?: string }) => entry.event === 'disconnected'), true)
721
721
  })
722
722
 
723
- it('blocks ambiguous connector sends when a thread references multiple people on the same connector', () => {
723
+ it('does not persist connector routing state onto a non-direct session during outbound sends', () => {
724
+ const output = runWithTempDataDir(`
725
+ const storageMod = await import('./src/lib/server/storage')
726
+ const managerMod = await import('./src/lib/server/connectors/manager')
727
+ const pluginsMod = await import('./src/lib/server/plugins')
728
+ const storage = storageMod.default || storageMod
729
+ const manager = managerMod.default || managerMod
730
+ const plugins = pluginsMod.default || pluginsMod
731
+
732
+ const sent = []
733
+ const now = Date.now()
734
+ plugins.getPluginManager().registerBuiltin('test-send-plugin', {
735
+ name: 'Test Send Plugin',
736
+ connectors: [{
737
+ id: 'test-send',
738
+ name: 'Test Send',
739
+ description: 'Outbound send capture',
740
+ startListener: async () => async () => {},
741
+ sendMessage: async (channelId, text, options) => {
742
+ sent.push({ channelId, text, options })
743
+ return { messageId: 'out-1' }
744
+ },
745
+ }],
746
+ })
747
+
748
+ storage.saveSettings({})
749
+ storage.saveConnectors({
750
+ conn_1: {
751
+ id: 'conn_1',
752
+ name: 'Test Send Connector',
753
+ platform: 'test-send',
754
+ agentId: 'agent_1',
755
+ credentialId: null,
756
+ config: { botToken: 'test-token' },
757
+ isEnabled: true,
758
+ status: 'stopped',
759
+ createdAt: now,
760
+ updatedAt: now,
761
+ },
762
+ })
763
+ storage.saveSessions({
764
+ main_thread: {
765
+ id: 'main_thread',
766
+ name: 'Molly',
767
+ cwd: process.env.WORKSPACE_DIR,
768
+ user: 'default',
769
+ provider: 'anthropic',
770
+ model: 'claude-test',
771
+ claudeSessionId: null,
772
+ messages: [],
773
+ createdAt: now,
774
+ lastActiveAt: now,
775
+ sessionType: 'human',
776
+ agentId: 'agent_1',
777
+ plugins: [],
778
+ },
779
+ })
780
+
781
+ try {
782
+ await manager.startConnector('conn_1')
783
+ const result = await manager.sendConnectorMessage({
784
+ connectorId: 'conn_1',
785
+ channelId: '15550001111@s.whatsapp.net',
786
+ text: 'hello',
787
+ sessionId: 'main_thread',
788
+ })
789
+ const session = storage.loadSessions().main_thread
790
+ console.log(JSON.stringify({ result, session, sent }))
791
+ } finally {
792
+ await manager.stopConnector('conn_1')
793
+ }
794
+ `)
795
+
796
+ assert.equal(output.result.messageId, 'out-1')
797
+ assert.equal(output.sent.length, 1)
798
+ assert.equal(output.session.connectorContext || null, null)
799
+ })
800
+
801
+ it('does not auto-send a second connector reply from a non-direct main-thread heartbeat after a direct connector reply', () => {
802
+ const output = runWithTempDataDir(`
803
+ const storageMod = await import('./src/lib/server/storage')
804
+ const managerMod = await import('./src/lib/server/connectors/manager')
805
+ const chatExecMod = await import('./src/lib/server/chat-execution/chat-execution')
806
+ const providersMod = await import('./src/lib/providers/index')
807
+ const pluginsMod = await import('./src/lib/server/plugins')
808
+ const storage = storageMod.default || storageMod
809
+ const manager = managerMod.default || managerMod
810
+ const chatExec = chatExecMod.default || chatExecMod
811
+ const providers = providersMod.default || providersMod
812
+ const plugins = pluginsMod.default || pluginsMod
813
+
814
+ const sent = []
815
+ const now = Date.now()
816
+ plugins.getPluginManager().registerBuiltin('test-dup-heartbeat-plugin', {
817
+ name: 'Test Dup Heartbeat Plugin',
818
+ connectors: [{
819
+ id: 'test-dup-heartbeat',
820
+ name: 'Test Dup Heartbeat',
821
+ description: 'Captures outbound sends for duplication regressions',
822
+ startListener: async () => async () => {},
823
+ sendMessage: async (channelId, text, options) => {
824
+ sent.push({ channelId, text, options })
825
+ return { messageId: 'out-' + sent.length }
826
+ },
827
+ }],
828
+ })
829
+
830
+ providers.PROVIDERS['test-provider'] = {
831
+ id: 'test-provider',
832
+ name: 'Test Provider',
833
+ models: ['test-model'],
834
+ requiresApiKey: false,
835
+ requiresEndpoint: false,
836
+ handler: {
837
+ streamChat: async (opts) => {
838
+ const message = String(opts.message || '')
839
+ const text = message.includes('AGENT_HEARTBEAT')
840
+ ? 'Sent the ferry status to your WhatsApp.'
841
+ : 'Direct connector reply'
842
+ opts.write('data: ' + JSON.stringify({ t: 'r', text }) + '\\n')
843
+ return ''
844
+ },
845
+ },
846
+ }
847
+
848
+ storage.saveSettings({})
849
+ storage.saveAgents({
850
+ agent_1: {
851
+ id: 'agent_1',
852
+ name: 'Hal2k',
853
+ provider: 'test-provider',
854
+ model: 'test-model',
855
+ plugins: [],
856
+ heartbeatEnabled: true,
857
+ heartbeatIntervalSec: 60,
858
+ threadSessionId: 'agent_thread',
859
+ createdAt: now,
860
+ updatedAt: now,
861
+ },
862
+ })
863
+ storage.saveConnectors({
864
+ conn_1: {
865
+ id: 'conn_1',
866
+ name: 'WhatsApp',
867
+ platform: 'test-dup-heartbeat',
868
+ agentId: 'agent_1',
869
+ credentialId: null,
870
+ config: { inboundDebounceMs: 0, botToken: 'test-token' },
871
+ isEnabled: true,
872
+ status: 'stopped',
873
+ createdAt: now,
874
+ updatedAt: now,
875
+ },
876
+ })
877
+ storage.saveSessions({
878
+ agent_thread: {
879
+ id: 'agent_thread',
880
+ name: 'Hal2k',
881
+ cwd: process.env.WORKSPACE_DIR,
882
+ user: 'default',
883
+ provider: 'test-provider',
884
+ model: 'test-model',
885
+ claudeSessionId: null,
886
+ codexThreadId: null,
887
+ opencodeSessionId: null,
888
+ delegateResumeIds: { claudeCode: null, codex: null, opencode: null, gemini: null },
889
+ messages: [],
890
+ createdAt: now,
891
+ lastActiveAt: now,
892
+ sessionType: 'human',
893
+ agentId: 'agent_1',
894
+ shortcutForAgentId: 'agent_1',
895
+ plugins: [],
896
+ connectorContext: {
897
+ connectorId: 'conn_1',
898
+ channelId: 'poisoned-main-thread',
899
+ senderId: 'wrong-user',
900
+ },
901
+ },
902
+ })
903
+
904
+ try {
905
+ await manager.startConnector('conn_1')
906
+ const connector = storage.loadConnectors().conn_1
907
+ const directResponse = await manager.routeConnectorMessageForTest(connector, {
908
+ platform: 'whatsapp',
909
+ channelId: '15550001111@s.whatsapp.net',
910
+ senderId: '15550001111@s.whatsapp.net',
911
+ senderName: 'Alice',
912
+ text: 'Did you get this?',
913
+ messageId: 'in-1',
914
+ isGroup: false,
915
+ })
916
+
917
+ await chatExec.executeSessionChatTurn({
918
+ sessionId: 'agent_thread',
919
+ message: 'AGENT_HEARTBEAT_TICK\\nConnector follow-up sweep',
920
+ internal: true,
921
+ source: 'heartbeat',
922
+ runId: 'run-main-heartbeat',
923
+ heartbeatConfig: {
924
+ ackMaxChars: 300,
925
+ showOk: false,
926
+ showAlerts: true,
927
+ target: null,
928
+ },
929
+ })
930
+
931
+ const sessions = storage.loadSessions()
932
+ const directSession = Object.values(sessions).find((entry) =>
933
+ entry.id !== 'agent_thread' && String(entry.name || '').startsWith('connector:')
934
+ )
935
+ console.log(JSON.stringify({
936
+ directResponse,
937
+ sent,
938
+ mainThread: sessions.agent_thread,
939
+ directSessionId: directSession?.id || null,
940
+ }))
941
+ } finally {
942
+ await manager.stopConnector('conn_1')
943
+ }
944
+ `)
945
+
946
+ assert.equal(output.directResponse, 'Direct connector reply')
947
+ assert.equal(output.sent.length, 0)
948
+ assert.ok(output.directSessionId)
949
+ assert.equal(output.mainThread.connectorContext || null, null)
950
+ })
951
+
952
+ it('suppresses replies when a matched sender boundary memory says not to answer unless directly addressed', () => {
953
+ const output = runWithTempDataDir(`
954
+ const storageMod = await import('./src/lib/server/storage')
955
+ const managerMod = await import('./src/lib/server/connectors/manager')
956
+ const providersMod = await import('./src/lib/providers/index')
957
+ const pluginsMod = await import('./src/lib/server/plugins')
958
+ const memoryDbMod = await import('./src/lib/server/memory/memory-db')
959
+ const storage = storageMod.default || storageMod
960
+ const manager = managerMod.default || managerMod
961
+ const providers = providersMod.default || providersMod
962
+ const plugins = pluginsMod.default || pluginsMod
963
+ const memoryDbApi = memoryDbMod.default || memoryDbMod
964
+ const memoryDb = memoryDbApi.getMemoryDb()
965
+
966
+ let providerCalls = 0
967
+ const now = Date.now()
968
+ providers.PROVIDERS['test-provider'] = {
969
+ id: 'test-provider',
970
+ name: 'Test Provider',
971
+ models: ['test-model'],
972
+ requiresApiKey: false,
973
+ requiresEndpoint: false,
974
+ handler: {
975
+ streamChat: async () => {
976
+ providerCalls += 1
977
+ return 'should not be sent'
978
+ },
979
+ },
980
+ }
981
+
982
+ plugins.getPluginManager().registerBuiltin('test-quiet-boundary-connector-plugin', {
983
+ name: 'Test Quiet Boundary Connector Plugin',
984
+ connectors: [{
985
+ id: 'test-quiet',
986
+ name: 'Test Quiet',
987
+ description: 'Quiet boundary test connector',
988
+ startListener: async () => async () => {},
989
+ sendMessage: async () => ({ messageId: 'unused' }),
990
+ }],
991
+ })
992
+
993
+ storage.saveSettings({})
994
+ storage.saveAgents({
995
+ agent_1: {
996
+ id: 'agent_1',
997
+ name: 'Hal',
998
+ provider: 'test-provider',
999
+ model: 'test-model',
1000
+ plugins: [],
1001
+ threadSessionId: 'agent_thread',
1002
+ createdAt: now,
1003
+ updatedAt: now,
1004
+ },
1005
+ })
1006
+ storage.saveConnectors({
1007
+ conn_1: {
1008
+ id: 'conn_1',
1009
+ name: 'WhatsApp',
1010
+ platform: 'test-quiet',
1011
+ agentId: 'agent_1',
1012
+ credentialId: null,
1013
+ config: { inboundDebounceMs: 0 },
1014
+ isEnabled: true,
1015
+ status: 'stopped',
1016
+ createdAt: now,
1017
+ updatedAt: now,
1018
+ },
1019
+ })
1020
+ storage.saveSessions({
1021
+ agent_thread: {
1022
+ id: 'agent_thread',
1023
+ name: 'Hal',
1024
+ cwd: process.env.WORKSPACE_DIR,
1025
+ user: 'default',
1026
+ provider: 'test-provider',
1027
+ model: 'test-model',
1028
+ claudeSessionId: null,
1029
+ messages: [],
1030
+ createdAt: now,
1031
+ lastActiveAt: now,
1032
+ sessionType: 'human',
1033
+ agentId: 'agent_1',
1034
+ plugins: [],
1035
+ },
1036
+ })
1037
+
1038
+ memoryDb.add({
1039
+ agentId: 'agent_1',
1040
+ sessionId: null,
1041
+ category: 'identity/preferences',
1042
+ title: 'Wife communication rule',
1043
+ content: 'Wayde\\'s wife (+44 7958 047282): Do NOT respond unless she addresses Hal directly or mentions Hal directly. If unsure, verify whether the message is for Wayde or Hal before responding.',
1044
+ })
1045
+
1046
+ const connector = storage.loadConnectors().conn_1
1047
+ const response = await manager.routeConnectorMessageForTest(connector, {
1048
+ platform: 'whatsapp',
1049
+ channelId: '447958047282@s.whatsapp.net',
1050
+ senderId: '447958047282@s.whatsapp.net',
1051
+ senderName: 'Wayde Wife',
1052
+ text: 'Dinner is ready for Wayde.',
1053
+ messageId: 'in-quiet-1',
1054
+ isGroup: false,
1055
+ })
1056
+
1057
+ const sessions = storage.loadSessions()
1058
+ const directSession = Object.values(sessions).find((entry) => entry.id !== 'agent_thread')
1059
+ console.log(JSON.stringify({
1060
+ response,
1061
+ providerCalls,
1062
+ directSessionMessageCount: directSession?.messages?.length || 0,
1063
+ mainThreadMessageCount: sessions.agent_thread?.messages?.length || 0,
1064
+ }))
1065
+ `)
1066
+
1067
+ assert.equal(output.response, 'NO_MESSAGE')
1068
+ assert.equal(output.providerCalls, 0)
1069
+ assert.equal(output.directSessionMessageCount, 0)
1070
+ assert.equal(output.mainThreadMessageCount, 0)
1071
+ })
1072
+
1073
+ it('requires an explicit target when a shared thread only has mirrored connector history', () => {
724
1074
  const output = runWithTempDataDir(`
725
1075
  const fs = await import('node:fs')
726
1076
  const path = await import('node:path')
@@ -841,9 +1191,7 @@ describe('sanitizeConnectorOutboundContent', () => {
841
1191
  }
842
1192
  `)
843
1193
 
844
- assert.match(output.raw, /multiple connector recipients/)
845
- assert.match(output.raw, /Alice/)
846
- assert.match(output.raw, /Gran/)
1194
+ assert.match(output.raw, /no target recipient configured/)
847
1195
  })
848
1196
 
849
1197
  it('keeps direct connector sessions isolated across four inbound senders for the same agent and mirrors their metadata into the main thread', () => {
@@ -978,13 +1326,15 @@ describe('sanitizeConnectorOutboundContent', () => {
978
1326
  it('excludes mirrored connector transcript entries from direct agent-thread history', () => {
979
1327
  const output = runWithTempDataDir(`
980
1328
  const storageMod = await import('./src/lib/server/storage')
981
- const managerMod = await import('./src/lib/server/connectors/manager')
982
1329
  const chatExecMod = await import('@/lib/server/chat-execution/chat-execution')
1330
+ const streamChatMod = await import('@/lib/server/chat-execution/stream-agent-chat')
983
1331
  const providersMod = await import('./src/lib/providers/index')
984
1332
  const storage = storageMod.default || storageMod
985
- const manager = managerMod.default || managerMod
986
1333
  const chatExec = chatExecMod.default || chatExecMod
1334
+ const streamChat = streamChatMod.default || streamChatMod
987
1335
  const providers = providersMod.default || providersMod
1336
+ const managerMod = await import('./src/lib/server/connectors/manager')
1337
+ const manager = managerMod.default || managerMod
988
1338
 
989
1339
  const now = Date.now()
990
1340
  providers.PROVIDERS['test-provider'] = {
@@ -994,24 +1344,126 @@ describe('sanitizeConnectorOutboundContent', () => {
994
1344
  requiresApiKey: false,
995
1345
  requiresEndpoint: false,
996
1346
  handler: {
997
- streamChat: async (opts) => {
998
- const history = typeof opts.loadHistory === 'function' ? opts.loadHistory(opts.session.id) : []
999
- return JSON.stringify({
1000
- historyCount: history.length,
1001
- texts: history.map((entry) => entry.text),
1002
- senderNames: history.map((entry) => entry.source?.senderName || null),
1003
- })
1004
- },
1347
+ streamChat: async () => '',
1005
1348
  },
1006
1349
  }
1350
+ streamChat.setStreamAgentChatForTest(async (opts) => ({
1351
+ fullText: JSON.stringify({
1352
+ historyCount: opts.history.length,
1353
+ texts: opts.history.map((entry) => entry.text),
1354
+ senderNames: opts.history.map((entry) => entry.source?.senderName || null),
1355
+ }),
1356
+ finalResponse: JSON.stringify({
1357
+ historyCount: opts.history.length,
1358
+ texts: opts.history.map((entry) => entry.text),
1359
+ senderNames: opts.history.map((entry) => entry.source?.senderName || null),
1360
+ }),
1361
+ }))
1007
1362
 
1008
- storage.saveSettings({})
1363
+ try {
1364
+ storage.saveSettings({})
1365
+ storage.saveAgents({
1366
+ agent_1: {
1367
+ id: 'agent_1',
1368
+ name: 'Molly',
1369
+ provider: 'test-provider',
1370
+ model: 'test-model',
1371
+ plugins: [],
1372
+ threadSessionId: 'agent_thread',
1373
+ createdAt: now,
1374
+ updatedAt: now,
1375
+ },
1376
+ })
1377
+ storage.saveConnectors({
1378
+ conn_1: {
1379
+ id: 'conn_1',
1380
+ name: 'WhatsApp',
1381
+ platform: 'whatsapp',
1382
+ agentId: 'agent_1',
1383
+ credentialId: null,
1384
+ config: { inboundDebounceMs: 0 },
1385
+ isEnabled: true,
1386
+ status: 'running',
1387
+ createdAt: now,
1388
+ updatedAt: now,
1389
+ },
1390
+ })
1391
+ storage.saveSessions({
1392
+ agent_thread: {
1393
+ id: 'agent_thread',
1394
+ name: 'Molly',
1395
+ cwd: process.env.WORKSPACE_DIR,
1396
+ user: 'default',
1397
+ provider: 'test-provider',
1398
+ model: 'test-model',
1399
+ claudeSessionId: null,
1400
+ messages: [],
1401
+ createdAt: now,
1402
+ lastActiveAt: now,
1403
+ sessionType: 'human',
1404
+ agentId: 'agent_1',
1405
+ plugins: [],
1406
+ },
1407
+ })
1408
+
1409
+ const connector = storage.loadConnectors().conn_1
1410
+ await manager.routeConnectorMessageForTest(connector, {
1411
+ platform: 'whatsapp',
1412
+ channelId: '15550001111@s.whatsapp.net',
1413
+ senderId: '15550001111@s.whatsapp.net',
1414
+ senderName: 'Alice',
1415
+ text: 'Hello from Alice',
1416
+ messageId: 'in-a',
1417
+ isGroup: false,
1418
+ })
1419
+ await manager.routeConnectorMessageForTest(connector, {
1420
+ platform: 'whatsapp',
1421
+ channelId: '278200000001@s.whatsapp.net',
1422
+ senderId: '278200000001@s.whatsapp.net',
1423
+ senderName: 'Gran',
1424
+ text: 'Hello from Gran',
1425
+ messageId: 'in-g',
1426
+ isGroup: false,
1427
+ })
1428
+
1429
+ const result = await chatExec.executeSessionChatTurn({
1430
+ sessionId: 'agent_thread',
1431
+ message: 'This is Wayde in the app.',
1432
+ })
1433
+
1434
+ const thread = storage.loadSessions().agent_thread
1435
+ const replyText = String(result.text || '').replace(/\\n+\\*-- Sent via Sample UI Plugin --\\*\\s*$/, '')
1436
+ console.log(JSON.stringify({
1437
+ reply: JSON.parse(replyText),
1438
+ threadMessages: thread.messages,
1439
+ }))
1440
+ } finally {
1441
+ streamChat.setStreamAgentChatForTest(null)
1442
+ }
1443
+ `)
1444
+
1445
+ assert.equal(output.reply.senderNames.includes('Alice'), false)
1446
+ assert.equal(output.reply.senderNames.includes('Gran'), false)
1447
+ assert.equal(output.reply.texts.some((entry: any) => /Alice|Gran/.test(String(entry))), false)
1448
+ assert.equal(output.reply.historyCount >= 1, true)
1449
+ assert.equal(output.threadMessages.some((msg: any) => msg.historyExcluded === true && msg.source?.connectorId === 'conn_1'), true)
1450
+ })
1451
+
1452
+ it('treats allowlist entries as the source of truth and does not create approval records for unknown senders', () => {
1453
+ const output = runWithTempDataDir(`
1454
+ const storageMod = await import('./src/lib/server/storage')
1455
+ const managerMod = await import('./src/lib/server/connectors/manager')
1456
+ const storage = storageMod.default || storageMod
1457
+ const manager = managerMod.default || managerMod
1458
+
1459
+ const now = Date.now()
1460
+ storage.saveSettings({ approvalsEnabled: true })
1009
1461
  storage.saveAgents({
1010
1462
  agent_1: {
1011
1463
  id: 'agent_1',
1012
1464
  name: 'Molly',
1013
- provider: 'test-provider',
1014
- model: 'test-model',
1465
+ provider: 'openai',
1466
+ model: 'gpt-4.1-mini',
1015
1467
  plugins: [],
1016
1468
  threadSessionId: 'agent_thread',
1017
1469
  createdAt: now,
@@ -1025,7 +1477,11 @@ describe('sanitizeConnectorOutboundContent', () => {
1025
1477
  platform: 'whatsapp',
1026
1478
  agentId: 'agent_1',
1027
1479
  credentialId: null,
1028
- config: { inboundDebounceMs: 0 },
1480
+ config: {
1481
+ inboundDebounceMs: 0,
1482
+ dmPolicy: 'allowlist',
1483
+ allowFrom: '15550001111',
1484
+ },
1029
1485
  isEnabled: true,
1030
1486
  status: 'running',
1031
1487
  createdAt: now,
@@ -1038,8 +1494,8 @@ describe('sanitizeConnectorOutboundContent', () => {
1038
1494
  name: 'Molly',
1039
1495
  cwd: process.env.WORKSPACE_DIR,
1040
1496
  user: 'default',
1041
- provider: 'test-provider',
1042
- model: 'test-model',
1497
+ provider: 'openai',
1498
+ model: 'gpt-4.1-mini',
1043
1499
  claudeSessionId: null,
1044
1500
  messages: [],
1045
1501
  createdAt: now,
@@ -1051,54 +1507,32 @@ describe('sanitizeConnectorOutboundContent', () => {
1051
1507
  })
1052
1508
 
1053
1509
  const connector = storage.loadConnectors().conn_1
1054
- await manager.routeConnectorMessageForTest(connector, {
1055
- platform: 'whatsapp',
1056
- channelId: '15550001111@s.whatsapp.net',
1057
- senderId: '15550001111@s.whatsapp.net',
1058
- senderName: 'Alice',
1059
- text: 'Hello from Alice',
1060
- messageId: 'in-a',
1061
- isGroup: false,
1062
- })
1063
- await manager.routeConnectorMessageForTest(connector, {
1510
+ const reply = await manager.routeConnectorMessageForTest(connector, {
1064
1511
  platform: 'whatsapp',
1065
- channelId: '278200000001@s.whatsapp.net',
1066
- senderId: '278200000001@s.whatsapp.net',
1067
- senderName: 'Gran',
1068
- text: 'Hello from Gran',
1069
- messageId: 'in-g',
1512
+ channelId: '16660002222@s.whatsapp.net',
1513
+ senderId: '16660002222@s.whatsapp.net',
1514
+ senderName: 'Bob',
1515
+ text: 'Hello from Bob',
1516
+ messageId: 'in-b-allowlist',
1070
1517
  isGroup: false,
1071
1518
  })
1072
-
1073
- const result = await chatExec.executeSessionChatTurn({
1074
- sessionId: 'agent_thread',
1075
- message: 'This is Wayde in the app.',
1076
- })
1077
-
1078
- const thread = storage.loadSessions().agent_thread
1079
- console.log(JSON.stringify({
1080
- reply: JSON.parse(result.text),
1081
- threadMessages: thread.messages,
1082
- }))
1519
+ const approvals = Object.values(storage.loadApprovals())
1520
+ console.log(JSON.stringify({ reply, approvals }))
1083
1521
  `)
1084
1522
 
1085
- assert.equal(output.reply.senderNames.includes('Alice'), false)
1086
- assert.equal(output.reply.senderNames.includes('Gran'), false)
1087
- assert.equal(output.reply.texts.some((entry: any) => /Alice|Gran/.test(String(entry))), false)
1088
- assert.equal(output.reply.historyCount >= 1, true)
1089
- assert.equal(output.threadMessages.some((msg: any) => msg.historyExcluded === true && msg.source?.connectorId === 'conn_1'), true)
1523
+ assert.match(output.reply, /not approved for this connector/i)
1524
+ assert.match(output.reply, /no automatic approval queue is created/i)
1525
+ assert.equal(output.approvals.length, 0)
1090
1526
  })
1091
1527
 
1092
- it('creates one reusable connector-sender approval for unknown allowlist senders and allows them after approval', () => {
1528
+ it('creates one reusable pairing request for unknown senders and allows them after approval', () => {
1093
1529
  const output = runWithTempDataDir(`
1094
1530
  const storageMod = await import('./src/lib/server/storage')
1095
1531
  const managerMod = await import('./src/lib/server/connectors/manager')
1096
- const approvalsMod = await import('./src/lib/server/approvals')
1097
1532
  const pairingMod = await import('./src/lib/server/connectors/pairing')
1098
1533
  const providersMod = await import('./src/lib/providers/index')
1099
1534
  const storage = storageMod.default || storageMod
1100
1535
  const manager = managerMod.default || managerMod
1101
- const approvals = approvalsMod.default || approvalsMod
1102
1536
  const pairing = pairingMod.default || pairingMod
1103
1537
  const providers = providersMod.default || providersMod
1104
1538
 
@@ -1141,8 +1575,7 @@ describe('sanitizeConnectorOutboundContent', () => {
1141
1575
  credentialId: null,
1142
1576
  config: {
1143
1577
  inboundDebounceMs: 0,
1144
- dmPolicy: 'allowlist',
1145
- allowFrom: '15550001111',
1578
+ dmPolicy: 'pairing',
1146
1579
  },
1147
1580
  isEnabled: true,
1148
1581
  status: 'running',
@@ -1181,39 +1614,37 @@ describe('sanitizeConnectorOutboundContent', () => {
1181
1614
 
1182
1615
  const first = await inbound('in-b1', 'Hello from Bob')
1183
1616
  const second = await inbound('in-b2', 'Following up before approval')
1184
- const pendingBefore = Object.values(storage.loadApprovals())
1185
- await approvals.submitDecision(pendingBefore[0].id, true)
1617
+ const pendingBefore = pairing.listPendingPairingRequests('conn_1')
1618
+ const approved = pairing.approvePairingCode('conn_1', pendingBefore[0].code)
1186
1619
  const allowed = pairing.listStoredAllowedSenders('conn_1')
1187
1620
  const third = await inbound('in-b3', 'Hello after approval')
1188
- const approvalsAfter = Object.values(storage.loadApprovals())
1621
+ const pendingAfter = pairing.listPendingPairingRequests('conn_1')
1189
1622
  const sessions = storage.loadSessions()
1190
1623
  const thread = sessions.agent_thread
1191
1624
  console.log(JSON.stringify({
1192
1625
  first,
1193
1626
  second,
1194
1627
  pendingBefore: pendingBefore.map((entry) => ({
1195
- id: entry.id,
1196
- category: entry.category,
1197
- status: entry.status,
1198
- title: entry.title,
1199
- data: entry.data,
1628
+ code: entry.code,
1629
+ senderId: entry.senderId,
1630
+ senderName: entry.senderName,
1200
1631
  })),
1632
+ approved,
1201
1633
  allowed,
1202
1634
  third,
1203
- approvalsAfter: approvalsAfter.map((entry) => ({ id: entry.id, status: entry.status })),
1635
+ pendingAfter,
1204
1636
  threadMessages: thread.messages,
1205
1637
  }))
1206
1638
  `)
1207
1639
 
1208
- assert.match(output.first, /pending approval/i)
1209
- assert.match(output.second, /pending approval/i)
1640
+ assert.match(output.first, /pending pairing/i)
1641
+ assert.match(output.second, /pending pairing/i)
1210
1642
  assert.equal(output.pendingBefore.length, 1)
1211
- assert.equal(output.pendingBefore[0].category, 'connector_sender')
1212
- assert.equal(output.pendingBefore[0].data.senderId, '16660002222@s.whatsapp.net')
1643
+ assert.equal(output.pendingBefore[0].senderId, '16660002222@s.whatsapp.net')
1644
+ assert.equal(output.approved.ok, true)
1213
1645
  assert.deepEqual(output.allowed, ['16660002222@s.whatsapp.net'])
1214
1646
  assert.equal(output.third, 'Approved hello to Bob')
1215
- assert.equal(output.approvalsAfter.length, 1)
1216
- assert.equal(output.approvalsAfter[0].status, 'approved')
1647
+ assert.equal(output.pendingAfter.length, 0)
1217
1648
  assert.equal(output.threadMessages.some((msg: any) => msg.source?.senderName === 'Bob' && msg.historyExcluded === true), true)
1218
1649
  })
1219
1650