From ee021be690bae4da1be505e55ec77803ea29d050 Mon Sep 17 00:00:00 2001 From: zhongqin Date: Fri, 4 Sep 2026 17:14:12 +0800 Subject: [PATCH] =?UTF-8?q?feat(agent):=20per-user=20memory=20isolation=20?= =?UTF-8?q?=E2=80=94=20owner=5Fid=E2=86=92user=5Fid=20+=20user-scoped=20me?= =?UTF-8?q?mory=20tiers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...bd3159d0a672386d6e83a9f7058a868f776e0.json | 12 ++ ...42d0f665e1715de020857b8b24f36ae707ea6.json | 12 ++ ...ead46ddc784fd797a82c30fcc4c463f4f9073.json | 12 -- ...32a1f2ede0939b7356425f078ddebe07f166b.json | 12 ++ ...c404113144fdcefcec7d52e7bc7ecfee00c20.json | 12 ++ ...15b1a38252dca2a48bd37d39fd79cc3c7e852.json | 12 -- ...ca18b2dd589a31840c07fb03ec77de6c7e945.json | 12 -- ...6c0333322de93ef5b8c12458216d7829c4cd7.json | 12 -- ...162d5fb575989499443a7042dbe5baccd7816.json | 12 ++ ...43abc30f76e71c76a930b27c3f9537ab1fb9b.json | 12 ++ ...81a2f82fe99b421927e4b35639fef0e4ead64.json | 12 -- ...aa19d4626b67ac88254c6b23cac55e1c42969.json | 12 -- ...69f7f4a4afc27bb0e9b066075bf1c5d7c197b.json | 27 ++++ ...d6cae8faaa8e275739bbd2f2c6824f373180.json} | 5 +- ...d19399453614aedab5ad9dc33d124301bf96.json} | 4 +- ...ddf0c50cf21f1d5afe3b7edf3a6e95571a9e.json} | 5 +- ...da63f2a8d2d55ef799ffdb82cf2f93d7b2cf5.json | 27 ---- ...460ba12b3966a805d9ad1b9a91402eff0d8a8.json | 26 ---- ...cbe3a33028dcff5bcf918b43dbcca4d5c2f6.json} | 4 +- ...a75271d044013ae971c69cff10320bdc47426.json | 26 ++++ ...476ae8aad0f6e2b7c2208721cf7d1250600e8.json | 12 -- ...49e3f7cea8b316bae565e34a438165e7b9061.json | 12 -- ...ff2e990240245c650afca2d827893e0b9ca5f.json | 12 ++ ...51234b9cb1a096c547858a9d0b572d7dc32c3.json | 12 -- ...6e6030f6e59a993749357d518d1c1ef9b5a6c.json | 12 ++ ...de80249284315929caa9ff38ced5d09733ab3.json | 12 -- ...f3ec31a8e67e77b442a471d28a18c3c3f6777.json | 12 -- ...00a1f90c9897188d7089d8d2dbefa7c4bc67c.json | 12 ++ ...fa870551956cf5aa5247bc2731cc7a47a6f7e.json | 12 -- ...6d9d1c021e587d0220fcad2821036b6f978c5.json | 12 ++ ...d19baeb00d99b320802f97d8c1571849d9b60.json | 12 ++ ...49348ef924bd7bf6361a40bf90b9e9b42c579.json | 12 ++ crates/core/src/agent/handler.rs | 6 +- crates/core/src/agent/memory_sql.rs | 47 ++++-- crates/core/src/agent/models/ai_agent.rs | 6 +- crates/core/src/agent/models/ai_memory.rs | 147 +++++++++++------- crates/core/src/agent/models/ai_session.rs | 8 +- crates/core/src/agent/service.rs | 63 +++++--- crates/core/src/worker/handlers/agent_run.rs | 2 +- crates/core/tests/ai_runtime.rs | 107 ++++++++++++- migrations/mysql/schema.mysql.sql | 9 +- migrations/postgres/schema.postgres.sql | 9 +- migrations/sqlite/schema.sqlite.sql | 9 +- 43 files changed, 505 insertions(+), 320 deletions(-) create mode 100644 .sqlx-mysql/query-132d2146e8d655e950a090d5851bd3159d0a672386d6e83a9f7058a868f776e0.json create mode 100644 .sqlx-mysql/query-15e22b4f55900a9c31cec2835e042d0f665e1715de020857b8b24f36ae707ea6.json delete mode 100644 .sqlx-mysql/query-4a8d98fb07ec1bd547cf61ab107ead46ddc784fd797a82c30fcc4c463f4f9073.json create mode 100644 .sqlx-mysql/query-4c266659ebf5124ad0565d9fe1d32a1f2ede0939b7356425f078ddebe07f166b.json create mode 100644 .sqlx-mysql/query-5b780dfa43760a2d3c3335277d4c404113144fdcefcec7d52e7bc7ecfee00c20.json delete mode 100644 .sqlx-mysql/query-6257a8c98466be78a432a055a6015b1a38252dca2a48bd37d39fd79cc3c7e852.json delete mode 100644 .sqlx-mysql/query-7059fbde4c6c733eba04ba2d593ca18b2dd589a31840c07fb03ec77de6c7e945.json delete mode 100644 .sqlx-mysql/query-88d45262cc05b6f8636ca3710c96c0333322de93ef5b8c12458216d7829c4cd7.json create mode 100644 .sqlx-mysql/query-c83bcb80850708a7dadd4465ec8162d5fb575989499443a7042dbe5baccd7816.json create mode 100644 .sqlx-mysql/query-d5d20d03fa613b046017dce882e43abc30f76e71c76a930b27c3f9537ab1fb9b.json delete mode 100644 .sqlx-mysql/query-dfe4c417190bf90d94b1bcadfb281a2f82fe99b421927e4b35639fef0e4ead64.json delete mode 100644 .sqlx-mysql/query-ee8dc80ac67d6b60c27bc673226aa19d4626b67ac88254c6b23cac55e1c42969.json create mode 100644 .sqlx-postgres/query-44ec0abd7ce2fb0cb0f29f0d2fb69f7f4a4afc27bb0e9b066075bf1c5d7c197b.json rename .sqlx-postgres/{query-30658436f530909d7dfe58ecf56df46b71c996e2f8c2d3774542be9abe008823.json => query-61789f63f11b7cbb5491028b75bdd6cae8faaa8e275739bbd2f2c6824f373180.json} (57%) rename .sqlx-postgres/{query-6ea327ab3b80f0f8df2d8cc5760d60bcc83e783474aa7ccab16f35306757e243.json => query-6267402cb714802edc33ebcfddb3d19399453614aedab5ad9dc33d124301bf96.json} (61%) rename .sqlx-postgres/{query-3254264892ba3ec9e97e2fe12d2a922b836cc14d0bb4ca95bf908dac0bf1eed4.json => query-9090d954cfd50d60494bc2ae9d5addf0c50cf21f1d5afe3b7edf3a6e95571a9e.json} (57%) delete mode 100644 .sqlx-postgres/query-bae4f2c5faef1743f83aa2f3a3bda63f2a8d2d55ef799ffdb82cf2f93d7b2cf5.json delete mode 100644 .sqlx-postgres/query-c822e2f0b784b83a8d1088917be460ba12b3966a805d9ad1b9a91402eff0d8a8.json rename .sqlx-postgres/{query-38071fc5790dafcaf2378554cba715c6861b796a76421f448452aa2d744bfaf8.json => query-dee2633f7202b23e318b5874aa25cbe3a33028dcff5bcf918b43dbcca4d5c2f6.json} (62%) create mode 100644 .sqlx-postgres/query-e4f207310d01a532e016e45aa31a75271d044013ae971c69cff10320bdc47426.json delete mode 100644 .sqlx-sqlite/query-10ee524b668c16ee8871c24c965476ae8aad0f6e2b7c2208721cf7d1250600e8.json delete mode 100644 .sqlx-sqlite/query-260fe0e1b28f5608edabdc12c5449e3f7cea8b316bae565e34a438165e7b9061.json create mode 100644 .sqlx-sqlite/query-6d834ba00b4b7e63c6f7d16aefbff2e990240245c650afca2d827893e0b9ca5f.json delete mode 100644 .sqlx-sqlite/query-848d4245ac2d8482ca4849d50dd51234b9cb1a096c547858a9d0b572d7dc32c3.json create mode 100644 .sqlx-sqlite/query-84aea1d0d6e6afe5219bc446abf6e6030f6e59a993749357d518d1c1ef9b5a6c.json delete mode 100644 .sqlx-sqlite/query-87a044b2745278ed15fd2820206de80249284315929caa9ff38ced5d09733ab3.json delete mode 100644 .sqlx-sqlite/query-a3fcf72d0626e33ad63d71ee772f3ec31a8e67e77b442a471d28a18c3c3f6777.json create mode 100644 .sqlx-sqlite/query-cb30413bb7f176feb4d30d4266a00a1f90c9897188d7089d8d2dbefa7c4bc67c.json delete mode 100644 .sqlx-sqlite/query-d35ba70511d480370277752d31ffa870551956cf5aa5247bc2731cc7a47a6f7e.json create mode 100644 .sqlx-sqlite/query-d36ffc5bd0ebcab09f7544ed73f6d9d1c021e587d0220fcad2821036b6f978c5.json create mode 100644 .sqlx-sqlite/query-e89db1a60b3d6c97e3aed72b60dd19baeb00d99b320802f97d8c1571849d9b60.json create mode 100644 .sqlx-sqlite/query-ff3572478372bf0d26ca9312a8949348ef924bd7bf6361a40bf90b9e9b42c579.json diff --git a/.sqlx-mysql/query-132d2146e8d655e950a090d5851bd3159d0a672386d6e83a9f7058a868f776e0.json b/.sqlx-mysql/query-132d2146e8d655e950a090d5851bd3159d0a672386d6e83a9f7058a868f776e0.json new file mode 100644 index 00000000..aa74fcec --- /dev/null +++ b/.sqlx-mysql/query-132d2146e8d655e950a090d5851bd3159d0a672386d6e83a9f7058a868f776e0.json @@ -0,0 +1,12 @@ +{ + "db_name": "MySQL", + "query": "INSERT INTO `ai_agents` (`id`, `user_id`, `name`, `system_prompt`, `provider`, `model`, `temperature`, `max_iterations`, `tools`, `memory_enabled`, `params`, `created_at`, `updated_at`) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + "describe": { + "columns": [], + "parameters": { + "Right": 13 + }, + "nullable": [] + }, + "hash": "132d2146e8d655e950a090d5851bd3159d0a672386d6e83a9f7058a868f776e0" +} diff --git a/.sqlx-mysql/query-15e22b4f55900a9c31cec2835e042d0f665e1715de020857b8b24f36ae707ea6.json b/.sqlx-mysql/query-15e22b4f55900a9c31cec2835e042d0f665e1715de020857b8b24f36ae707ea6.json new file mode 100644 index 00000000..93cf7cb7 --- /dev/null +++ b/.sqlx-mysql/query-15e22b4f55900a9c31cec2835e042d0f665e1715de020857b8b24f36ae707ea6.json @@ -0,0 +1,12 @@ +{ + "db_name": "MySQL", + "query": "INSERT INTO `ai_sessions` (`id`, `agent_id`, `user_id`, `title`, `status`, `last_seq`, `created_at`, `updated_at`, `last_active_at`) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", + "describe": { + "columns": [], + "parameters": { + "Right": 9 + }, + "nullable": [] + }, + "hash": "15e22b4f55900a9c31cec2835e042d0f665e1715de020857b8b24f36ae707ea6" +} diff --git a/.sqlx-mysql/query-4a8d98fb07ec1bd547cf61ab107ead46ddc784fd797a82c30fcc4c463f4f9073.json b/.sqlx-mysql/query-4a8d98fb07ec1bd547cf61ab107ead46ddc784fd797a82c30fcc4c463f4f9073.json deleted file mode 100644 index d3556c9b..00000000 --- a/.sqlx-mysql/query-4a8d98fb07ec1bd547cf61ab107ead46ddc784fd797a82c30fcc4c463f4f9073.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "MySQL", - "query": "INSERT INTO `ai_memories` (`id`, `agent_id`, `session_id`, `mem_key`, `content`, `category`, `importance`, `pinned`, `created_at`, `updated_at`) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", - "describe": { - "columns": [], - "parameters": { - "Right": 10 - }, - "nullable": [] - }, - "hash": "4a8d98fb07ec1bd547cf61ab107ead46ddc784fd797a82c30fcc4c463f4f9073" -} diff --git a/.sqlx-mysql/query-4c266659ebf5124ad0565d9fe1d32a1f2ede0939b7356425f078ddebe07f166b.json b/.sqlx-mysql/query-4c266659ebf5124ad0565d9fe1d32a1f2ede0939b7356425f078ddebe07f166b.json new file mode 100644 index 00000000..abe6d979 --- /dev/null +++ b/.sqlx-mysql/query-4c266659ebf5124ad0565d9fe1d32a1f2ede0939b7356425f078ddebe07f166b.json @@ -0,0 +1,12 @@ +{ + "db_name": "MySQL", + "query": "INSERT INTO `ai_memories` (`id`, `agent_id`, `user_id`, `session_id`, `mem_key`, `content`, `category`, `importance`, `pinned`, `created_at`, `updated_at`, `tenant_id`) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + "describe": { + "columns": [], + "parameters": { + "Right": 12 + }, + "nullable": [] + }, + "hash": "4c266659ebf5124ad0565d9fe1d32a1f2ede0939b7356425f078ddebe07f166b" +} diff --git a/.sqlx-mysql/query-5b780dfa43760a2d3c3335277d4c404113144fdcefcec7d52e7bc7ecfee00c20.json b/.sqlx-mysql/query-5b780dfa43760a2d3c3335277d4c404113144fdcefcec7d52e7bc7ecfee00c20.json new file mode 100644 index 00000000..ca266fed --- /dev/null +++ b/.sqlx-mysql/query-5b780dfa43760a2d3c3335277d4c404113144fdcefcec7d52e7bc7ecfee00c20.json @@ -0,0 +1,12 @@ +{ + "db_name": "MySQL", + "query": "INSERT INTO `ai_memories` (`id`, `agent_id`, `user_id`, `session_id`, `mem_key`, `content`, `category`, `importance`, `pinned`, `created_at`, `updated_at`) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + "describe": { + "columns": [], + "parameters": { + "Right": 11 + }, + "nullable": [] + }, + "hash": "5b780dfa43760a2d3c3335277d4c404113144fdcefcec7d52e7bc7ecfee00c20" +} diff --git a/.sqlx-mysql/query-6257a8c98466be78a432a055a6015b1a38252dca2a48bd37d39fd79cc3c7e852.json b/.sqlx-mysql/query-6257a8c98466be78a432a055a6015b1a38252dca2a48bd37d39fd79cc3c7e852.json deleted file mode 100644 index 61a75410..00000000 --- a/.sqlx-mysql/query-6257a8c98466be78a432a055a6015b1a38252dca2a48bd37d39fd79cc3c7e852.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "MySQL", - "query": "INSERT INTO `ai_memories` (`id`, `agent_id`, `session_id`, `mem_key`, `content`, `category`, `importance`, `pinned`, `created_at`, `updated_at`, `tenant_id`) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", - "describe": { - "columns": [], - "parameters": { - "Right": 11 - }, - "nullable": [] - }, - "hash": "6257a8c98466be78a432a055a6015b1a38252dca2a48bd37d39fd79cc3c7e852" -} diff --git a/.sqlx-mysql/query-7059fbde4c6c733eba04ba2d593ca18b2dd589a31840c07fb03ec77de6c7e945.json b/.sqlx-mysql/query-7059fbde4c6c733eba04ba2d593ca18b2dd589a31840c07fb03ec77de6c7e945.json deleted file mode 100644 index 89da17d2..00000000 --- a/.sqlx-mysql/query-7059fbde4c6c733eba04ba2d593ca18b2dd589a31840c07fb03ec77de6c7e945.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "MySQL", - "query": "INSERT INTO `ai_agents` (`id`, `owner_id`, `name`, `system_prompt`, `provider`, `model`, `temperature`, `max_iterations`, `tools`, `memory_enabled`, `params`, `created_at`, `updated_at`) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", - "describe": { - "columns": [], - "parameters": { - "Right": 13 - }, - "nullable": [] - }, - "hash": "7059fbde4c6c733eba04ba2d593ca18b2dd589a31840c07fb03ec77de6c7e945" -} diff --git a/.sqlx-mysql/query-88d45262cc05b6f8636ca3710c96c0333322de93ef5b8c12458216d7829c4cd7.json b/.sqlx-mysql/query-88d45262cc05b6f8636ca3710c96c0333322de93ef5b8c12458216d7829c4cd7.json deleted file mode 100644 index a2529829..00000000 --- a/.sqlx-mysql/query-88d45262cc05b6f8636ca3710c96c0333322de93ef5b8c12458216d7829c4cd7.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "MySQL", - "query": "INSERT INTO `ai_sessions` (`id`, `agent_id`, `owner_id`, `title`, `status`, `last_seq`, `created_at`, `updated_at`, `last_active_at`) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", - "describe": { - "columns": [], - "parameters": { - "Right": 9 - }, - "nullable": [] - }, - "hash": "88d45262cc05b6f8636ca3710c96c0333322de93ef5b8c12458216d7829c4cd7" -} diff --git a/.sqlx-mysql/query-c83bcb80850708a7dadd4465ec8162d5fb575989499443a7042dbe5baccd7816.json b/.sqlx-mysql/query-c83bcb80850708a7dadd4465ec8162d5fb575989499443a7042dbe5baccd7816.json new file mode 100644 index 00000000..2e913edb --- /dev/null +++ b/.sqlx-mysql/query-c83bcb80850708a7dadd4465ec8162d5fb575989499443a7042dbe5baccd7816.json @@ -0,0 +1,12 @@ +{ + "db_name": "MySQL", + "query": "INSERT INTO `ai_sessions` (`id`, `agent_id`, `user_id`, `title`, `status`, `last_seq`, `created_at`, `updated_at`, `last_active_at`, `tenant_id`) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + "describe": { + "columns": [], + "parameters": { + "Right": 10 + }, + "nullable": [] + }, + "hash": "c83bcb80850708a7dadd4465ec8162d5fb575989499443a7042dbe5baccd7816" +} diff --git a/.sqlx-mysql/query-d5d20d03fa613b046017dce882e43abc30f76e71c76a930b27c3f9537ab1fb9b.json b/.sqlx-mysql/query-d5d20d03fa613b046017dce882e43abc30f76e71c76a930b27c3f9537ab1fb9b.json new file mode 100644 index 00000000..9d5d6fd8 --- /dev/null +++ b/.sqlx-mysql/query-d5d20d03fa613b046017dce882e43abc30f76e71c76a930b27c3f9537ab1fb9b.json @@ -0,0 +1,12 @@ +{ + "db_name": "MySQL", + "query": "INSERT INTO `ai_agents` (`id`, `user_id`, `name`, `system_prompt`, `provider`, `model`, `temperature`, `max_iterations`, `tools`, `memory_enabled`, `params`, `created_at`, `updated_at`, `tenant_id`) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + "describe": { + "columns": [], + "parameters": { + "Right": 14 + }, + "nullable": [] + }, + "hash": "d5d20d03fa613b046017dce882e43abc30f76e71c76a930b27c3f9537ab1fb9b" +} diff --git a/.sqlx-mysql/query-dfe4c417190bf90d94b1bcadfb281a2f82fe99b421927e4b35639fef0e4ead64.json b/.sqlx-mysql/query-dfe4c417190bf90d94b1bcadfb281a2f82fe99b421927e4b35639fef0e4ead64.json deleted file mode 100644 index ce2e634b..00000000 --- a/.sqlx-mysql/query-dfe4c417190bf90d94b1bcadfb281a2f82fe99b421927e4b35639fef0e4ead64.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "MySQL", - "query": "INSERT INTO `ai_sessions` (`id`, `agent_id`, `owner_id`, `title`, `status`, `last_seq`, `created_at`, `updated_at`, `last_active_at`, `tenant_id`) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", - "describe": { - "columns": [], - "parameters": { - "Right": 10 - }, - "nullable": [] - }, - "hash": "dfe4c417190bf90d94b1bcadfb281a2f82fe99b421927e4b35639fef0e4ead64" -} diff --git a/.sqlx-mysql/query-ee8dc80ac67d6b60c27bc673226aa19d4626b67ac88254c6b23cac55e1c42969.json b/.sqlx-mysql/query-ee8dc80ac67d6b60c27bc673226aa19d4626b67ac88254c6b23cac55e1c42969.json deleted file mode 100644 index 37cd82d8..00000000 --- a/.sqlx-mysql/query-ee8dc80ac67d6b60c27bc673226aa19d4626b67ac88254c6b23cac55e1c42969.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "MySQL", - "query": "INSERT INTO `ai_agents` (`id`, `owner_id`, `name`, `system_prompt`, `provider`, `model`, `temperature`, `max_iterations`, `tools`, `memory_enabled`, `params`, `created_at`, `updated_at`, `tenant_id`) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", - "describe": { - "columns": [], - "parameters": { - "Right": 14 - }, - "nullable": [] - }, - "hash": "ee8dc80ac67d6b60c27bc673226aa19d4626b67ac88254c6b23cac55e1c42969" -} diff --git a/.sqlx-postgres/query-44ec0abd7ce2fb0cb0f29f0d2fb69f7f4a4afc27bb0e9b066075bf1c5d7c197b.json b/.sqlx-postgres/query-44ec0abd7ce2fb0cb0f29f0d2fb69f7f4a4afc27bb0e9b066075bf1c5d7c197b.json new file mode 100644 index 00000000..922a787f --- /dev/null +++ b/.sqlx-postgres/query-44ec0abd7ce2fb0cb0f29f0d2fb69f7f4a4afc27bb0e9b066075bf1c5d7c197b.json @@ -0,0 +1,27 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO \"ai_agents\" (\"id\", \"user_id\", \"name\", \"system_prompt\", \"provider\", \"model\", \"temperature\", \"max_iterations\", \"tools\", \"memory_enabled\", \"params\", \"created_at\", \"updated_at\", \"tenant_id\") VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8", + "Int8", + "Text", + "Text", + "Text", + "Text", + "Float8", + "Int4", + "Jsonb", + "Bool", + "Jsonb", + "Timestamptz", + "Timestamptz", + "Text" + ] + }, + "nullable": [] + }, + "hash": "44ec0abd7ce2fb0cb0f29f0d2fb69f7f4a4afc27bb0e9b066075bf1c5d7c197b" +} diff --git a/.sqlx-postgres/query-30658436f530909d7dfe58ecf56df46b71c996e2f8c2d3774542be9abe008823.json b/.sqlx-postgres/query-61789f63f11b7cbb5491028b75bdd6cae8faaa8e275739bbd2f2c6824f373180.json similarity index 57% rename from .sqlx-postgres/query-30658436f530909d7dfe58ecf56df46b71c996e2f8c2d3774542be9abe008823.json rename to .sqlx-postgres/query-61789f63f11b7cbb5491028b75bdd6cae8faaa8e275739bbd2f2c6824f373180.json index f8cb5762..91986673 100644 --- a/.sqlx-postgres/query-30658436f530909d7dfe58ecf56df46b71c996e2f8c2d3774542be9abe008823.json +++ b/.sqlx-postgres/query-61789f63f11b7cbb5491028b75bdd6cae8faaa8e275739bbd2f2c6824f373180.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "INSERT INTO \"ai_memories\" (\"id\", \"agent_id\", \"session_id\", \"mem_key\", \"content\", \"category\", \"importance\", \"pinned\", \"created_at\", \"updated_at\", \"tenant_id\") VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)", + "query": "INSERT INTO \"ai_memories\" (\"id\", \"agent_id\", \"user_id\", \"session_id\", \"mem_key\", \"content\", \"category\", \"importance\", \"pinned\", \"created_at\", \"updated_at\", \"tenant_id\") VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)", "describe": { "columns": [], "parameters": { @@ -8,6 +8,7 @@ "Int8", "Int8", "Int8", + "Int8", "Text", "Text", "Text", @@ -20,5 +21,5 @@ }, "nullable": [] }, - "hash": "30658436f530909d7dfe58ecf56df46b71c996e2f8c2d3774542be9abe008823" + "hash": "61789f63f11b7cbb5491028b75bdd6cae8faaa8e275739bbd2f2c6824f373180" } diff --git a/.sqlx-postgres/query-6ea327ab3b80f0f8df2d8cc5760d60bcc83e783474aa7ccab16f35306757e243.json b/.sqlx-postgres/query-6267402cb714802edc33ebcfddb3d19399453614aedab5ad9dc33d124301bf96.json similarity index 61% rename from .sqlx-postgres/query-6ea327ab3b80f0f8df2d8cc5760d60bcc83e783474aa7ccab16f35306757e243.json rename to .sqlx-postgres/query-6267402cb714802edc33ebcfddb3d19399453614aedab5ad9dc33d124301bf96.json index 233c0898..b238c05d 100644 --- a/.sqlx-postgres/query-6ea327ab3b80f0f8df2d8cc5760d60bcc83e783474aa7ccab16f35306757e243.json +++ b/.sqlx-postgres/query-6267402cb714802edc33ebcfddb3d19399453614aedab5ad9dc33d124301bf96.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "INSERT INTO \"ai_sessions\" (\"id\", \"agent_id\", \"owner_id\", \"title\", \"status\", \"last_seq\", \"created_at\", \"updated_at\", \"last_active_at\", \"tenant_id\") VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", + "query": "INSERT INTO \"ai_sessions\" (\"id\", \"agent_id\", \"user_id\", \"title\", \"status\", \"last_seq\", \"created_at\", \"updated_at\", \"last_active_at\", \"tenant_id\") VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", "describe": { "columns": [], "parameters": { @@ -19,5 +19,5 @@ }, "nullable": [] }, - "hash": "6ea327ab3b80f0f8df2d8cc5760d60bcc83e783474aa7ccab16f35306757e243" + "hash": "6267402cb714802edc33ebcfddb3d19399453614aedab5ad9dc33d124301bf96" } diff --git a/.sqlx-postgres/query-3254264892ba3ec9e97e2fe12d2a922b836cc14d0bb4ca95bf908dac0bf1eed4.json b/.sqlx-postgres/query-9090d954cfd50d60494bc2ae9d5addf0c50cf21f1d5afe3b7edf3a6e95571a9e.json similarity index 57% rename from .sqlx-postgres/query-3254264892ba3ec9e97e2fe12d2a922b836cc14d0bb4ca95bf908dac0bf1eed4.json rename to .sqlx-postgres/query-9090d954cfd50d60494bc2ae9d5addf0c50cf21f1d5afe3b7edf3a6e95571a9e.json index 496cffee..15553e75 100644 --- a/.sqlx-postgres/query-3254264892ba3ec9e97e2fe12d2a922b836cc14d0bb4ca95bf908dac0bf1eed4.json +++ b/.sqlx-postgres/query-9090d954cfd50d60494bc2ae9d5addf0c50cf21f1d5afe3b7edf3a6e95571a9e.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "INSERT INTO \"ai_memories\" (\"id\", \"agent_id\", \"session_id\", \"mem_key\", \"content\", \"category\", \"importance\", \"pinned\", \"created_at\", \"updated_at\") VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", + "query": "INSERT INTO \"ai_memories\" (\"id\", \"agent_id\", \"user_id\", \"session_id\", \"mem_key\", \"content\", \"category\", \"importance\", \"pinned\", \"created_at\", \"updated_at\") VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)", "describe": { "columns": [], "parameters": { @@ -8,6 +8,7 @@ "Int8", "Int8", "Int8", + "Int8", "Text", "Text", "Text", @@ -19,5 +20,5 @@ }, "nullable": [] }, - "hash": "3254264892ba3ec9e97e2fe12d2a922b836cc14d0bb4ca95bf908dac0bf1eed4" + "hash": "9090d954cfd50d60494bc2ae9d5addf0c50cf21f1d5afe3b7edf3a6e95571a9e" } diff --git a/.sqlx-postgres/query-bae4f2c5faef1743f83aa2f3a3bda63f2a8d2d55ef799ffdb82cf2f93d7b2cf5.json b/.sqlx-postgres/query-bae4f2c5faef1743f83aa2f3a3bda63f2a8d2d55ef799ffdb82cf2f93d7b2cf5.json deleted file mode 100644 index ca4bc668..00000000 --- a/.sqlx-postgres/query-bae4f2c5faef1743f83aa2f3a3bda63f2a8d2d55ef799ffdb82cf2f93d7b2cf5.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "INSERT INTO \"ai_agents\" (\"id\", \"owner_id\", \"name\", \"system_prompt\", \"provider\", \"model\", \"temperature\", \"max_iterations\", \"tools\", \"memory_enabled\", \"params\", \"created_at\", \"updated_at\", \"tenant_id\") VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Int8", - "Int8", - "Text", - "Text", - "Text", - "Text", - "Float8", - "Int4", - "Jsonb", - "Bool", - "Jsonb", - "Timestamptz", - "Timestamptz", - "Text" - ] - }, - "nullable": [] - }, - "hash": "bae4f2c5faef1743f83aa2f3a3bda63f2a8d2d55ef799ffdb82cf2f93d7b2cf5" -} diff --git a/.sqlx-postgres/query-c822e2f0b784b83a8d1088917be460ba12b3966a805d9ad1b9a91402eff0d8a8.json b/.sqlx-postgres/query-c822e2f0b784b83a8d1088917be460ba12b3966a805d9ad1b9a91402eff0d8a8.json deleted file mode 100644 index 10dbcb8d..00000000 --- a/.sqlx-postgres/query-c822e2f0b784b83a8d1088917be460ba12b3966a805d9ad1b9a91402eff0d8a8.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "INSERT INTO \"ai_agents\" (\"id\", \"owner_id\", \"name\", \"system_prompt\", \"provider\", \"model\", \"temperature\", \"max_iterations\", \"tools\", \"memory_enabled\", \"params\", \"created_at\", \"updated_at\") VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Int8", - "Int8", - "Text", - "Text", - "Text", - "Text", - "Float8", - "Int4", - "Jsonb", - "Bool", - "Jsonb", - "Timestamptz", - "Timestamptz" - ] - }, - "nullable": [] - }, - "hash": "c822e2f0b784b83a8d1088917be460ba12b3966a805d9ad1b9a91402eff0d8a8" -} diff --git a/.sqlx-postgres/query-38071fc5790dafcaf2378554cba715c6861b796a76421f448452aa2d744bfaf8.json b/.sqlx-postgres/query-dee2633f7202b23e318b5874aa25cbe3a33028dcff5bcf918b43dbcca4d5c2f6.json similarity index 62% rename from .sqlx-postgres/query-38071fc5790dafcaf2378554cba715c6861b796a76421f448452aa2d744bfaf8.json rename to .sqlx-postgres/query-dee2633f7202b23e318b5874aa25cbe3a33028dcff5bcf918b43dbcca4d5c2f6.json index 77be63e8..7684d694 100644 --- a/.sqlx-postgres/query-38071fc5790dafcaf2378554cba715c6861b796a76421f448452aa2d744bfaf8.json +++ b/.sqlx-postgres/query-dee2633f7202b23e318b5874aa25cbe3a33028dcff5bcf918b43dbcca4d5c2f6.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "INSERT INTO \"ai_sessions\" (\"id\", \"agent_id\", \"owner_id\", \"title\", \"status\", \"last_seq\", \"created_at\", \"updated_at\", \"last_active_at\") VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", + "query": "INSERT INTO \"ai_sessions\" (\"id\", \"agent_id\", \"user_id\", \"title\", \"status\", \"last_seq\", \"created_at\", \"updated_at\", \"last_active_at\") VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", "describe": { "columns": [], "parameters": { @@ -18,5 +18,5 @@ }, "nullable": [] }, - "hash": "38071fc5790dafcaf2378554cba715c6861b796a76421f448452aa2d744bfaf8" + "hash": "dee2633f7202b23e318b5874aa25cbe3a33028dcff5bcf918b43dbcca4d5c2f6" } diff --git a/.sqlx-postgres/query-e4f207310d01a532e016e45aa31a75271d044013ae971c69cff10320bdc47426.json b/.sqlx-postgres/query-e4f207310d01a532e016e45aa31a75271d044013ae971c69cff10320bdc47426.json new file mode 100644 index 00000000..e8ccb7c3 --- /dev/null +++ b/.sqlx-postgres/query-e4f207310d01a532e016e45aa31a75271d044013ae971c69cff10320bdc47426.json @@ -0,0 +1,26 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO \"ai_agents\" (\"id\", \"user_id\", \"name\", \"system_prompt\", \"provider\", \"model\", \"temperature\", \"max_iterations\", \"tools\", \"memory_enabled\", \"params\", \"created_at\", \"updated_at\") VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8", + "Int8", + "Text", + "Text", + "Text", + "Text", + "Float8", + "Int4", + "Jsonb", + "Bool", + "Jsonb", + "Timestamptz", + "Timestamptz" + ] + }, + "nullable": [] + }, + "hash": "e4f207310d01a532e016e45aa31a75271d044013ae971c69cff10320bdc47426" +} diff --git a/.sqlx-sqlite/query-10ee524b668c16ee8871c24c965476ae8aad0f6e2b7c2208721cf7d1250600e8.json b/.sqlx-sqlite/query-10ee524b668c16ee8871c24c965476ae8aad0f6e2b7c2208721cf7d1250600e8.json deleted file mode 100644 index e29855a2..00000000 --- a/.sqlx-sqlite/query-10ee524b668c16ee8871c24c965476ae8aad0f6e2b7c2208721cf7d1250600e8.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "INSERT INTO ai_sessions (id, agent_id, owner_id, title, status, last_seq, created_at, updated_at, last_active_at, tenant_id) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)", - "describe": { - "columns": [], - "parameters": { - "Right": 10 - }, - "nullable": [] - }, - "hash": "10ee524b668c16ee8871c24c965476ae8aad0f6e2b7c2208721cf7d1250600e8" -} diff --git a/.sqlx-sqlite/query-260fe0e1b28f5608edabdc12c5449e3f7cea8b316bae565e34a438165e7b9061.json b/.sqlx-sqlite/query-260fe0e1b28f5608edabdc12c5449e3f7cea8b316bae565e34a438165e7b9061.json deleted file mode 100644 index 5526bdb2..00000000 --- a/.sqlx-sqlite/query-260fe0e1b28f5608edabdc12c5449e3f7cea8b316bae565e34a438165e7b9061.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "INSERT INTO ai_memories (id, agent_id, session_id, mem_key, content, category, importance, pinned, created_at, updated_at, tenant_id) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)", - "describe": { - "columns": [], - "parameters": { - "Right": 11 - }, - "nullable": [] - }, - "hash": "260fe0e1b28f5608edabdc12c5449e3f7cea8b316bae565e34a438165e7b9061" -} diff --git a/.sqlx-sqlite/query-6d834ba00b4b7e63c6f7d16aefbff2e990240245c650afca2d827893e0b9ca5f.json b/.sqlx-sqlite/query-6d834ba00b4b7e63c6f7d16aefbff2e990240245c650afca2d827893e0b9ca5f.json new file mode 100644 index 00000000..848a2d85 --- /dev/null +++ b/.sqlx-sqlite/query-6d834ba00b4b7e63c6f7d16aefbff2e990240245c650afca2d827893e0b9ca5f.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO ai_agents (id, user_id, name, system_prompt, provider, model, temperature, max_iterations, tools, memory_enabled, params, created_at, updated_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13)", + "describe": { + "columns": [], + "parameters": { + "Right": 13 + }, + "nullable": [] + }, + "hash": "6d834ba00b4b7e63c6f7d16aefbff2e990240245c650afca2d827893e0b9ca5f" +} diff --git a/.sqlx-sqlite/query-848d4245ac2d8482ca4849d50dd51234b9cb1a096c547858a9d0b572d7dc32c3.json b/.sqlx-sqlite/query-848d4245ac2d8482ca4849d50dd51234b9cb1a096c547858a9d0b572d7dc32c3.json deleted file mode 100644 index 8ccdeac0..00000000 --- a/.sqlx-sqlite/query-848d4245ac2d8482ca4849d50dd51234b9cb1a096c547858a9d0b572d7dc32c3.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "INSERT INTO ai_agents (id, owner_id, name, system_prompt, provider, model, temperature, max_iterations, tools, memory_enabled, params, created_at, updated_at, tenant_id) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14)", - "describe": { - "columns": [], - "parameters": { - "Right": 14 - }, - "nullable": [] - }, - "hash": "848d4245ac2d8482ca4849d50dd51234b9cb1a096c547858a9d0b572d7dc32c3" -} diff --git a/.sqlx-sqlite/query-84aea1d0d6e6afe5219bc446abf6e6030f6e59a993749357d518d1c1ef9b5a6c.json b/.sqlx-sqlite/query-84aea1d0d6e6afe5219bc446abf6e6030f6e59a993749357d518d1c1ef9b5a6c.json new file mode 100644 index 00000000..badf49c8 --- /dev/null +++ b/.sqlx-sqlite/query-84aea1d0d6e6afe5219bc446abf6e6030f6e59a993749357d518d1c1ef9b5a6c.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO ai_sessions (id, agent_id, user_id, title, status, last_seq, created_at, updated_at, last_active_at, tenant_id) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)", + "describe": { + "columns": [], + "parameters": { + "Right": 10 + }, + "nullable": [] + }, + "hash": "84aea1d0d6e6afe5219bc446abf6e6030f6e59a993749357d518d1c1ef9b5a6c" +} diff --git a/.sqlx-sqlite/query-87a044b2745278ed15fd2820206de80249284315929caa9ff38ced5d09733ab3.json b/.sqlx-sqlite/query-87a044b2745278ed15fd2820206de80249284315929caa9ff38ced5d09733ab3.json deleted file mode 100644 index b56798be..00000000 --- a/.sqlx-sqlite/query-87a044b2745278ed15fd2820206de80249284315929caa9ff38ced5d09733ab3.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "INSERT INTO ai_memories (id, agent_id, session_id, mem_key, content, category, importance, pinned, created_at, updated_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)", - "describe": { - "columns": [], - "parameters": { - "Right": 10 - }, - "nullable": [] - }, - "hash": "87a044b2745278ed15fd2820206de80249284315929caa9ff38ced5d09733ab3" -} diff --git a/.sqlx-sqlite/query-a3fcf72d0626e33ad63d71ee772f3ec31a8e67e77b442a471d28a18c3c3f6777.json b/.sqlx-sqlite/query-a3fcf72d0626e33ad63d71ee772f3ec31a8e67e77b442a471d28a18c3c3f6777.json deleted file mode 100644 index 01de0295..00000000 --- a/.sqlx-sqlite/query-a3fcf72d0626e33ad63d71ee772f3ec31a8e67e77b442a471d28a18c3c3f6777.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "INSERT INTO ai_agents (id, owner_id, name, system_prompt, provider, model, temperature, max_iterations, tools, memory_enabled, params, created_at, updated_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13)", - "describe": { - "columns": [], - "parameters": { - "Right": 13 - }, - "nullable": [] - }, - "hash": "a3fcf72d0626e33ad63d71ee772f3ec31a8e67e77b442a471d28a18c3c3f6777" -} diff --git a/.sqlx-sqlite/query-cb30413bb7f176feb4d30d4266a00a1f90c9897188d7089d8d2dbefa7c4bc67c.json b/.sqlx-sqlite/query-cb30413bb7f176feb4d30d4266a00a1f90c9897188d7089d8d2dbefa7c4bc67c.json new file mode 100644 index 00000000..487490a1 --- /dev/null +++ b/.sqlx-sqlite/query-cb30413bb7f176feb4d30d4266a00a1f90c9897188d7089d8d2dbefa7c4bc67c.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO ai_memories (id, agent_id, user_id, session_id, mem_key, content, category, importance, pinned, created_at, updated_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)", + "describe": { + "columns": [], + "parameters": { + "Right": 11 + }, + "nullable": [] + }, + "hash": "cb30413bb7f176feb4d30d4266a00a1f90c9897188d7089d8d2dbefa7c4bc67c" +} diff --git a/.sqlx-sqlite/query-d35ba70511d480370277752d31ffa870551956cf5aa5247bc2731cc7a47a6f7e.json b/.sqlx-sqlite/query-d35ba70511d480370277752d31ffa870551956cf5aa5247bc2731cc7a47a6f7e.json deleted file mode 100644 index 82f87ab8..00000000 --- a/.sqlx-sqlite/query-d35ba70511d480370277752d31ffa870551956cf5aa5247bc2731cc7a47a6f7e.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "INSERT INTO ai_sessions (id, agent_id, owner_id, title, status, last_seq, created_at, updated_at, last_active_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)", - "describe": { - "columns": [], - "parameters": { - "Right": 9 - }, - "nullable": [] - }, - "hash": "d35ba70511d480370277752d31ffa870551956cf5aa5247bc2731cc7a47a6f7e" -} diff --git a/.sqlx-sqlite/query-d36ffc5bd0ebcab09f7544ed73f6d9d1c021e587d0220fcad2821036b6f978c5.json b/.sqlx-sqlite/query-d36ffc5bd0ebcab09f7544ed73f6d9d1c021e587d0220fcad2821036b6f978c5.json new file mode 100644 index 00000000..9c48d4da --- /dev/null +++ b/.sqlx-sqlite/query-d36ffc5bd0ebcab09f7544ed73f6d9d1c021e587d0220fcad2821036b6f978c5.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO ai_agents (id, user_id, name, system_prompt, provider, model, temperature, max_iterations, tools, memory_enabled, params, created_at, updated_at, tenant_id) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14)", + "describe": { + "columns": [], + "parameters": { + "Right": 14 + }, + "nullable": [] + }, + "hash": "d36ffc5bd0ebcab09f7544ed73f6d9d1c021e587d0220fcad2821036b6f978c5" +} diff --git a/.sqlx-sqlite/query-e89db1a60b3d6c97e3aed72b60dd19baeb00d99b320802f97d8c1571849d9b60.json b/.sqlx-sqlite/query-e89db1a60b3d6c97e3aed72b60dd19baeb00d99b320802f97d8c1571849d9b60.json new file mode 100644 index 00000000..6db16df5 --- /dev/null +++ b/.sqlx-sqlite/query-e89db1a60b3d6c97e3aed72b60dd19baeb00d99b320802f97d8c1571849d9b60.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO ai_memories (id, agent_id, user_id, session_id, mem_key, content, category, importance, pinned, created_at, updated_at, tenant_id) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12)", + "describe": { + "columns": [], + "parameters": { + "Right": 12 + }, + "nullable": [] + }, + "hash": "e89db1a60b3d6c97e3aed72b60dd19baeb00d99b320802f97d8c1571849d9b60" +} diff --git a/.sqlx-sqlite/query-ff3572478372bf0d26ca9312a8949348ef924bd7bf6361a40bf90b9e9b42c579.json b/.sqlx-sqlite/query-ff3572478372bf0d26ca9312a8949348ef924bd7bf6361a40bf90b9e9b42c579.json new file mode 100644 index 00000000..5653a1ea --- /dev/null +++ b/.sqlx-sqlite/query-ff3572478372bf0d26ca9312a8949348ef924bd7bf6361a40bf90b9e9b42c579.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO ai_sessions (id, agent_id, user_id, title, status, last_seq, created_at, updated_at, last_active_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)", + "describe": { + "columns": [], + "parameters": { + "Right": 9 + }, + "nullable": [] + }, + "hash": "ff3572478372bf0d26ca9312a8949348ef924bd7bf6361a40bf90b9e9b42c579" +} diff --git a/crates/core/src/agent/handler.rs b/crates/core/src/agent/handler.rs index cd5cdc7d..dc6fe96c 100644 --- a/crates/core/src/agent/handler.rs +++ b/crates/core/src/agent/handler.rs @@ -281,7 +281,7 @@ pub async fn get_messages( let owner = current_owner(&auth)?; let id = crate::types::snowflake_id::parse_id(&id)?; let session = ai_service::find_session(&state.pool, id, auth.tenant_id()).await?; - if session.owner_id != owner { + if session.user_id != owner { return Err(AppError::ForbiddenOwnership); } let messages = ai_service::list_messages( @@ -310,7 +310,7 @@ pub async fn compact_session( let owner = current_owner(&auth)?; let id = crate::types::snowflake_id::parse_id(&id)?; let session = ai_service::find_session(&state.pool, id, auth.tenant_id()).await?; - if session.owner_id != owner { + if session.user_id != owner { return Err(AppError::ForbiddenOwnership); } let agent = ai_service::find_agent(&state.pool, session.agent_id, auth.tenant_id()).await?; @@ -339,7 +339,7 @@ pub async fn run_turn( let owner = current_owner(&auth)?; let id = crate::types::snowflake_id::parse_id(&id)?; let session = ai_service::find_session(&state.pool, id, auth.tenant_id()).await?; - if session.owner_id != owner { + if session.user_id != owner { return Err(AppError::ForbiddenOwnership); } let agent = ai_service::find_agent(&state.pool, session.agent_id, auth.tenant_id()).await?; diff --git a/crates/core/src/agent/memory_sql.rs b/crates/core/src/agent/memory_sql.rs index a64d9a04..3f1e3417 100644 --- a/crates/core/src/agent/memory_sql.rs +++ b/crates/core/src/agent/memory_sql.rs @@ -18,14 +18,21 @@ pub struct ScopedMemory { pool: crate::db::Pool, tenant: Option, agent: SnowflakeId, + user: Option, } impl ScopedMemory { - pub fn new(pool: crate::db::Pool, tenant: Option, agent: SnowflakeId) -> Arc { + pub fn new( + pool: crate::db::Pool, + tenant: Option, + agent: SnowflakeId, + user: Option, + ) -> Arc { Arc::new(Self { pool, tenant, agent, + user, }) } @@ -45,6 +52,7 @@ impl Memory for ScopedMemory { &self.pool, self.tenant(), self.agent, + self.user, key, content, "core", @@ -62,24 +70,31 @@ impl Memory for ScopedMemory { limit: usize, ) -> Result, MemoryError> { let limit = limit.clamp(1, 50) as i64; - ai_memory::recall_memories(&self.pool, self.agent, self.tenant(), query, limit) - .await - .map(|rows| { - rows.into_iter() - // Host-managed tiers (daily logs) never surface as model - // memory; only durable Core facts are recallable/injected. - .filter(|m| m.category == "core") - .map(|m| MemoryEntry { - key: m.key, - content: m.content, - }) - .collect() - }) - .map_err(|e| MemoryError::Recall(e.to_string())) + ai_memory::recall_memories( + &self.pool, + self.agent, + self.user, + self.tenant(), + query, + limit, + ) + .await + .map(|rows| { + rows.into_iter() + // Host-managed tiers (daily logs) never surface as model + // memory; only durable Core facts are recallable/injected. + .filter(|m| m.category == "core") + .map(|m| MemoryEntry { + key: m.key, + content: m.content, + }) + .collect() + }) + .map_err(|e| MemoryError::Recall(e.to_string())) } async fn forget(&self, key: &str) -> Result { - ai_memory::forget_memory(&self.pool, self.agent, key, self.tenant()) + ai_memory::forget_memory(&self.pool, self.agent, self.user, key, self.tenant()) .await .map_err(|e| MemoryError::Forget(e.to_string())) } diff --git a/crates/core/src/agent/models/ai_agent.rs b/crates/core/src/agent/models/ai_agent.rs index 74300780..f0f05293 100644 --- a/crates/core/src/agent/models/ai_agent.rs +++ b/crates/core/src/agent/models/ai_agent.rs @@ -12,7 +12,7 @@ use crate::utils::tz::{Timestamp, now_utc}; pub struct AiAgent { pub id: SnowflakeId, pub tenant_id: Option, - pub owner_id: Option, + pub user_id: Option, pub name: String, pub system_prompt: String, pub provider: String, @@ -31,7 +31,7 @@ pub struct AiAgent { pub async fn create_agent( pool: &crate::db::Pool, tenant_id: Option<&str>, - owner_id: Option, + user_id: Option, name: &str, system_prompt: &str, provider: &str, @@ -49,7 +49,7 @@ pub async fn create_agent( "ai_agents", [ "id" => id, - "owner_id" => owner_id, + "user_id" => user_id, "name" => name, "system_prompt" => system_prompt, "provider" => provider, diff --git a/crates/core/src/agent/models/ai_memory.rs b/crates/core/src/agent/models/ai_memory.rs index 0deff2ec..92a8f5d4 100644 --- a/crates/core/src/agent/models/ai_memory.rs +++ b/crates/core/src/agent/models/ai_memory.rs @@ -31,18 +31,19 @@ pub struct AiMemory { pub updated_at: Timestamp, } -/// Find a memory row by (agent, key). +/// Find a live row scoped to (agent, user, key). pub async fn find_memory_by_key( pool: &crate::db::Pool, agent_id: SnowflakeId, + user_id: Option, key: &str, tenant_id: Option<&str>, ) -> AppResult> { + let (clause, _) = scope_clause(tenant_id, user_id, 3); let sql = format!( - "SELECT {MEMORY_COLS} FROM ai_memories WHERE agent_id = {} AND mem_key = {}{}", + "SELECT {MEMORY_COLS} FROM ai_memories WHERE agent_id = {} AND mem_key = {}{clause}", crate::db::Driver::ph(1), - crate::db::Driver::ph(2), - tenant_filter(tenant_id, 3) + crate::db::Driver::ph(2) ); let mut q = sqlx::query_as::<_, AiMemory>(crate::db::safe_sql(&sql)) .bind(agent_id) @@ -50,22 +51,26 @@ pub async fn find_memory_by_key( if let Some(tid) = tenant_id { q = q.bind(tid); } + if let Some(uid) = user_id { + q = q.bind(uid); + } Ok(q.fetch_optional(pool).await?) } -/// Upsert by (tenant, agent, key): update the live row content or insert. +/// Upsert by (tenant, agent, user, key): update the live row content or insert. #[allow(clippy::too_many_arguments)] pub async fn store_memory( pool: &crate::db::Pool, tenant_id: Option<&str>, agent_id: SnowflakeId, + user_id: Option, key: &str, content: &str, category: &str, importance: f64, pinned: bool, ) -> AppResult { - if let Some(existing) = find_memory_by_key(pool, agent_id, key, tenant_id).await? { + if let Some(existing) = find_memory_by_key(pool, agent_id, user_id, key, tenant_id).await? { let now = now_utc(); let result = raisfast_derive::crud_update!( pool, @@ -86,6 +91,7 @@ pub async fn store_memory( [ "id" => id, "agent_id" => agent_id, + "user_id" => user_id, "session_id" => None::, "mem_key" => key, "content" => content, @@ -97,18 +103,18 @@ pub async fn store_memory( ], tenant: tenant_id )?; - match find_memory_by_key(pool, agent_id, key, tenant_id).await? { + match find_memory_by_key(pool, agent_id, user_id, key, tenant_id).await? { Some(m) => Ok(m), None => Err(AppError::not_found("ai_memory")), } } -/// Keyword recall over live rows. `query = None` returns most recent first. -/// Keyword matching is portable `LIKE` (case-sensitive on PG/MySQL); a proper -/// keyword/BM25 rank is a later enhancement. +/// Keyword recall over live rows, scoped to one user. `query = None` returns +/// most recent first. pub async fn recall_memories( pool: &crate::db::Pool, agent_id: SnowflakeId, + user_id: Option, tenant_id: Option<&str>, query: Option<&str>, limit: i64, @@ -122,11 +128,8 @@ pub async fn recall_memories( "SELECT {MEMORY_COLS} FROM ai_memories WHERE agent_id = {} AND superseded_by IS NULL", crate::db::Driver::ph(1) ); - let mut n = 2; - if tenant_id.is_some() { - sql.push_str(&format!(" AND tenant_id = {}", crate::db::Driver::ph(n))); - n += 1; - } + let (tail, mut n) = scope_clause(tenant_id, user_id, 2); + sql.push_str(&tail); if keyword.is_some() { sql.push_str(&format!( " AND (mem_key LIKE {} OR content LIKE {})", @@ -144,24 +147,28 @@ pub async fn recall_memories( if let Some(tid) = tenant_id { q = q.bind(tid); } + if let Some(uid) = user_id { + q = q.bind(uid); + } if let Some(pat) = &keyword { q = q.bind(pat.as_str()).bind(pat.as_str()); } Ok(q.bind(limit).fetch_all(pool).await?) } -/// Delete a memory row by (agent, key). Returns whether a row was removed. +/// Delete a memory row scoped to (agent, user, key). pub async fn forget_memory( pool: &crate::db::Pool, agent_id: SnowflakeId, + user_id: Option, key: &str, tenant_id: Option<&str>, ) -> AppResult { + let (clause, _) = scope_clause(tenant_id, user_id, 3); let sql = format!( - "DELETE FROM ai_memories WHERE agent_id = {} AND mem_key = {}{}", + "DELETE FROM ai_memories WHERE agent_id = {} AND mem_key = {}{clause}", crate::db::Driver::ph(1), - crate::db::Driver::ph(2), - tenant_filter(tenant_id, 3) + crate::db::Driver::ph(2) ); let mut q = sqlx::query(crate::db::safe_sql(&sql)) .bind(agent_id) @@ -169,14 +176,30 @@ pub async fn forget_memory( if let Some(tid) = tenant_id { q = q.bind(tid); } + if let Some(uid) = user_id { + q = q.bind(uid); + } Ok(q.execute(pool).await?.rows_affected() > 0) } -/// `" AND tenant_id = {ph}"` for `Some`, `""` for `None`. -fn tenant_filter(tenant_id: Option<&str>, start_index: usize) -> String { - tenant_id - .map(|_| format!(" AND tenant_id = {}", crate::db::Driver::ph(start_index))) - .unwrap_or_default() +/// Optional scope clause ` AND tenant_id = ? AND user_id = ?` (either/both). +/// Returns the next free placeholder index. +fn scope_clause( + tenant_id: Option<&str>, + user_id: Option, + start_index: usize, +) -> (String, usize) { + let mut out = String::new(); + let mut n = start_index; + if tenant_id.is_some() { + out.push_str(&format!(" AND tenant_id = {}", crate::db::Driver::ph(n))); + n += 1; + } + if user_id.is_some() { + out.push_str(&format!(" AND user_id = {}", crate::db::Driver::ph(n))); + n += 1; + } + (out, n) } /// Memory-tier budget (port of zeroclaw `budget.rs` caps): row and byte caps @@ -207,25 +230,34 @@ pub struct BudgetReport { pub evicted_by_bytes: i64, } -fn live_scope_sql(tenant_id: Option<&str>, start_index: usize) -> String { - // start_index is the placeholder slot for `category`; agent goes next, then - // the optional tenant filter. - let agent_ph = crate::db::Driver::ph(start_index + 1); - let mut sql = format!( - " WHERE category = {} AND agent_id = {agent_ph} AND superseded_by IS NULL", - crate::db::Driver::ph(start_index) +fn live_scope( + tenant_id: Option<&str>, + user_id: Option, + start_index: usize, +) -> (String, usize) { + let mut out = format!( + " WHERE category = {} AND agent_id = {} AND superseded_by IS NULL", + crate::db::Driver::ph(start_index), + crate::db::Driver::ph(start_index + 1) ); - sql.push_str(&tenant_filter(tenant_id, start_index + 2)); - sql + let mut n = start_index + 2; + if tenant_id.is_some() { + out.push_str(&format!(" AND tenant_id = {}", crate::db::Driver::ph(n))); + n += 1; + } + if user_id.is_some() { + out.push_str(&format!(" AND user_id = {}", crate::db::Driver::ph(n))); + n += 1; + } + (out, n) } -/// Compact one category to its configured budget (lowest `importance` first, -/// ties broken by `created_at`). Live rows = `superseded_by IS NULL`; rows that -/// were superseded are never budget-evicted (they are soft-hidden history). +/// Compact one category to its configured budget, scoped to (agent, user). pub async fn compact_category_to_budget( pool: &crate::db::Pool, tenant_id: Option<&str>, agent_id: SnowflakeId, + user_id: Option, category: &str, cfg: MemoryBudgetConfig, ) -> AppResult { @@ -237,10 +269,10 @@ pub async fn compact_category_to_budget( // ── row cap ───────────────────────────────────────────────────────────── if max_rows > 0 { - let current = count_live(pool, tenant_id, agent_id, category).await?; + let current = count_live(pool, tenant_id, agent_id, user_id, category).await?; if current > max_rows { let excess = current - max_rows; - let ids = evictable_ids(pool, tenant_id, agent_id, category, excess).await?; + let ids = evictable_ids(pool, tenant_id, agent_id, user_id, category, excess).await?; report.evicted_by_count += delete_ids(pool, &ids).await? as i64; } } @@ -248,11 +280,11 @@ pub async fn compact_category_to_budget( // ── byte cap: evict one row at a time until under budget ─────────────── if max_bytes > 0 { loop { - let current_bytes = live_bytes(pool, tenant_id, agent_id, category).await?; + let current_bytes = live_bytes(pool, tenant_id, agent_id, user_id, category).await?; if current_bytes <= max_bytes { break; } - let ids = evictable_ids(pool, tenant_id, agent_id, category, 1).await?; + let ids = evictable_ids(pool, tenant_id, agent_id, user_id, category, 1).await?; if ids.is_empty() { break; } @@ -266,18 +298,22 @@ async fn count_live( pool: &crate::db::Pool, tenant_id: Option<&str>, agent_id: SnowflakeId, + user_id: Option, category: &str, ) -> AppResult { + let (clause, _) = live_scope(tenant_id, user_id, 1); let sql = format!( - "SELECT {} FROM ai_memories{}", - crate::db::Driver::cast_int("COUNT(*)"), - live_scope_sql(tenant_id, 1) + "SELECT {} FROM ai_memories{clause}", + crate::db::Driver::cast_int("COUNT(*)") ); let mut q = sqlx::query_scalar::<_, i64>(crate::db::safe_sql(&sql)).bind(category); q = q.bind(agent_id); if let Some(tid) = tenant_id { q = q.bind(tid); } + if let Some(uid) = user_id { + q = q.bind(uid); + } Ok(q.fetch_one(pool).await?) } @@ -285,42 +321,47 @@ async fn live_bytes( pool: &crate::db::Pool, tenant_id: Option<&str>, agent_id: SnowflakeId, + user_id: Option, category: &str, ) -> AppResult { + let (clause, _) = live_scope(tenant_id, user_id, 1); let sql = format!( - "SELECT {} FROM ai_memories{}", - crate::db::Driver::cast_int("COALESCE(SUM(LENGTH(content)), 0)"), - live_scope_sql(tenant_id, 1) + "SELECT {} FROM ai_memories{clause}", + crate::db::Driver::cast_int("COALESCE(SUM(LENGTH(content)), 0)") ); let mut q = sqlx::query_scalar::<_, i64>(crate::db::safe_sql(&sql)).bind(category); q = q.bind(agent_id); if let Some(tid) = tenant_id { q = q.bind(tid); } + if let Some(uid) = user_id { + q = q.bind(uid); + } Ok(q.fetch_one(pool).await?) } -/// Lowest-value live rows (importance asc, created_at asc), up to `limit`. +/// Lowest-value live rows for (agent, user), up to `limit`. async fn evictable_ids( pool: &crate::db::Pool, tenant_id: Option<&str>, agent_id: SnowflakeId, + user_id: Option, category: &str, limit: i64, ) -> AppResult> { - // Placeholder slots: category(1), agent(2), tenant(3 if present); the LIMIT - // placeholder follows the last scope bind. - let limit_ph = crate::db::Driver::ph(if tenant_id.is_some() { 4 } else { 3 }); + let (clause, n) = live_scope(tenant_id, user_id, 1); + let limit_ph = crate::db::Driver::ph(n); let sql = format!( - "SELECT id FROM ai_memories{} AND pinned = FALSE ORDER BY importance ASC, created_at ASC LIMIT {}", - live_scope_sql(tenant_id, 1), - limit_ph + "SELECT id FROM ai_memories{clause} AND pinned = FALSE ORDER BY importance ASC, created_at ASC LIMIT {limit_ph}" ); let mut q = sqlx::query_scalar::<_, i64>(crate::db::safe_sql(&sql)).bind(category); q = q.bind(agent_id); if let Some(tid) = tenant_id { q = q.bind(tid); } + if let Some(uid) = user_id { + q = q.bind(uid); + } Ok(q.bind(limit) .fetch_all(pool) .await? diff --git a/crates/core/src/agent/models/ai_session.rs b/crates/core/src/agent/models/ai_session.rs index 9545caef..ce25b65a 100644 --- a/crates/core/src/agent/models/ai_session.rs +++ b/crates/core/src/agent/models/ai_session.rs @@ -15,7 +15,7 @@ pub struct AiSession { pub id: SnowflakeId, pub tenant_id: Option, pub agent_id: SnowflakeId, - pub owner_id: SnowflakeId, + pub user_id: SnowflakeId, pub title: String, pub status: String, pub meta: Option, @@ -31,7 +31,7 @@ pub async fn create_session( pool: &crate::db::Pool, tenant_id: Option<&str>, agent_id: SnowflakeId, - owner_id: SnowflakeId, + user_id: SnowflakeId, title: &str, ) -> AppResult { let id = crate::utils::id::new_snowflake_id(); @@ -42,7 +42,7 @@ pub async fn create_session( [ "id" => id, "agent_id" => agent_id, - "owner_id" => owner_id, + "user_id" => user_id, "title" => title, "status" => "open", "last_seq" => 0i64, @@ -78,7 +78,7 @@ pub async fn list_sessions( agent_id: SnowflakeId, ) -> AppResult> { let sql = format!( - "SELECT id, tenant_id, agent_id, owner_id, title, status, meta, last_seq, \ + "SELECT id, tenant_id, agent_id, user_id, title, status, meta, last_seq, \ created_at, updated_at, last_active_at FROM ai_sessions \ WHERE agent_id = {}{} ORDER BY last_active_at DESC", crate::db::Driver::ph(1), diff --git a/crates/core/src/agent/service.rs b/crates/core/src/agent/service.rs index 5131fa43..40e104eb 100644 --- a/crates/core/src/agent/service.rs +++ b/crates/core/src/agent/service.rs @@ -422,9 +422,15 @@ async fn run_turn_inner( ai_message::list_messages_after(pool, session_id, tenant_id, None, 10_000).await?; let mut history: Vec = existing.iter().filter_map(row_to_chat_message).collect(); + // Session owner = the memory user: all long-term memory is scoped + // (tenant, agent, user) so different users never share facts. + let session_owner = + crate::agent::models::ai_session::find_session_by_id(pool, session_id, tenant_id).await?; + let memory_user: Option = Some(session_owner.user_id); + // Memory-tier hygiene (zeroclaw budget.rs semantics): keep core/daily rows // within configured caps. Best effort — eviction failures never fail a turn. - memory_hygiene(pool, ai, agent.id, tenant_id).await; + memory_hygiene(pool, ai, agent.id, tenant_id, memory_user).await; // We always send an assembled framework system prompt (agent/system_prompt // is embedded inside it), so the engine always inserts one leading System. let had_system = true; @@ -440,7 +446,7 @@ async fn run_turn_inner( seq += 1; // Build the engine with a scoped memory handle + its tools. - let memory = ScopedMemory::new(pool.clone(), agent.tenant_id.clone(), agent.id); + let memory = ScopedMemory::new(pool.clone(), agent.tenant_id.clone(), agent.id, memory_user); let mut tools = extra_tools.unwrap_or_default(); register_memory_tools(&mut tools, memory.clone()); apply_tool_allowlist(&mut tools, agent); @@ -571,6 +577,7 @@ async fn run_turn_inner( ai, agent, tenant_id, + memory_user, &existing, prior_cover, cover_seq, @@ -709,7 +716,7 @@ async fn run_turn_inner( ) .await?; ai_session::advance_last_seq(pool, session_id, tenant_id, seq).await?; - touch_daily_log(pool, agent, tenant_id).await; + touch_daily_log(pool, agent, tenant_id, memory_user).await; Ok(AgentTurnResult { text: outcome.text, @@ -812,19 +819,19 @@ pub async fn update_agent( crate::agent::models::ai_agent::find_agent_by_id(pool, id, tenant_id).await } -/// Create a session owned by `owner_id` on an agent. +/// Create a session owned by `user_id` on an agent. pub async fn create_session( pool: &crate::db::Pool, tenant: Option, agent_id: SnowflakeId, - owner_id: SnowflakeId, + user_id: SnowflakeId, title: &str, ) -> AppResult { crate::agent::models::ai_session::create_session( pool, tenant.as_deref(), agent_id, - owner_id, + user_id, title, ) .await @@ -839,15 +846,15 @@ pub async fn find_session( crate::agent::models::ai_session::find_session_by_id(pool, id, tenant).await } -/// Sessions of one agent owned by `owner_id`. +/// Sessions of one agent owned by `user_id`. pub async fn list_my_sessions( pool: &crate::db::Pool, tenant: Option<&str>, agent_id: SnowflakeId, - owner_id: SnowflakeId, + user_id: SnowflakeId, ) -> AppResult> { let all = crate::agent::models::ai_session::list_sessions(pool, tenant, agent_id).await?; - Ok(all.into_iter().filter(|s| s.owner_id == owner_id).collect()) + Ok(all.into_iter().filter(|s| s.user_id == user_id).collect()) } /// Replay slice of the session log. @@ -869,6 +876,7 @@ async fn memory_hygiene( ai: &AiConfig, agent_id: SnowflakeId, tenant: Option<&str>, + user: Option, ) { use crate::agent::models::ai_memory::MemoryBudgetConfig; let budget = MemoryBudgetConfig { @@ -881,7 +889,7 @@ async fn memory_hygiene( } for category in ["core", "daily"] { match crate::agent::models::ai_memory::compact_category_to_budget( - pool, tenant, agent_id, category, budget, + pool, tenant, agent_id, user, category, budget, ) .await { @@ -1007,6 +1015,7 @@ async fn consolidate_folded_memory( ai: &AiConfig, agent: &AiAgent, tenant: Option<&str>, + user: Option, slice_text: &str, ) -> AppResult<()> { let provider = provider_for(agent, ai)?; @@ -1053,7 +1062,7 @@ async fn consolidate_folded_memory( let mut existing_norms: Vec = if list.is_empty() { Vec::new() } else { - crate::agent::models::ai_memory::recall_memories(pool, agent.id, tenant, None, 1_000) + crate::agent::models::ai_memory::recall_memories(pool, agent.id, user, tenant, None, 1_000) .await? .into_iter() .filter(|m| m.category == "core") @@ -1092,7 +1101,7 @@ async fn consolidate_folded_memory( // LLM wording/numbering drift) upserts instead of piling duplicates. let key = format!("fact_{}", &sha256_hex(&norm)[..12]); if let Err(e) = crate::agent::models::ai_memory::store_memory( - pool, tenant, agent.id, &key, content, "core", importance, false, + pool, tenant, agent.id, user, &key, content, "core", importance, false, ) .await { @@ -1113,11 +1122,13 @@ async fn consolidate_folded_memory( /// Extract durable facts from rows whose `seq` falls in `(prior_cover, /// new_cover]` — i.e. exactly the turns this fold took out of the window. +#[allow(clippy::too_many_arguments)] async fn consolidate_folded_range( pool: &crate::db::Pool, ai: &AiConfig, agent: &AiAgent, tenant: Option<&str>, + user: Option, existing: &[AiMessage], prior_cover: i64, new_cover: i64, @@ -1130,7 +1141,7 @@ async fn consolidate_folded_range( .collect(); if !rows.is_empty() { let text = crate::agent::context::fold_text(&rows); - consolidate_folded_memory(pool, ai, agent, tenant, &text).await?; + consolidate_folded_memory(pool, ai, agent, tenant, user, &text).await?; } Ok(()) } @@ -1170,6 +1181,9 @@ pub async fn compact_session( .unwrap_or(8_000); let prev_ctx = latest_ctx_row(&existing); let prior_cover = prev_ctx.as_ref().map_or(0, |p| p.cover_seq); + let session_owner = + crate::agent::models::ai_session::find_session_by_id(pool, session_id, tenant).await?; + let compact_user: Option = Some(session_owner.user_id); let (cover, summary, folded_now) = ensure_ctx_window(ai, agent, &existing, prev_ctx, 0, tail, true).await?; if folded_now && let Some(text) = summary.as_deref() { @@ -1188,9 +1202,17 @@ pub async fn compact_session( // Manual compact also consolidates the freshly folded turns into Core // memory (same semantics as the automatic fold path). if ai.memory_consolidate - && let Err(e) = - consolidate_folded_range(pool, ai, agent, tenant, &existing, prior_cover, cover) - .await + && let Err(e) = consolidate_folded_range( + pool, + ai, + agent, + tenant, + compact_user, + &existing, + prior_cover, + cover, + ) + .await { tracing::warn!(session = session_id.0, error = %e, "memory consolidation failed"); } else if !ai.memory_consolidate { @@ -1346,13 +1368,18 @@ async fn ensure_ctx_window( /// Host-managed daily session log (zeroclaw `Daily` tier): one row per agent /// per day, auto-updated after each turn; never surfaced via model recall and /// subject to the `daily_max_rows` budget. Not model-initiated. -async fn touch_daily_log(pool: &crate::db::Pool, agent: &AiAgent, tenant: Option<&str>) { +async fn touch_daily_log( + pool: &crate::db::Pool, + agent: &AiAgent, + tenant: Option<&str>, + user: Option, +) { let now = crate::utils::tz::now_utc(); let date = now.format("%Y-%m-%d").to_string(); let key = format!("daily_{date}"); let content = format!("{date} 当日会话活跃记录(host 自动维护)"); if let Err(e) = crate::agent::models::ai_memory::store_memory( - pool, tenant, agent.id, &key, &content, "daily", 0.3, false, + pool, tenant, agent.id, user, &key, &content, "daily", 0.3, false, ) .await { diff --git a/crates/core/src/worker/handlers/agent_run.rs b/crates/core/src/worker/handlers/agent_run.rs index 596cd052..75d8895f 100644 --- a/crates/core/src/worker/handlers/agent_run.rs +++ b/crates/core/src/worker/handlers/agent_run.rs @@ -128,7 +128,7 @@ impl JobHandler for AgentRunHandler { &self.pool, Some(tenant), agent_id, - agent.owner_id.unwrap_or(agent_id), + agent.user_id.unwrap_or(agent_id), "scheduled", ) .await? diff --git a/crates/core/tests/ai_runtime.rs b/crates/core/tests/ai_runtime.rs index 0471a49f..d1029a5b 100644 --- a/crates/core/tests/ai_runtime.rs +++ b/crates/core/tests/ai_runtime.rs @@ -167,7 +167,12 @@ async fn ai_models_roundtrip() { assert_eq!(sess.last_seq, seq); // memory via ScopedMemory + keyword recall - let memory = ScopedMemory::new(pool.clone(), Some(tenant_id.clone()), agent.id); + let memory = ScopedMemory::new( + pool.clone(), + Some(tenant_id.clone()), + agent.id, + Some(agent.id), + ); memory .store("nickname", "小明") .await @@ -175,7 +180,7 @@ async fn ai_models_roundtrip() { let hits = memory.recall(Some("小明"), 5).await.expect("memory recall"); assert_eq!(hits.len(), 1); assert_eq!(hits[0].key, "nickname"); - let recent = ai_memory::recall_memories(&pool, agent.id, Some(&tenant_id), None, 5) + let recent = ai_memory::recall_memories(&pool, agent.id, None, Some(&tenant_id), None, 5) .await .expect("model recall"); assert!(!recent.is_empty()); @@ -223,6 +228,7 @@ async fn memory_budget_evicts_by_importance_and_spares_conversation() { &pool, Some(&tenant_id), agent.id, + None, &format!("k{}", i + 1), &format!("core fact {}", i + 1), "core", @@ -237,6 +243,7 @@ async fn memory_budget_evicts_by_importance_and_spares_conversation() { &pool, Some(&tenant_id), agent.id, + None, "pinned-rule", "pinned low-value rule", "core", @@ -251,6 +258,7 @@ async fn memory_budget_evicts_by_importance_and_spares_conversation() { &pool, Some(&tenant_id), agent.id, + None, &format!("c{i}"), "chatter", "conversation", @@ -266,17 +274,29 @@ async fn memory_budget_evicts_by_importance_and_spares_conversation() { core_max_bytes: 0, daily_max_rows: 0, }; + let all_core: i64 = sqlx::query_scalar( + "SELECT COUNT(*) FROM ai_memories WHERE agent_id=$1 AND category='core'", + ) + .bind(agent.id) + .fetch_one(&pool) + .await + .unwrap(); + eprintln!("core rows before compact: {all_core}"); let report = - ai_memory::compact_category_to_budget(&pool, Some(&tenant_id), agent.id, "core", cfg) + ai_memory::compact_category_to_budget(&pool, Some(&tenant_id), agent.id, None, "core", cfg) .await .expect("compact core"); + eprintln!( + "report count={} bytes={}", + report.evicted_by_count, report.evicted_by_bytes + ); assert_eq!( report.evicted_by_count, 4, "four lowest-value non-pinned core rows evicted (pinned counts toward the row cap)" ); assert_eq!(report.evicted_by_bytes, 0); - let live = ai_memory::recall_memories(&pool, agent.id, Some(&tenant_id), None, 100) + let live = ai_memory::recall_memories(&pool, agent.id, None, Some(&tenant_id), None, 100) .await .expect("recall"); let mut core_keys: Vec<&str> = live @@ -302,6 +322,7 @@ async fn memory_budget_evicts_by_importance_and_spares_conversation() { &pool, Some(&tenant_id), agent.id, + None, &format!("d{}", i + 1), "day log", "daily", @@ -320,6 +341,7 @@ async fn memory_budget_evicts_by_importance_and_spares_conversation() { &pool, Some(&tenant_id), agent.id, + None, "daily", daily_cfg, ) @@ -327,7 +349,7 @@ async fn memory_budget_evicts_by_importance_and_spares_conversation() { .expect("compact daily"); assert_eq!(d_report.evicted_by_count, 2, "daily trimmed to its row cap"); let daily_keys: Vec = - ai_memory::recall_memories(&pool, agent.id, Some(&tenant_id), None, 100) + ai_memory::recall_memories(&pool, agent.id, None, Some(&tenant_id), None, 100) .await .expect("recall daily") .into_iter() @@ -350,3 +372,78 @@ async fn memory_budget_evicts_by_importance_and_spares_conversation() { .await .expect("delete agent"); } + +/// Two users on the same agent/tenant must never see each other's memory. +#[tokio::test] +async fn memory_is_isolated_per_user() { + let pool = test_pool(); + let tenant_id = tenant(); + let agent = ai_agent::create_agent( + &pool, + Some(&tenant_id), + None, + "iso-agent", + "iso", + "openai_compat", + "test-model", + None, + Vec::new(), + true, + None, + ) + .await + .expect("create agent"); + + let user_a = raisfast::utils::id::new_snowflake_id(); + let user_b = raisfast::utils::id::new_snowflake_id(); + + ai_memory::store_memory( + &pool, + Some(&tenant_id), + agent.id, + Some(user_a), + "pref", + "爱吃辣", + "core", + 0.8, + false, + ) + .await + .expect("store a"); + ai_memory::store_memory( + &pool, + Some(&tenant_id), + agent.id, + Some(user_b), + "pref", + "不吃辣", + "core", + 0.8, + false, + ) + .await + .expect("store b"); + + let ra = ai_memory::recall_memories(&pool, agent.id, Some(user_a), Some(&tenant_id), None, 10) + .await + .expect("recall a"); + let rb = ai_memory::recall_memories(&pool, agent.id, Some(user_b), Some(&tenant_id), None, 10) + .await + .expect("recall b"); + + assert_eq!(ra.len(), 1); + assert_eq!(rb.len(), 1); + assert!(ra[0].content.contains("爱吃辣")); + assert!(rb[0].content.contains("不吃辣")); + assert!(ra[0].key == rb[0].key, "same logical key, different users"); + + // cleanup + sqlx::query("DELETE FROM ai_memories WHERE tenant_id = $1") + .bind(&tenant_id) + .execute(&pool) + .await + .expect("clean"); + ai_agent::delete_agent(&pool, agent.id, Some(&tenant_id)) + .await + .expect("del"); +} diff --git a/migrations/mysql/schema.mysql.sql b/migrations/mysql/schema.mysql.sql index 71e48d74..69b62e9a 100644 --- a/migrations/mysql/schema.mysql.sql +++ b/migrations/mysql/schema.mysql.sql @@ -1447,7 +1447,7 @@ CREATE TABLE IF NOT EXISTS flow_trigger ( CREATE TABLE IF NOT EXISTS ai_agents ( id BIGINT PRIMARY KEY, tenant_id VARCHAR(36) NOT NULL DEFAULT 'default', - owner_id BIGINT NULL, + user_id BIGINT NULL, name VARCHAR(255) NOT NULL, system_prompt TEXT NOT NULL, provider VARCHAR(64) NOT NULL, @@ -1467,7 +1467,7 @@ CREATE TABLE IF NOT EXISTS ai_sessions ( id BIGINT PRIMARY KEY, tenant_id VARCHAR(36) NOT NULL DEFAULT 'default', agent_id BIGINT NOT NULL, - owner_id BIGINT NOT NULL, + user_id BIGINT NOT NULL, title VARCHAR(500) NOT NULL DEFAULT '', status VARCHAR(32) NOT NULL DEFAULT 'open', meta JSON NULL, @@ -1476,7 +1476,7 @@ CREATE TABLE IF NOT EXISTS ai_sessions ( updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, last_active_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, INDEX idx_ai_sessions_tenant_agent (tenant_id, agent_id), - INDEX idx_ai_sessions_owner_active (owner_id, last_active_at) + INDEX idx_ai_sessions_owner_active (user_id, last_active_at) ); CREATE TABLE IF NOT EXISTS ai_messages ( @@ -1506,6 +1506,7 @@ CREATE TABLE IF NOT EXISTS ai_memories ( id BIGINT PRIMARY KEY, tenant_id VARCHAR(36) NOT NULL DEFAULT 'default', agent_id BIGINT NOT NULL, + user_id BIGINT, session_id BIGINT NULL, mem_key VARCHAR(255) NOT NULL, content TEXT NOT NULL, @@ -1515,7 +1516,7 @@ CREATE TABLE IF NOT EXISTS ai_memories ( pinned BOOLEAN NOT NULL DEFAULT FALSE, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - UNIQUE (tenant_id, agent_id, mem_key), + UNIQUE (tenant_id, agent_id, user_id, mem_key), INDEX idx_ai_memories_agent_live (agent_id, superseded_by), INDEX idx_ai_memories_agent_category (agent_id, category) ); diff --git a/migrations/postgres/schema.postgres.sql b/migrations/postgres/schema.postgres.sql index 175ff0f4..3c6c5d23 100644 --- a/migrations/postgres/schema.postgres.sql +++ b/migrations/postgres/schema.postgres.sql @@ -1510,7 +1510,7 @@ CREATE INDEX IF NOT EXISTS idx_flow_trigger_flow ON flow_trigger(flow_id); CREATE TABLE IF NOT EXISTS ai_agents ( id BIGINT PRIMARY KEY, tenant_id TEXT NOT NULL DEFAULT 'default', - owner_id BIGINT, + user_id BIGINT, name TEXT NOT NULL, system_prompt TEXT NOT NULL DEFAULT '', provider TEXT NOT NULL, @@ -1531,7 +1531,7 @@ CREATE TABLE IF NOT EXISTS ai_sessions ( id BIGINT PRIMARY KEY, tenant_id TEXT NOT NULL DEFAULT 'default', agent_id BIGINT NOT NULL, - owner_id BIGINT NOT NULL, + user_id BIGINT NOT NULL, title TEXT NOT NULL DEFAULT '', status TEXT NOT NULL DEFAULT 'open', meta JSONB, @@ -1541,7 +1541,7 @@ CREATE TABLE IF NOT EXISTS ai_sessions ( last_active_at TIMESTAMPTZ(0) NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_ai_sessions_tenant_agent ON ai_sessions(tenant_id, agent_id); -CREATE INDEX IF NOT EXISTS idx_ai_sessions_owner_active ON ai_sessions(owner_id, last_active_at); +CREATE INDEX IF NOT EXISTS idx_ai_sessions_owner_active ON ai_sessions(user_id, last_active_at); -- Append-only conversation event log (externalized session). role: system/user/ -- assistant/tool/meta. kind: chat/assistant_tool_calls/tool_result/turn:meta/ @@ -1574,6 +1574,7 @@ CREATE TABLE IF NOT EXISTS ai_memories ( id BIGINT PRIMARY KEY, tenant_id TEXT NOT NULL DEFAULT 'default', agent_id BIGINT NOT NULL, + user_id BIGINT, session_id BIGINT, mem_key TEXT NOT NULL, content TEXT NOT NULL, @@ -1583,7 +1584,7 @@ CREATE TABLE IF NOT EXISTS ai_memories ( pinned BOOLEAN NOT NULL DEFAULT FALSE, created_at TIMESTAMPTZ(0) NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ(0) NOT NULL DEFAULT NOW(), - UNIQUE (tenant_id, agent_id, mem_key) + UNIQUE (tenant_id, agent_id, user_id, mem_key) ); CREATE INDEX IF NOT EXISTS idx_ai_memories_agent_live ON ai_memories(agent_id, superseded_by); CREATE INDEX IF NOT EXISTS idx_ai_memories_agent_category ON ai_memories(agent_id, category); diff --git a/migrations/sqlite/schema.sqlite.sql b/migrations/sqlite/schema.sqlite.sql index 6f0fcfda..628cd0aa 100644 --- a/migrations/sqlite/schema.sqlite.sql +++ b/migrations/sqlite/schema.sqlite.sql @@ -1507,7 +1507,7 @@ CREATE INDEX IF NOT EXISTS idx_flow_trigger_flow ON flow_trigger(flow_id); CREATE TABLE IF NOT EXISTS ai_agents ( id INTEGER PRIMARY KEY, tenant_id TEXT NOT NULL DEFAULT 'default', - owner_id INTEGER, + user_id INTEGER, name TEXT NOT NULL, system_prompt TEXT NOT NULL DEFAULT '', provider TEXT NOT NULL, @@ -1527,7 +1527,7 @@ CREATE TABLE IF NOT EXISTS ai_sessions ( id INTEGER PRIMARY KEY, tenant_id TEXT NOT NULL DEFAULT 'default', agent_id INTEGER NOT NULL, - owner_id INTEGER NOT NULL, + user_id INTEGER NOT NULL, title TEXT NOT NULL DEFAULT '', status TEXT NOT NULL DEFAULT 'open', meta TEXT, @@ -1537,7 +1537,7 @@ CREATE TABLE IF NOT EXISTS ai_sessions ( last_active_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now')) ); CREATE INDEX IF NOT EXISTS idx_ai_sessions_tenant_agent ON ai_sessions(tenant_id, agent_id); -CREATE INDEX IF NOT EXISTS idx_ai_sessions_owner_active ON ai_sessions(owner_id, last_active_at); +CREATE INDEX IF NOT EXISTS idx_ai_sessions_owner_active ON ai_sessions(user_id, last_active_at); CREATE TABLE IF NOT EXISTS ai_messages ( id INTEGER PRIMARY KEY, @@ -1566,6 +1566,7 @@ CREATE TABLE IF NOT EXISTS ai_memories ( id INTEGER PRIMARY KEY, tenant_id TEXT NOT NULL DEFAULT 'default', agent_id INTEGER NOT NULL, + user_id BIGINT, session_id INTEGER, mem_key TEXT NOT NULL, content TEXT NOT NULL, @@ -1575,7 +1576,7 @@ CREATE TABLE IF NOT EXISTS ai_memories ( pinned INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now')), updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now')), - UNIQUE (tenant_id, agent_id, mem_key) + UNIQUE (tenant_id, agent_id, user_id, mem_key) ); CREATE INDEX IF NOT EXISTS idx_ai_memories_agent_live ON ai_memories(agent_id, superseded_by); CREATE INDEX IF NOT EXISTS idx_ai_memories_agent_category ON ai_memories(agent_id, category);