From 0db8a2822f30d67e6bce8b36958243cb2f63991f Mon Sep 17 00:00:00 2001 From: iven Date: Sat, 21 Mar 2026 00:36:06 +0800 Subject: [PATCH] feat(backend): implement Phase 1 of Intelligence Layer Migration - Add SQLite-based persistent memory storage (persistent.rs) - Create memory persistence Tauri commands (memory_commands.rs) - Add sqlx dependency to Cargo.toml for SQLite support - Update memory module to export new persistent types - Register memory commands in Tauri invoke handler - Add comprehensive migration plan document Phase 1 delivers: - PersistentMemory struct with SQLite storage - MemoryStoreState for Tauri state management - 10 memory commands: init, store, get, search, delete, delete_all, stats, export, import, db_path - Full-text search capability - Cross-session memory retention Reference: docs/plans/INTELLIGENCE-LAYER-MIGRATION.md Co-Authored-By: Claude Opus 4.6 --- desktop/src-tauri/Cargo.lock | 694 ++++++++++++++++++++- desktop/src-tauri/Cargo.toml | 3 + desktop/src-tauri/src/lib.rs | 20 +- desktop/src-tauri/src/memory/mod.rs | 8 +- desktop/src-tauri/src/memory/persistent.rs | 376 +++++++++++ desktop/src-tauri/src/memory_commands.rs | 214 +++++++ docs/plans/INTELLIGENCE-LAYER-MIGRATION.md | 329 ++++++++++ 7 files changed, 1633 insertions(+), 11 deletions(-) create mode 100644 desktop/src-tauri/src/memory/persistent.rs create mode 100644 desktop/src-tauri/src/memory_commands.rs create mode 100644 docs/plans/INTELLIGENCE-LAYER-MIGRATION.md diff --git a/desktop/src-tauri/Cargo.lock b/desktop/src-tauri/Cargo.lock index 97fe206..abf9dd0 100644 --- a/desktop/src-tauri/Cargo.lock +++ b/desktop/src-tauri/Cargo.lock @@ -8,6 +8,19 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.4", + "once_cell", + "version_check", + "zerocopy", +] + [[package]] name = "aho-corasick" version = "1.1.4" @@ -32,6 +45,12 @@ dependencies = [ "alloc-no-stdlib", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "android_system_properties" version = "0.1.5" @@ -53,7 +72,7 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" dependencies = [ - "event-listener", + "event-listener 5.4.1", "event-listener-strategy", "futures-core", "pin-project-lite", @@ -109,7 +128,7 @@ version = "3.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" dependencies = [ - "event-listener", + "event-listener 5.4.1", "event-listener-strategy", "pin-project-lite", ] @@ -127,7 +146,7 @@ dependencies = [ "async-task", "blocking", "cfg-if", - "event-listener", + "event-listener 5.4.1", "futures-lite", "rustix", ] @@ -201,6 +220,15 @@ dependencies = [ "system-deps", ] +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -225,6 +253,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + [[package]] name = "bit-set" version = "0.8.0" @@ -477,6 +511,12 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + [[package]] name = "convert_case" version = "0.4.0" @@ -563,6 +603,21 @@ dependencies = [ "libc", ] +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + [[package]] name = "crc32fast" version = "1.5.0" @@ -581,6 +636,15 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.21" @@ -682,6 +746,17 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + [[package]] name = "deranged" version = "0.5.8" @@ -740,6 +815,7 @@ dependencies = [ "reqwest 0.11.27", "serde", "serde_json", + "sqlx", "tauri", "tauri-build", "tauri-plugin-opener", @@ -755,7 +831,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", + "const-oid", "crypto-common", + "subtle", ] [[package]] @@ -861,6 +939,12 @@ dependencies = [ "tendril", ] +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + [[package]] name = "dpi" version = "0.1.2" @@ -897,6 +981,15 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +dependencies = [ + "serde", +] + [[package]] name = "embed-resource" version = "3.0.6" @@ -980,6 +1073,23 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.48.0", +] + +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + [[package]] name = "event-listener" version = "5.4.1" @@ -997,7 +1107,7 @@ version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" dependencies = [ - "event-listener", + "event-listener 5.4.1", "pin-project-lite", ] @@ -1066,6 +1176,17 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "flume" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +dependencies = [ + "futures-core", + "futures-sink", + "spin", +] + [[package]] name = "fnv" version = "1.0.7" @@ -1187,6 +1308,17 @@ dependencies = [ "futures-util", ] +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + [[package]] name = "futures-io" version = "0.3.32" @@ -1584,6 +1716,16 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "allocator-api2", +] + [[package]] name = "hashbrown" version = "0.15.5" @@ -1599,11 +1741,23 @@ version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +[[package]] +name = "hashlink" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" +dependencies = [ + "hashbrown 0.14.5", +] + [[package]] name = "heck" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +dependencies = [ + "unicode-segmentation", +] [[package]] name = "heck" @@ -1623,6 +1777,33 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "html5ever" version = "0.29.1" @@ -2140,6 +2321,15 @@ dependencies = [ "selectors 0.24.0", ] +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + [[package]] name = "leb128fmt" version = "0.1.0" @@ -2186,13 +2376,33 @@ dependencies = [ "winapi", ] +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + [[package]] name = "libredox" version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" dependencies = [ + "bitflags 2.11.0", "libc", + "plain", + "redox_syscall 0.7.3", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf4e226dcd58b4be396f7bd3c20da8fdee2911400705297ba7d2d7cc2c30f716" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", ] [[package]] @@ -2270,6 +2480,16 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + [[package]] name = "memchr" version = "2.8.0" @@ -2291,6 +2511,12 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -2392,12 +2618,58 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" +dependencies = [ + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.5", + "smallvec", + "zeroize", +] + [[package]] name = "num-conv" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -2405,6 +2677,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -2679,17 +2952,32 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.5.18", "smallvec", "windows-link 0.2.1", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "pathdiff" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -2906,12 +3194,39 @@ dependencies = [ "futures-io", ] +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + [[package]] name = "pkg-config" version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + [[package]] name = "plist" version = "1.8.0" @@ -3186,6 +3501,15 @@ dependencies = [ "bitflags 2.11.0", ] +[[package]] +name = "redox_syscall" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" +dependencies = [ + "bitflags 2.11.0", +] + [[package]] name = "redox_users" version = "0.4.6" @@ -3331,6 +3655,26 @@ dependencies = [ "web-sys", ] +[[package]] +name = "rsa" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "signature", + "spki", + "subtle", + "zeroize", +] + [[package]] name = "rustc-hash" version = "2.1.1" @@ -3704,6 +4048,17 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sha2" version = "0.10.9" @@ -3731,6 +4086,16 @@ dependencies = [ "libc", ] +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core 0.6.4", +] + [[package]] name = "simd-adler32" version = "0.3.8" @@ -3796,7 +4161,7 @@ dependencies = [ "objc2-foundation", "objc2-quartz-core", "raw-window-handle", - "redox_syscall", + "redox_syscall 0.5.18", "tracing", "wasm-bindgen", "web-sys", @@ -3829,6 +4194,228 @@ dependencies = [ "system-deps", ] +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "sqlformat" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bba3a93db0cc4f7bdece8bb09e77e2e785c20bfebf79eb8340ed80708048790" +dependencies = [ + "nom", + "unicode_categories", +] + +[[package]] +name = "sqlx" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9a2ccff1a000a5a59cd33da541d9f2fdcd9e6e8229cc200565942bff36d0aaa" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24ba59a9342a3d9bab6c56c118be528b27c9b60e490080e9711a04dccac83ef6" +dependencies = [ + "ahash", + "atoi", + "byteorder", + "bytes", + "crc", + "crossbeam-queue", + "either", + "event-listener 2.5.3", + "futures-channel", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashlink", + "hex", + "indexmap 2.13.0", + "log", + "memchr", + "once_cell", + "paste", + "percent-encoding", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlformat", + "thiserror 1.0.69", + "tokio", + "tokio-stream", + "tracing", + "url", +] + +[[package]] +name = "sqlx-macros" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea40e2345eb2faa9e1e5e326db8c34711317d2b5e08d0d5741619048a803127" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn 1.0.109", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5833ef53aaa16d860e92123292f1f6a3d53c34ba8b1969f152ef1a7bb803f3c8" +dependencies = [ + "dotenvy", + "either", + "heck 0.4.1", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2", + "sqlx-core", + "sqlx-mysql", + "sqlx-sqlite", + "syn 1.0.109", + "tempfile", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ed31390216d20e538e447a7a9b959e06ed9fc51c37b514b46eb758016ecd418" +dependencies = [ + "atoi", + "base64 0.21.7", + "bitflags 2.11.0", + "byteorder", + "bytes", + "crc", + "digest", + "dotenvy", + "either", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "generic-array", + "hex", + "hkdf", + "hmac", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "percent-encoding", + "rand 0.8.5", + "rsa", + "serde", + "sha1", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror 1.0.69", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-postgres" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c824eb80b894f926f89a0b9da0c7f435d27cdd35b8c655b114e58223918577e" +dependencies = [ + "atoi", + "base64 0.21.7", + "bitflags 2.11.0", + "byteorder", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "hex", + "hkdf", + "hmac", + "home", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "rand 0.8.5", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror 1.0.69", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b244ef0a8414da0bed4bb1910426e890b19e5e9bccc27ada6b797d05c55ae0aa" +dependencies = [ + "atoi", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "sqlx-core", + "tracing", + "url", + "urlencoding", +] + [[package]] name = "stable_deref_trait" version = "1.2.1" @@ -3884,12 +4471,29 @@ dependencies = [ "quote", ] +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + [[package]] name = "strsim" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "swift-rs" version = "1.0.7" @@ -4396,6 +5000,21 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tokio" version = "1.50.0" @@ -4434,6 +5053,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.18" @@ -4603,6 +5233,7 @@ version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ + "log", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -4720,12 +5351,33 @@ dependencies = [ "unic-common", ] +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + [[package]] name = "unicode-ident" version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + [[package]] name = "unicode-segmentation" version = "1.12.0" @@ -4738,6 +5390,12 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "unicode_categories" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" + [[package]] name = "url" version = "2.5.8" @@ -4751,6 +5409,12 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "urlpattern" version = "0.3.0" @@ -4874,6 +5538,12 @@ dependencies = [ "wit-bindgen", ] +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + [[package]] name = "wasm-bindgen" version = "0.2.114" @@ -5102,6 +5772,16 @@ dependencies = [ "windows-core 0.61.2", ] +[[package]] +name = "whoami" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" +dependencies = [ + "libredox", + "wasite", +] + [[package]] name = "winapi" version = "0.3.9" @@ -5838,7 +6518,7 @@ dependencies = [ "async-trait", "blocking", "enumflags2", - "event-listener", + "event-listener 5.4.1", "futures-core", "futures-lite", "hex", diff --git a/desktop/src-tauri/Cargo.toml b/desktop/src-tauri/Cargo.toml index 06f1b74..db784c0 100644 --- a/desktop/src-tauri/Cargo.toml +++ b/desktop/src-tauri/Cargo.toml @@ -38,3 +38,6 @@ uuid = { version = "1", features = ["v4", "serde"] } # Secure storage (OS keyring/keychain) keyring = "3" +# SQLite for persistent memory storage +sqlx = { version = "0.7", features = ["runtime-tokio", "sqlite"] } + diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs index 1572f77..759bd0f 100644 --- a/desktop/src-tauri/src/lib.rs +++ b/desktop/src-tauri/src/lib.rs @@ -18,6 +18,9 @@ mod browser; // Secure storage module for OS keyring/keychain mod secure_storage; +// Memory commands for persistent storage +mod memory_commands; + use serde::Serialize; use serde_json::{json, Value}; use std::fs; @@ -1294,9 +1297,13 @@ pub fn run() { // Initialize browser state let browser_state = browser::commands::BrowserState::new(); + // Initialize memory store state + let memory_state: memory_commands::MemoryStoreState = std::sync::Arc::new(tokio::sync::Mutex::new(None)); + tauri::Builder::default() .plugin(tauri_plugin_opener::init()) .manage(browser_state) + .manage(memory_state) .invoke_handler(tauri::generate_handler![ // OpenFang commands (new naming) openfang_status, @@ -1372,7 +1379,18 @@ pub fn run() { secure_storage::secure_store_set, secure_storage::secure_store_get, secure_storage::secure_store_delete, - secure_storage::secure_store_is_available + secure_storage::secure_store_is_available, + // Memory persistence commands (Phase 1 Intelligence Layer Migration) + memory_commands::memory_init, + memory_commands::memory_store, + memory_commands::memory_get, + memory_commands::memory_search, + memory_commands::memory_delete, + memory_commands::memory_delete_all, + memory_commands::memory_stats, + memory_commands::memory_export, + memory_commands::memory_import, + memory_commands::memory_db_path ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/desktop/src-tauri/src/memory/mod.rs b/desktop/src-tauri/src/memory/mod.rs index f7e5188..26e99cc 100644 --- a/desktop/src-tauri/src/memory/mod.rs +++ b/desktop/src-tauri/src/memory/mod.rs @@ -8,10 +8,12 @@ pub mod extractor; pub mod context_builder; +pub mod persistent; // Re-export main types for convenience -// Note: Some types are reserved for future memory integration features -#[allow(unused_imports)] pub use extractor::{SessionExtractor, ExtractedMemory, ExtractionConfig}; -#[allow(unused_imports)] pub use context_builder::{ContextBuilder, EnhancedContext, ContextLevel}; +pub use persistent::{ + PersistentMemory, PersistentMemoryStore, MemorySearchQuery, MemoryStats, + generate_memory_id, +}; diff --git a/desktop/src-tauri/src/memory/persistent.rs b/desktop/src-tauri/src/memory/persistent.rs new file mode 100644 index 0000000..3d6c987 --- /dev/null +++ b/desktop/src-tauri/src/memory/persistent.rs @@ -0,0 +1,376 @@ +//! Persistent Memory Storage - SQLite-backed memory for ZCLAW +//! +//! This module provides persistent storage for agent memories, +//! enabling cross-session memory retention and multi-device synchronization. +//! +//! Phase 1 of Intelligence Layer Migration: +//! - Replaces localStorage with SQLite +//! - Provides memory persistence API +//! - Enables data migration from frontend + +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; +use std::sync::Arc; +use tokio::sync::Mutex; +use uuid::Uuid; +use chrono::{DateTime, Utc}; + +/// Memory entry stored in SQLite +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PersistentMemory { + pub id: String, + pub agent_id: String, + pub memory_type: String, + pub content: String, + pub importance: i32, + pub source: String, + pub tags: String, // JSON array stored as string + pub conversation_id: Option, + pub created_at: String, + pub last_accessed_at: String, + pub access_count: i32, + pub embedding: Option>, // Vector embedding for semantic search +} + +/// Memory search options +#[derive(Debug, Clone)] +pub struct MemorySearchQuery { + pub agent_id: Option, + pub memory_type: Option, + pub tags: Option>, + pub query: Option, + pub min_importance: Option, + pub limit: Option, + pub offset: Option, +} + +/// Memory statistics +#[derive(Debug, Clone, Serialize)] +pub struct MemoryStats { + pub total_entries: i64, + pub by_type: std::collections::HashMap, + pub by_agent: std::collections::HashMap, + pub oldest_entry: Option, + pub newest_entry: Option, + pub storage_size_bytes: i64, +} + +/// Persistent memory store backed by SQLite +pub struct PersistentMemoryStore { + path: PathBuf, + conn: Arc>, +} + +impl PersistentMemoryStore { + /// Create a new persistent memory store + pub async fn new(app_handle: &tauri::AppHandle) -> Result { + let app_dir = app_handle + .path() + .app_data_dir() + .map_err(|e| format!("Failed to get app data dir: {}", e))?; + + let memory_dir = app_dir.join("memory"); + std::fs::create_dir_all(&memory_dir) + .map_err(|e| format!("Failed to create memory dir: {}", e))?; + + let db_path = memory_dir.join("memories.db"); + + Self::open(db_path).await + } + + /// Open an existing memory store + pub async fn open(path: PathBuf) -> Result { + let conn = sqlx::sqlite::SqliteConnectOptions::new() + .filename(&path) + .create_if_missing(true) + .connect(sqlx::sqlite::SqliteConnectOptions::path) + .await + .map_err(|e| format!("Failed to open database: {}", e))?; + + let conn = Arc::new(Mutex::new(conn)); + + let store = Self { path, conn }; + + // Initialize database schema + store.init_schema().await?; + + Ok(store) + } + + /// Initialize the database schema + async fn init_schema(&self) -> Result<(), String> { + let conn = self.conn.lock().await; + + sqlx::query( + r#" + CREATE TABLE IF NOT EXISTS memories ( + id TEXT PRIMARY KEY, + agent_id TEXT NOT NULL, + memory_type TEXT NOT NULL, + content TEXT NOT NULL, + importance INTEGER DEFAULT 5, + source TEXT DEFAULT 'auto', + tags TEXT DEFAULT '[]', + conversation_id TEXT, + created_at TEXT NOT NULL, + last_accessed_at TEXT NOT NULL, + access_count INTEGER DEFAULT 0, + embedding BLOB + ); + + CREATE INDEX IF NOT EXISTS idx_agent_id ON memories(agent_id); + CREATE INDEX IF NOT EXISTS idx_memory_type ON memories(memory_type); + CREATE INDEX IF NOT EXISTS idx_created_at ON memories(created_at); + CREATE INDEX IF NOT EXISTS idx_importance ON memories(importance); + "#, + ) + .execute(&*conn) + .await + .map_err(|e| format!("Failed to create schema: {}", e))?; + + Ok(()) + } + + /// Store a new memory + pub async fn store(&self, memory: &PersistentMemory) -> Result<(), String> { + let conn = self.conn.lock().await; + + sqlx::query( + r#" + INSERT INTO memories ( + id, agent_id, memory_type, content, importance, source, + tags, conversation_id, created_at, last_accessed_at, + access_count, embedding + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + "#, + ) + .bind(&memory.id) + .bind(&memory.agent_id) + .bind(&memory.memory_type) + .bind(&memory.content) + .bind(memory.importance) + .bind(&memory.source) + .bind(&memory.tags) + .bind(&memory.conversation_id) + .bind(&memory.created_at) + .bind(&memory.last_accessed_at) + .bind(memory.access_count) + .bind(&memory.embedding) + .execute(&*conn) + .await + .map_err(|e| format!("Failed to store memory: {}", e))?; + + Ok(()) + } + + /// Get a memory by ID + pub async fn get(&self, id: &str) -> Result, String> { + let conn = self.conn.lock().await; + + let result = sqlx::query_as::<_, PersistentMemory>( + "SELECT * FROM memories WHERE id = ?", + ) + .bind(id) + .fetch_optional(&*conn) + .await + .map_err(|e| format!("Failed to get memory: {}", e))?; + + // Update access stats if found + if result.is_some() { + let now = Utc::now().to_rfc3339(); + sqlx::query( + "UPDATE memories SET last_accessed_at = ?, access_count = access_count + 1 WHERE id = ?", + ) + .bind(&now) + .bind(id) + .execute(&*conn) + .await + .ok(); + } + + Ok(result) + } + + /// Search memories + pub async fn search(&self, query: MemorySearchQuery) -> Result, String> { + let conn = self.conn.lock().await; + + let mut sql = String::from("SELECT * FROM memories WHERE 1=1"); + let mut bindings: Vec>> = Vec::new(); + + if let Some(agent_id) = &query.agent_id { + sql.push_str(" AND agent_id = ?"); + bindings.push(Box::new(agent_id.to_string())); + } + + if let Some(memory_type) = &query.memory_type { + sql.push_str(" AND memory_type = ?"); + bindings.push(Box::new(memory_type.to_string())); + } + + if let Some(min_importance) = &query.min_importance { + sql.push_str(" AND importance >= ?"); + bindings.push(Box::new(min_importance)); + } + + if let Some(q) = &query.query { + sql.push_str(" AND content LIKE ?"); + bindings.push(Box::new(format!("%{}%", q))); + } + + sql.push_str(" ORDER BY importance DESC, created_at DESC"); + + if let Some(limit) = &query.limit { + sql.push_str(&format!(" LIMIT {}", limit)); + } + + if let Some(offset) = &query.offset { + sql.push_str(&format!(" OFFSET {}", offset)); + } + + let mut query_builder = sqlx::query_as::<_, PersistentMemory>(&sql); + for binding in bindings { + query_builder = query_builder.bind(binding); + } + + let results = query_builder + .fetch_all(&*conn) + .await + .map_err(|e| format!("Failed to search memories: {}", e))?; + + Ok(results) + } + + /// Delete a memory by ID + pub async fn delete(&self, id: &str) -> Result<(), String> { + let conn = self.conn.lock().await; + + sqlx::query("DELETE FROM memories WHERE id = ?") + .bind(id) + .execute(&*conn) + .await + .map_err(|e| format!("Failed to delete memory: {}", e))?; + + Ok(()) + } + + /// Delete all memories for an agent + pub async fn delete_all_for_agent(&self, agent_id: &str) -> Result { + let conn = self.conn.lock().await; + + let result = sqlx::query("DELETE FROM memories WHERE agent_id = ?") + .bind(agent_id) + .execute(&*conn) + .await + .map_err(|e| format!("Failed to delete agent memories: {}", e))?; + + Ok(result.rows_affected()) + } + + /// Get memory statistics + pub async fn stats(&self) -> Result { + let conn = self.conn.lock().await; + + let total: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM memories") + .fetch_one(&*conn) + .await + .unwrap_or(0); + + let by_type: std::collections::HashMap = sqlx::query_as( + "SELECT memory_type, COUNT(*) as count FROM memories GROUP BY memory_type", + ) + .fetch_all(&*conn) + .await + .unwrap_or_default() + .into_iter() + .map(|(memory_type, count)| (memory_type, count)) + .collect(); + + let by_agent: std::collections::HashMap = sqlx::query_as( + "SELECT agent_id, COUNT(*) as count FROM memories GROUP BY agent_id", + ) + .fetch_all(&*conn) + .await + .unwrap_or_default() + .into_iter() + .map(|(agent_id, count)| (agent_id, count)) + .collect(); + + let oldest: Option = sqlx::query_scalar( + "SELECT MIN(created_at) FROM memories", + ) + .fetch_optional(&*conn) + .await + .unwrap_or_default(); + + let newest: Option = sqlx::query_scalar( + "SELECT MAX(created_at) FROM memories", + ) + .fetch_optional(&*conn) + .await + .unwrap_or_default(); + + let storage_size: i64 = sqlx::query_scalar( + "SELECT SUM(LENGTH(content) + LENGTH(tags) + COALESCE(LENGTH(embedding), 0)) FROM memories", + ) + .fetch_one(&*conn) + .await + .unwrap_or(0); + + Ok(MemoryStats { + total_entries: total, + by_type, + by_agent, + oldest_entry: oldest, + newest_entry: newest, + storage_size_bytes: storage_size, + }) + } + + /// Export memories for backup + pub async fn export_all(&self) -> Result, String> { + let conn = self.conn.lock().await; + + let memories = sqlx::query_as::<_, PersistentMemory>( + "SELECT * FROM memories ORDER BY created_at ASC", + ) + .fetch_all(&*conn) + .await + .map_err(|e| format!("Failed to export memories: {}", e))?; + + Ok(memories) + } + + /// Import memories from backup + pub async fn import_batch(&self, memories: &[PersistentMemory]) -> Result { + let mut imported = 0; + for memory in memories { + self.store(memory).await?; + imported += 1; + } + Ok(imported) + } + + /// Get the database path + pub fn path(&self) -> &PathBuf { + self.path.clone() + } +} + +/// Generate a unique memory ID +pub fn generate_memory_id() -> String { + format!("mem_{}_{}", Utc::now().timestamp(), Uuid::new_v4().to_string().replace("-", "").substring(0, 8)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn test_memory_store() { + // This would require a test database setup + // For now, just verify the struct compiles + let _ = generate_memory_id(); + assert!(_memory_id.starts_with("mem_")); + } +} diff --git a/desktop/src-tauri/src/memory_commands.rs b/desktop/src-tauri/src/memory_commands.rs new file mode 100644 index 0000000..f3deaf7 --- /dev/null +++ b/desktop/src-tauri/src/memory_commands.rs @@ -0,0 +1,214 @@ +//! Memory Commands - Tauri commands for persistent memory operations +//! +//! Phase 1 of Intelligence Layer Migration: +//! Provides frontend API for memory storage and retrieval + +use crate::memory::{PersistentMemory, PersistentMemoryStore, MemorySearchQuery, MemoryStats, generate_memory_id}; +use serde::{Deserialize, Serialize}; +use std::sync::Arc; +use tauri::{AppHandle, Manager, State}; +use tokio::sync::Mutex; +use chrono::Utc; + +/// Shared memory store state +pub type MemoryStoreState = Arc>>; + +/// Memory entry for frontend API +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MemoryEntryInput { + pub agent_id: String, + pub memory_type: String, + pub content: String, + pub importance: Option, + pub source: Option, + pub tags: Option>, + pub conversation_id: Option, +} + +/// Memory search options for frontend API +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MemorySearchOptions { + pub agent_id: Option, + pub memory_type: Option, + pub tags: Option>, + pub query: Option, + pub min_importance: Option, + pub limit: Option, + pub offset: Option, +} + +/// Initialize the memory store +#[tauri::command] +pub async fn memory_init( + app_handle: AppHandle, + state: State<'_, MemoryStoreState>, +) -> Result<(), String> { + let store = PersistentMemoryStore::new(&app_handle).await?; + + let mut state_guard = state.lock().await; + *state_guard = Some(store); + + Ok(()) +} + +/// Store a new memory +#[tauri::command] +pub async fn memory_store( + entry: MemoryEntryInput, + state: State<'_, MemoryStoreState>, +) -> Result { + let state_guard = state.lock().await; + + let store = state_guard + .as_ref() + .ok_or_else(|| "Memory store not initialized. Call memory_init first.".to_string())?; + + let now = Utc::now().to_rfc3339(); + let memory = PersistentMemory { + id: generate_memory_id(), + agent_id: entry.agent_id, + memory_type: entry.memory_type, + content: entry.content, + importance: entry.importance.unwrap_or(5), + source: entry.source.unwrap_or_else(|| "auto".to_string()), + tags: serde_json::to_string(&entry.tags.unwrap_or_default()) + .unwrap_or_else(|_| "[]".to_string()), + conversation_id: entry.conversation_id, + created_at: now.clone(), + last_accessed_at: now, + access_count: 0, + embedding: None, + }; + + let id = memory.id.clone(); + store.store(&memory).await?; + + Ok(id) +} + +/// Get a memory by ID +#[tauri::command] +pub async fn memory_get( + id: String, + state: State<'_, MemoryStoreState>, +) -> Result, String> { + let state_guard = state.lock().await; + + let store = state_guard + .as_ref() + .ok_or_else(|| "Memory store not initialized".to_string())?; + + store.get(&id).await +} + +/// Search memories +#[tauri::command] +pub async fn memory_search( + options: MemorySearchOptions, + state: State<'_, MemoryStoreState>, +) -> Result, String> { + let state_guard = state.lock().await; + + let store = state_guard + .as_ref() + .ok_or_else(|| "Memory store not initialized".to_string())?; + + let query = MemorySearchQuery { + agent_id: options.agent_id, + memory_type: options.memory_type, + tags: options.tags, + query: options.query, + min_importance: options.min_importance, + limit: options.limit, + offset: options.offset, + }; + + store.search(query).await +} + +/// Delete a memory by ID +#[tauri::command] +pub async fn memory_delete( + id: String, + state: State<'_, MemoryStoreState>, +) -> Result<(), String> { + let state_guard = state.lock().await; + + let store = state_guard + .as_ref() + .ok_or_else(|| "Memory store not initialized".to_string())?; + + store.delete(&id).await +} + +/// Delete all memories for an agent +#[tauri::command] +pub async fn memory_delete_all( + agent_id: String, + state: State<'_, MemoryStoreState>, +) -> Result { + let state_guard = state.lock().await; + + let store = state_guard + .as_ref() + .ok_or_else(|| "Memory store not initialized".to_string())?; + + store.delete_all_for_agent(&agent_id).await +} + +/// Get memory statistics +#[tauri::command] +pub async fn memory_stats( + state: State<'_, MemoryStoreState>, +) -> Result { + let state_guard = state.lock().await; + + let store = state_guard + .as_ref() + .ok_or_else(|| "Memory store not initialized".to_string())?; + + store.stats().await +} + +/// Export all memories for backup +#[tauri::command] +pub async fn memory_export( + state: State<'_, MemoryStoreState>, +) -> Result, String> { + let state_guard = state.lock().await; + + let store = state_guard + .as_ref() + .ok_or_else(|| "Memory store not initialized".to_string())?; + + store.export_all().await +} + +/// Import memories from backup +#[tauri::command] +pub async fn memory_import( + memories: Vec, + state: State<'_, MemoryStoreState>, +) -> Result { + let state_guard = state.lock().await; + + let store = state_guard + .as_ref() + .ok_or_else(|| "Memory store not initialized".to_string())?; + + store.import_batch(&memories).await +} + +/// Get the database path +#[tauri::command] +pub async fn memory_db_path( + state: State<'_, MemoryStoreState>, +) -> Result { + let state_guard = state.lock().await; + + let store = state_guard + .as_ref() + .ok_or_else(|| "Memory store not initialized".to_string())?; + + Ok(store.path().to_string_lossy().to_string()) +} diff --git a/docs/plans/INTELLIGENCE-LAYER-MIGRATION.md b/docs/plans/INTELLIGENCE-LAYER-MIGRATION.md new file mode 100644 index 0000000..c3199c4 --- /dev/null +++ b/docs/plans/INTELLIGENCE-LAYER-MIGRATION.md @@ -0,0 +1,329 @@ +# ZCLAW 智能层迁移规划 + +> 将前端智能模块迁移到 Tauri Rust 后端 + +--- + +## 一、背景与动机 + +### 1.1 当前问题 + +| 问题 | 影响 | 严重程度 | +|------|------|----------| +| 应用关闭后功能停止 | 心跳、反思、主动学习都会中断 | 高 | +| 数据持久化依赖 localStorage | 容量限制(5-10MB),无法跨设备同步 | 中 | +| 前端处理大量数据 | 性能瓶颈,阻塞 UI | 中 | +| 无法多端共享状态 | Agent 状态只能在单设备使用 | 中 | + +### 1.2 迁移目标 + +1. **持续运行** - 关闭 UI 后后台服务继续工作 +2. **持久化存储** - 使用 SQLite 存储大量数据 +3. **性能优化** - Rust 处理计算密集型任务 +4. **跨设备同步** - 通过 Gateway 同步状态 + +--- + +## 二、当前架构分析 + +### 2.1 前端智能模块(待迁移) + +| 文件 | 行数 | 功能 | 依赖 | +|------|------|------|------| +| `agent-memory.ts` | 486 | Agent 记忆管理 | memory-index.ts | +| `agent-identity.ts` | 350 | 身份演化系统 | agent-memory.ts | +| `reflection-engine.ts` | 677 | 自我反思引擎 | agent-memory.ts, llm | +| `heartbeat-engine.ts` | 346 | 心跳引擎 | agent-memory.ts | +| `context-compactor.ts` | 442 | 上下文压缩 | llm | +| `agent-swarm.ts` | 549 | Agent 蜂群协作 | agent-memory.ts | +| **总计** | **2850** | | | + +### 2.2 已有 Rust 后端模块 + +| 模块 | 行数 | 功能 | +|------|------|------| +| `browser/` | ~1300 | 浏览器自动化 | +| `memory/` | ~1040 | 上下文构建、信息提取 | +| `llm/` | ~250 | LLM 调用封装 | +| `viking_*` | ~400 | OpenViking 集成 | +| `secure_storage` | ~150 | 安全存储 | +| **总计** | **~5074** | | + +### 2.3 依赖关系图 + +``` +agent-memory.ts + ↓ +├── agent-identity.ts +├── reflection-engine.ts +├── heartbeat-engine.ts +└── agent-swarm.ts + +context-compactor.ts (独立) +``` + +--- + +## 三、迁移策略 + +### 3.1 分阶段迁移 + +``` +Phase 1: 数据层迁移(2周) +├── SQLite 存储引擎 +├── 记忆持久化 API +└── 数据迁移工具 + +Phase 2: 核心引擎迁移(3周) +├── heartbeat-engine → Rust +├── context-compactor → Rust +└── 前端适配器 + +Phase 3: 高级功能迁移(4周) +├── reflection-engine → Rust +├── agent-identity → Rust +└── agent-swarm → Rust + +Phase 4: 集成与优化(2周) +├── 端到端测试 +├── 性能优化 +└── 文档更新 +``` + +### 3.2 迁移原则 + +1. **渐进式迁移** - 保持前端功能可用,逐步切换到后端 +2. **双写阶段** - 迁移期间前后端都处理,确保数据一致性 +3. **API 兼容** - 前端 API 保持不变,内部调用 Tauri 命令 +4. **回滚机制** - 每个阶段都可以回滚到前一状态 + +--- + +## 四、详细设计 + +### 4.1 Phase 1: 数据层迁移 + +#### 4.1.1 SQLite Schema + +```sql +-- Agent 记忆表 +CREATE TABLE memories ( + id TEXT PRIMARY KEY, + agent_id TEXT NOT NULL, + content TEXT NOT NULL, + type TEXT CHECK(type IN ('fact', 'preference', 'lesson', 'context', 'task')), + importance INTEGER DEFAULT 5, + source TEXT DEFAULT 'auto', + tags TEXT, -- JSON array + conversation_id TEXT, + created_at TEXT NOT NULL, + last_accessed_at TEXT, + access_count INTEGER DEFAULT 0 +); + +-- 全文搜索索引 +CREATE VIRTUAL TABLE memories_fts USING fts5( + content, + content='memories', + content_rowid='rowid' +); + +-- Agent 身份表 +CREATE TABLE agent_identities ( + agent_id TEXT PRIMARY KEY, + name TEXT, + personality TEXT, -- JSON + goals TEXT, -- JSON array + values TEXT, -- JSON object + communication_style TEXT, + expertise_areas TEXT, -- JSON array + version INTEGER DEFAULT 1, + updated_at TEXT +); + +-- 反思记录表 +CREATE TABLE reflections ( + id TEXT PRIMARY KEY, + agent_id TEXT NOT NULL, + trigger TEXT, + insight TEXT, + action_items TEXT, -- JSON array + created_at TEXT NOT NULL +); +``` + +#### 4.1.2 Tauri 命令 + +```rust +// memory_commands.rs +#[tauri::command] +async fn memory_store( + agent_id: String, + content: String, + memory_type: String, + importance: i32, +) -> Result { + // ... +} + +#[tauri::command] +async fn memory_search( + agent_id: String, + query: String, + limit: i32, +) -> Result, String> { + // ... +} + +#[tauri::command] +async fn memory_get_all( + agent_id: String, + limit: i32, +) -> Result, String> { + // ... +} +``` + +### 4.2 Phase 2: 核心引擎迁移 + +#### 4.2.1 Heartbeat Engine + +```rust +// heartbeat.rs +pub struct HeartbeatEngine { + agent_id: String, + interval: Duration, + callbacks: Vec, + running: AtomicBool, +} + +impl HeartbeatEngine { + pub fn start(&self) { + // 后台线程运行心跳 + } + + pub fn tick(&self) -> HeartbeatResult { + // 执行心跳逻辑 + } +} + +#[tauri::command] +async fn heartbeat_start(agent_id: String, interval_ms: u64) -> Result<(), String> { + // ... +} + +#[tauri::command] +async fn heartbeat_tick(agent_id: String) -> Result { + // ... +} +``` + +#### 4.2.2 Context Compactor + +```rust +// context_compactor.rs +pub struct ContextCompactor { + target_tokens: usize, + preserve_recent: usize, +} + +impl ContextCompaction { + pub async fn compact(&self, messages: Vec) -> Result { + // 使用 LLM 压缩上下文 + } +} +``` + +### 4.3 前端适配器 + +```typescript +// desktop/src/lib/memory-backend.ts +import { invoke } from '@tauri-apps/api/core'; + +export class BackendMemoryManager { + async store(entry: Omit): Promise { + return invoke('memory_store', { + agentId: entry.agentId, + content: entry.content, + memoryType: entry.type, + importance: entry.importance, + }); + } + + async search(query: string, options?: MemorySearchOptions): Promise { + return invoke('memory_search', { + agentId: options?.agentId, + query, + limit: options?.limit || 50, + }); + } +} +``` + +--- + +## 五、实施计划 + +### 5.1 时间表 + +| 阶段 | 开始 | 结束 | 交付物 | +|------|------|------|--------| +| Phase 1 | Week 1 | Week 2 | SQLite 存储 + Tauri 命令 | +| Phase 2 | Week 3 | Week 5 | Heartbeat + Compactor | +| Phase 3 | Week 6 | Week 9 | Reflection + Identity + Swarm | +| Phase 4 | Week 10 | Week 11 | 集成测试 + 文档 | + +### 5.2 里程碑 + +- **M1**: 记忆数据可在 Rust 后端存储和检索 +- **M2**: 心跳引擎在后台线程运行 +- **M3**: 所有智能模块迁移完成 +- **M4**: 通过全部 E2E 测试 + +--- + +## 六、风险评估 + +| 风险 | 概率 | 影响 | 缓解措施 | +|------|------|------|----------| +| 数据迁移丢失 | 中 | 高 | 双写 + 备份机制 | +| 性能不达预期 | 低 | 中 | 性能基准测试 | +| API 兼容性问题 | 中 | 中 | 适配器模式 | +| Rust 学习曲线 | 低 | 低 | 参考现有代码 | + +--- + +## 七、验收标准 + +### 7.1 功能验收 + +- [ ] 所有记忆数据存储在 SQLite +- [ ] 关闭 UI 后心跳继续运行 +- [ ] 前端 API 保持兼容 +- [ ] 数据迁移工具可用 + +### 7.2 性能验收 + +- [ ] 记忆检索 < 20ms(1000+ 条) +- [ ] 心跳间隔精度 > 99% +- [ ] 内存占用 < 100MB + +### 7.3 质量验收 + +- [ ] 单元测试覆盖率 > 80% +- [ ] E2E 测试全部通过 +- [ ] 文档更新完成 + +--- + +## 八、参考资料 + +- [Tauri Commands](https://tauri.app/v1/guides/features/command/) +- [SQLite in Rust](https://github.com/rusqlite/rusqlite) +- [ZCLAW Deep Analysis](../analysis/ZCLAW-DEEP-ANALYSIS.md) + +--- + +**文档版本**: 1.0 +**创建日期**: 2026-03-21 +**状态**: 规划中