@swarmclawai/swarmclaw 0.9.2 → 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.
- package/README.md +12 -10
- package/bundled-skills/google-workspace/SKILL.md +2 -0
- package/package.json +1 -1
- package/src/app/agents/page.tsx +2 -1
- package/src/app/api/chatrooms/[id]/chat/route.ts +1 -1
- package/src/app/api/clawhub/install/route.ts +2 -0
- package/src/app/api/skills/[id]/route.ts +4 -0
- package/src/app/api/skills/route.ts +4 -0
- package/src/app/globals.css +28 -0
- package/src/app/home/page.tsx +11 -0
- package/src/app/settings/page.tsx +12 -5
- package/src/components/agents/agent-sheet.tsx +5 -5
- package/src/components/connectors/connector-list.tsx +2 -5
- package/src/components/logs/log-list.tsx +2 -5
- package/src/components/providers/provider-list.tsx +2 -5
- package/src/components/runs/run-list.tsx +2 -6
- package/src/components/schedules/schedule-list.tsx +7 -1
- package/src/components/ui/full-screen-loader.tsx +0 -29
- package/src/components/ui/page-loader.tsx +69 -0
- package/src/lib/runtime/runtime-loop.ts +21 -1
- package/src/lib/server/agents/agent-thread-session.test.ts +64 -0
- package/src/lib/server/agents/agent-thread-session.ts +1 -1
- package/src/lib/server/agents/main-agent-loop-advanced.test.ts +77 -0
- package/src/lib/server/agents/main-agent-loop.ts +259 -0
- package/src/lib/server/agents/orchestrator-lg.ts +12 -8
- package/src/lib/server/agents/orchestrator.ts +11 -7
- package/src/lib/server/chat-execution/chat-execution-advanced.test.ts +11 -10
- package/src/lib/server/chat-execution/chat-execution-session-sync.test.ts +116 -3
- package/src/lib/server/chat-execution/chat-execution-utils.test.ts +56 -0
- package/src/lib/server/chat-execution/chat-execution-utils.ts +24 -0
- package/src/lib/server/chat-execution/chat-execution.ts +116 -29
- package/src/lib/server/chat-execution/chat-streaming-utils.ts +1 -38
- package/src/lib/server/chat-execution/stream-agent-chat.test.ts +67 -76
- package/src/lib/server/chat-execution/stream-agent-chat.ts +119 -110
- package/src/lib/server/chat-execution/stream-continuation.ts +1 -1
- package/src/lib/server/chatrooms/chatroom-helpers.test.ts +26 -0
- package/src/lib/server/chatrooms/chatroom-helpers.ts +11 -8
- package/src/lib/server/connectors/contact-boundaries.ts +101 -0
- package/src/lib/server/connectors/manager.test.ts +504 -73
- package/src/lib/server/connectors/manager.ts +41 -10
- package/src/lib/server/connectors/session-consolidation.ts +2 -0
- package/src/lib/server/connectors/session-kind.ts +7 -0
- package/src/lib/server/connectors/session.test.ts +104 -0
- package/src/lib/server/connectors/session.ts +5 -2
- package/src/lib/server/identity-continuity.test.ts +4 -3
- package/src/lib/server/identity-continuity.ts +8 -4
- package/src/lib/server/memory/memory-policy.test.ts +5 -15
- package/src/lib/server/memory/memory-policy.ts +11 -41
- package/src/lib/server/memory/session-archive-memory.ts +2 -1
- package/src/lib/server/runtime/heartbeat-service.test.ts +46 -0
- package/src/lib/server/runtime/heartbeat-service.ts +5 -1
- package/src/lib/server/runtime/runtime-settings.test.ts +4 -4
- package/src/lib/server/runtime/runtime-settings.ts +4 -0
- package/src/lib/server/runtime/session-run-manager.ts +2 -0
- package/src/lib/server/session-reset-policy.test.ts +17 -3
- package/src/lib/server/session-reset-policy.ts +4 -2
- package/src/lib/server/session-tools/connector.ts +11 -10
- package/src/lib/server/session-tools/crud.ts +41 -7
- package/src/lib/server/session-tools/delegate.ts +3 -3
- package/src/lib/server/session-tools/index.ts +2 -0
- package/src/lib/server/session-tools/manage-skills.test.ts +194 -0
- package/src/lib/server/session-tools/memory.ts +209 -48
- package/src/lib/server/session-tools/skill-runtime.test.ts +175 -0
- package/src/lib/server/session-tools/skill-runtime.ts +382 -0
- package/src/lib/server/session-tools/skills.ts +575 -0
- package/src/lib/server/skills/runtime-skill-resolver.test.ts +162 -0
- package/src/lib/server/skills/runtime-skill-resolver.ts +750 -0
- package/src/lib/server/skills/skill-discovery.ts +4 -0
- package/src/lib/server/skills/skills-normalize.test.ts +28 -0
- package/src/lib/server/skills/skills-normalize.ts +93 -1
- package/src/lib/server/storage.ts +1 -1
- package/src/lib/server/tasks/task-followups.test.ts +124 -0
- package/src/lib/server/tasks/task-followups.ts +88 -13
- package/src/types/index.ts +30 -2
- package/src/views/settings/section-runtime-loop.tsx +38 -0
|
@@ -592,9 +592,9 @@ describe('sanitizeConnectorOutboundContent', () => {
|
|
|
592
592
|
storage.saveSessions({
|
|
593
593
|
session_1: {
|
|
594
594
|
id: 'session_1',
|
|
595
|
-
name: '
|
|
595
|
+
name: 'connector:whatsapp:gran',
|
|
596
596
|
cwd: process.env.WORKSPACE_DIR,
|
|
597
|
-
user: '
|
|
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('
|
|
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, /
|
|
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 (
|
|
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
|
-
|
|
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: '
|
|
1014
|
-
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: {
|
|
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: '
|
|
1042
|
-
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: '
|
|
1066
|
-
senderId: '
|
|
1067
|
-
senderName: '
|
|
1068
|
-
text: 'Hello from
|
|
1069
|
-
messageId: 'in-
|
|
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
|
-
|
|
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.
|
|
1086
|
-
assert.
|
|
1087
|
-
assert.equal(output.
|
|
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
|
|
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: '
|
|
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 =
|
|
1185
|
-
|
|
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
|
|
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
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
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
|
-
|
|
1635
|
+
pendingAfter,
|
|
1204
1636
|
threadMessages: thread.messages,
|
|
1205
1637
|
}))
|
|
1206
1638
|
`)
|
|
1207
1639
|
|
|
1208
|
-
assert.match(output.first, /pending
|
|
1209
|
-
assert.match(output.second, /pending
|
|
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].
|
|
1212
|
-
assert.equal(output.
|
|
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.
|
|
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
|
|