mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
feat(memory): per-entry glob patterns for extraPaths (#121209)
* feat(memory): support globbed extra paths Allow root-relative glob patterns on existing memory.search.extraPaths entries across discovery, watching, reindex identity, and QMD migration. * fix(memory): drop stale lease type import * fix(memory): enforce extra path glob scope
This commit is contained in:
committed by
GitHub
parent
db61480056
commit
8dbd30ba74
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"core": 2284,
|
||||
"core": 2288,
|
||||
"channel": 3718,
|
||||
"plugin": 4053
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
f34db5e8582e38f1a81098b29eec7852301a1d496a45a889115d597dc4ffee50 config-baseline.json
|
||||
8cbea1bb4c3c0bd933da5641c08a3086e4579d8bcc496468cc951d7e388978c9 config-baseline.core.json
|
||||
d7e15b44c0f74635ce38421e72f48f42db439d8c87f29696640ee03c594e73c5 config-baseline.json
|
||||
25b006d7596d8f607c1ad8d973a60a1a5da4b33fc6c2f56a2b3f02b479d9c503 config-baseline.core.json
|
||||
9b08f3be75acaf4459d4bdb383b90bd04c408f09db3fdea211518f8d4ae14b84 config-baseline.channel.json
|
||||
7ce54344ec431d11f8a1ac92a112a9352fc55a4e43be71a08b91f9a06f924fb6 config-baseline.plugin.json
|
||||
|
||||
@@ -1,151 +1,151 @@
|
||||
a06111c1cdb17e9ea1f2bbf2f90b86b8e93150c091b964b6fc5f5e67f5a4b632 module/account-core
|
||||
d20eb6a6a77f72566266b88d191b30155757a1cc3d3886d3b6c437176bbac771 module/account-helpers
|
||||
a592581a61518734a8a410ec9f71069529ee5fbb54848daf650199eb1cb557ca module/account-core
|
||||
2b55fba911cc5c81076b706e8c817ab487e57c5a8da53d53afa39756a48e4dbc module/account-helpers
|
||||
71522995185b956a0cc4927a472cc8d1153e5e998874bfd9a750513175174713 module/account-id
|
||||
006e581db74e461dcf4d7fd299b09d285f32c91ac12a56de1d21307d272e5a0a module/account-resolution
|
||||
3fe118210b885af40088457ed81ffa5ede18c8e695295731a2ee059af46843cc module/agent-config-primitives
|
||||
4f8b033a23a7bd17edf27911485fe3a4bd204440aa6320d11fa735e8749e55dc module/agent-harness
|
||||
1e320b19a8ffc35faef38cbe863d815d7bb15a222cae4a8008ff1a9d39af2b52 module/agent-harness-runtime
|
||||
595ef30046b8ba4bfd427e5a33b65ca28ab1a699ce7e90d6afa2ba2b187e22c0 module/agent-media-payload
|
||||
c2e2abd35f38b2c34a4c642e965c94071b894c08d6b7ec1ea4b8f96e3d51f269 module/agent-runtime
|
||||
72fa7e17dd1694a24f373dece9d3035c3fc25e1e113954328e16df03d9be7b55 module/agent-scope-runtime
|
||||
64fc61ddb3ed2367ad193c8174e5ddf4f563d7456acd44ab3529219a8d0776fb module/account-resolution
|
||||
4fbb1c87e99399f842a20d75d5e35a4b7064a1b7f02115c23f9a2a7cdcfb57ee module/agent-config-primitives
|
||||
1fbd58fb125a1ab69bacc7a700cac67a8a1d6c0d3b229212561c8cd1c8d72381 module/agent-harness
|
||||
789148fb788284586e15d65e8c5115bbfe7df91d201aff952dd63b70c37dda8f module/agent-harness-runtime
|
||||
762a2c3df44621e9464fdd3ef437be2a878bfa9a7e8ca7670f88ae549e601614 module/agent-media-payload
|
||||
8913627dfb1dded389ccfba6609af2b1f45b9e434098b29500988c618ce3fc9c module/agent-runtime
|
||||
66cad8b985017a2487e47d6794dede2d16348f717e7f445cd5df4af2eb7f95f4 module/agent-scope-runtime
|
||||
8fecb210e22bce4532b6ab649b09465f0bd2c857a44abf40db7d683d6491e6da module/allow-from
|
||||
ac6c71c3b7c3d1f6d3219d7514a1d1e0c44198358e6e9a4eae06e318d52a8433 module/allowlist-config-edit
|
||||
136ddb70973082ec051eee5a5c2a78da5359f6d0e2dc058be5f7049ad5d2608e module/approval-auth-runtime
|
||||
cdf0f3cab2f9e19bf9a7b9125872b4e9986bcd0530d17a3bdc76ec00aa9d8388 module/approval-client-runtime
|
||||
235abceca3e9392ab39e5e63231337ae9dc35bbb773fb79e0e5a0b56be403455 module/approval-delivery-runtime
|
||||
b784be7a3514d186ff62c8d1688fc38d6c0dbad00ecca438e5429d95c0b3b6c9 module/approval-gateway-runtime
|
||||
9750dd4c52ac4b14142be0d39205da33faf584fdb48bb4d2942972e608147ccd module/approval-handler-adapter-runtime
|
||||
fa1d9baff79016a97bdab8f44d6fd6f534fd69d2bf7c3de34a744b278a97a654 module/approval-handler-runtime
|
||||
a0904844a521a3391cbb183e8657b69f7550b6245f88a7bcd7b5c12b2e3fae24 module/approval-native-runtime
|
||||
343b855a38a460a8c3b65b005698ed5c311e9f6b93e7d42a142c5cddb3a08a6f module/approval-reply-runtime
|
||||
9657575dca903644b9281b14c6522ecf90e95878368ade8a8ad19850314eaef4 module/approval-runtime
|
||||
f0cd21c1854e209363fc2cbc684ba9dee9668ba7b5d0b57a96d4c8da825d7337 module/allowlist-config-edit
|
||||
6a76c827ecd4c7f66a9b93533849572d5211da2090a4121758f5a26450a8d77c module/approval-auth-runtime
|
||||
9765ea0869a5b0b84b97617e82c93f2bf7a1a33df03cfe38e661657c746069c8 module/approval-client-runtime
|
||||
6b6611999dacdb86603d98b741526b337eed58e7d4a479b71aa849339f71845f module/approval-delivery-runtime
|
||||
afba69ce95b3a939511c1f1e6b27a5999e8793304cba7578b0785c7edc342580 module/approval-gateway-runtime
|
||||
a9f10f7c70287d7dc8ad224ca6424234652e74908ae7de2fc7edf4c98c91de4a module/approval-handler-adapter-runtime
|
||||
f8b5c31c956b567827d58e9a1f6493846448b3c0ae30904cc7f4d872b8071d26 module/approval-handler-runtime
|
||||
cb52e36a0de53874de5115ec797d39e576533cd3dda576c9b6c57af4f2888dca module/approval-native-runtime
|
||||
b102781a2d78bbfafcf4205aeb2c169ad69bc4010edaf376647036cf3fba48d7 module/approval-reply-runtime
|
||||
c93d529d020ac1ee52c230e0883f9880656908e8404ef96251bcf7c93afd39f4 module/approval-runtime
|
||||
01ca912836b8dec672f705e294f72d346e778557e4c591317d67558ea7669c0b module/archive
|
||||
d7e53de63b0ac11a266e4abdc18ba6e9401b80309f5c8f5f6a72a00f65dfe3bd module/boolean-param
|
||||
1b87d321f91ac51be8010cfd45163e1b1f13e4f68de60539fa8518f54258b4b5 module/channel-actions
|
||||
665c32fdc411e106469713fce1862d4b38287723dae1527b29bb50703f93a3a0 module/channel-config-helpers
|
||||
076689cd8e62c842580d61a34d97bcf42c90406287d428fc5d0d3eb373ec274d module/channel-actions
|
||||
d1341161e2f5d44eaf7eca3a777ab3f106c7db1db32778bf98c75c7fa7d57ca4 module/channel-config-helpers
|
||||
c2cc71d5070b6071c51248b0648d1ad1a9468d3737df890adc77ec02025e8853 module/channel-config-primitives
|
||||
76ad615d374431580ea1755594e2ce3ce047ac1de9fd4621053dca0fbc3afc4d module/channel-config-schema
|
||||
63d4f5bc22d6e7779fcfa1f73e70df689189fcc876af26e05a4e3334b95675a9 module/channel-contract
|
||||
9ca88ca940d0f07b307093c76d32a1c4c2093538628f6cd9ab65abd62e4dc08f module/channel-core
|
||||
f6ddf9086bc224b4b1a516cd7e056467a529d19435930b1c8ddff7052d890ffa module/channel-dm-policy
|
||||
116ad3ac327d8185cd5097cc79c31d94a01c5528a03297d1fd9f07f8e5862937 module/channel-entry-contract
|
||||
b14ed3e3235fab5725a5eac86bb7910174a21ae1d7f2b4433891867396dcc566 module/channel-feedback
|
||||
cdc5b1312806beed04fedbbd16bb34509cc5250b424ff3c18963428efade9f04 module/channel-inbound
|
||||
f58349b93db16be763c7ba4c00cbc13b1b64911c8eefa5a1bfba1e95809e81cf module/channel-inbound-debounce
|
||||
cb7f865c9953b5f0925c918b2bed29fb6629fbd643f4f69bd2bf345970864cbf module/channel-ingress-runtime
|
||||
af775123f5526907732b7bebd6e2258ffe4ec733b2435007361bf7aba5b4eaf4 module/channel-lifecycle
|
||||
9fa7e5f9b3515f0bbc252210b034d39c25de9eb431def30dddce3198c309efac module/channel-config-schema
|
||||
a6af4971aa36345f1a32d97981bd36cc29794f62f30bd57859df2816c65f6fde module/channel-contract
|
||||
02a07336e2ccb44ffe617f75dd95590b46e4b7780860c9f73f6c3ee0d376cb3b module/channel-core
|
||||
40f8e726de245e17cf7bf908bb7d6fb084f4927f5ccc20b0c089f47af1bd7a0e module/channel-dm-policy
|
||||
58918972c9b991058b1b82c2e36f37e15b7d01d1a9bd8dec29e4080b9ab2b532 module/channel-entry-contract
|
||||
56b185eb99eb4981056befd38cd9511b655923934acb1c22aeec7d9c504bdf96 module/channel-feedback
|
||||
deffb7f24c1d0dc4494e31e5f2041b91d5f052b90e7ff9083c6c8f9003bced79 module/channel-inbound
|
||||
780abefd1600c55da219da43c769c873d071ab616b5e02d22f365677ed5a7be4 module/channel-inbound-debounce
|
||||
b72485da4fe5d7f73352f4474d00e30b749b64c8a2066755e104958cd96036c2 module/channel-ingress-runtime
|
||||
f83762680c0d0aa6fa1bad9e53c70bfdf3694126dbdd9491db764a69ed3f6d67 module/channel-lifecycle
|
||||
0e47457e38d1df0bd572e1408cde2ca6a788b65205f43c585316b5ad3a8f2f16 module/channel-logging
|
||||
1b151db6f444da029d8115dd06450467146d0a587676502673f799db74ca9776 module/channel-message
|
||||
cbb1dd1f2765cad226b57b18e8bcc293e801375975afb0d5e7d5cecb55138c77 module/channel-outbound
|
||||
0b64eeb4eaca3c295bae24479f5afd68f7714384bcc721640b36ee07dd4af3ba module/channel-pairing
|
||||
d025a8eb7542dbfca088c163b8216b2030ccca326b8a688e883cde8813a2db78 module/channel-plugin-common
|
||||
113d135501f2777f308c4c3a59dc29f722f8c385ddfe5628994f14ed37246442 module/channel-policy
|
||||
c3bdfac92ace16eccca1bd44c95a3e73e39d7f996f1714cdfdf102647305df55 module/channel-reply-pipeline
|
||||
67f3db77df2c786a6dc3429b4f94dbad89e152591dd3727ce3ad33bbb0b9c5e0 module/channel-message
|
||||
401b0346239cadfb4db1c48e07f4e76f8f793839a926e828c47566667c645655 module/channel-outbound
|
||||
1108ed0e70c34994add0fcb5f6b0681568ef81a714b2bf7441edf9a5775283fc module/channel-pairing
|
||||
a1157e965dfa77ce935f73bc54bf393fd6bfb1495d95b5154c43c5e13f22d4a6 module/channel-plugin-common
|
||||
c2c7900e47c0595bbc8a166aa60cfbc5a0e02f9a4c0985d5fd630c7dec4c3dca module/channel-policy
|
||||
fde35c74c5db0f89b235210a1eca1b4e0712181c456f24793958fa73ee0ce042 module/channel-reply-pipeline
|
||||
482370e60135db9bfaf07f24bab549e5fde09ab265a6061a1f587c5d93929e91 module/channel-runtime-context
|
||||
121aaced4cfdbd2dd16462e685699d021107d5d1bc4f9324363173c086b72605 module/channel-secret-basic-runtime
|
||||
965cc7241ab4f6398611052f873f06b47b996fd72c948ea973c2cac3eaf4a820 module/channel-secret-runtime
|
||||
9d25865ba44e3160b8716b08ed0ad93ab95e2c1bdde28047f65dd03fe66efd83 module/channel-send-result
|
||||
b89224a72be296c82eeb1933b179ec0479627e80e1c61717b6cc7a778dbfa5b2 module/channel-setup
|
||||
329fee4cb0ca430ea9249e4090e889c459402a40d42acd9c0f2a20053167502b module/channel-status
|
||||
95dc206f832a0f563238dcea72d41554f409a3a9df081f41c52a8241c7dc5161 module/channel-streaming
|
||||
32c2e9ea4f954a6b04d6e61b392e63d42b5cc74ccbdc1404c94a4c97463f0ca1 module/channel-secret-basic-runtime
|
||||
941673d941b3ab9bd553ba9153305af7933019254a504a603907a237d06b5e79 module/channel-secret-runtime
|
||||
4647a0e849e3aa193af751c71d1b137947887a489b999043618bfaebb1a37002 module/channel-send-result
|
||||
4493cf54a54d7159e27d33884b1cd231cd54a038a85a3d5d3676e149954444a7 module/channel-setup
|
||||
66df387345c0d64083904f4271228bb1e5660e40b00f42afc5d802d3ff07627f module/channel-status
|
||||
198ed5042f86da7731dad61bc66f44bcb249fd9e369d79cf4509df172d5cab7f module/channel-streaming
|
||||
67df67da5ae72e9eaeb19d41b6bd2432ec4fd8b7b63b2b616fb98f3b4e0ec41d module/channel-streaming-config
|
||||
fdeffe356c7c4edeec9f8fd03edcadc375eabc7a9412e582b10c3180e3ef40fc module/cli-argv
|
||||
ad12670dbfe538f8d0ebf4fb2b68080e93a760278278e6b1ce9bb129d4b2d533 module/collection-runtime
|
||||
0c1e2add15c5cb0e1d735467950d3bea327d668638d517bfab7faf5bd6db1f3f module/command-auth
|
||||
5bf07a178a69219c3822f4ee899680d71477b13c260b522a174d41191a7cf29a module/command-auth-native
|
||||
4db2a3623b116976a2e4cb366163194a8073c8d56a9c74105127bee59c7d07f4 module/command-detection
|
||||
0f6cf0b06cd65f2bfded9d1054873166fa6ad22c71f4f1cc5afb7a49711cc365 module/command-primitives-runtime
|
||||
03eb97e7b47a79ad4b40b37a2bac418c14f85e90969f70fd6fc5c80bb27b8c49 module/command-status
|
||||
b56349e8d8034152be55161d4bb4c9bf69d0f68f7ea07431f28a4ee7a707d576 module/config-contracts
|
||||
9d2c42377ef981ea6732f96bd3a9c1a16fa2852936516a3fca5adc2b6b0b736e module/config-mutation
|
||||
6c70300fd4bf84808f742687eaad92339fc942abd5cfa2378807a0756decdbd1 module/config-runtime
|
||||
d5a157da395c2b548347b7fae054af0a90afbcae71c7171d0be5c66d92271328 module/conversation-runtime
|
||||
e096d5aa2ed29b0a1d705f81aa69a23d481b2d1bbdc5e20fce18e747ae7d55d7 module/core
|
||||
2704dd2371a9374a385d65dd8b4f3a3c27450b212be5b22d0ac71ceb7b1ab31d module/dedupe-runtime
|
||||
8134198b58398da63ddc6e04040d1150d9f462bf8ae56f944acbc84db2a5362e module/command-auth
|
||||
38214365f960d43d71625f58d5fa5cc7a28d4b01fa0f9c8a621bacd9c3bac41e module/command-auth-native
|
||||
396f60be14d8fe1144076cffeafc2e2ddc0379b5e664f9fd45d13a0edc4a9f2f module/command-detection
|
||||
5155909167a810ca258ec9baf4814e7d495cf25bd287fbb22be55eb1a8d88dfe module/command-primitives-runtime
|
||||
0ab02df5c3904386bfb062e7dce0a24e2ed835b54dcc0112e367346624cb0efe module/command-status
|
||||
02011f70a5de5f1861609206038b101d3e9bbf39bf45a3cfaaae56596299225b module/config-contracts
|
||||
7ee5bc35d30165be33ab229a96dd6a6e3e04c620fa4c41753d568798c514feb3 module/config-mutation
|
||||
08beaa7b5b8797bf248068c73332847b196c5ce47a74c67e788b3281aea46a97 module/config-runtime
|
||||
069ca892c9d9c0d82f9a0d1e72970da2b1bc5d3c4fc15e127b18bbf78292cd2a module/conversation-runtime
|
||||
82e34c7a2af309677ce2f54477ad026369b2880b96684e38989e1896dd12cf83 module/core
|
||||
eceacf43fa56f443298bb5f489ae78677cb9f13df4d4abad7634b5d5537a0e01 module/dedupe-runtime
|
||||
ebef0e650ab45e44c9335e2b3e15588c968cea6dadd125364a076f9c50ad1e8c module/device-bootstrap
|
||||
4fe9beab67598950c144d86add5fd3b1ce475aaf37644d2f9798703052d98210 module/diagnostic-runtime
|
||||
734898717c8669f1c3a35dca93db121508368edac7dafcd1ccce71f432680979 module/directory-runtime
|
||||
93c2f1688a65651c844d5317c0cf0bace3bf2e0477047947b269fa1e6a3ef160 module/discord
|
||||
2b01f2e5a52713158665372b358fb6059903f68b6efbedff700741d9feca7696 module/error-runtime
|
||||
03eeafd10471b94a2651e0d42380a32f5ffd4f33c47e4b0161205093163e9407 module/extension-shared
|
||||
9ecd22bc3acae3f9b0556b305c4232abe54a7993b910caa789cba6cf917dd1e2 module/diagnostic-runtime
|
||||
6d90412b97efcc675d16acc3aeb2752520a613ce4bb25b87e47f84fd6b36736d module/directory-runtime
|
||||
841d0b1fcb267198e4422be8b3ebdc6625bf24ec5ab513606ff72ac2353993ab module/discord
|
||||
f2d69888d0c799e12d97b5f92393906aa843be25f61cb6a6c55d32f1db800a44 module/error-runtime
|
||||
1caf8cc5552ca5e392599457e3f9616e033de199d9f47c12320cdee617a6b0e9 module/extension-shared
|
||||
dd9f6e0fd33cc88b22543c1ee30cc09cf4de4d8f30dff7b7f9cebef885c21543 module/gateway-method-runtime
|
||||
b5dfcf2977a334afb5a138ba9af196902ae8baf54a843c57496ed826e410178b module/gateway-runtime
|
||||
721c77e48f30f92bd41f9eddab0a573e1791a01f3f98d6f1ef1e8de4fc09916d module/gateway-runtime
|
||||
575656e5e0195c8d1813a4e2e3a271e800bb97d44f2dd78c242e3b6714ffa097 module/group-access
|
||||
f3595668fc4c20b2b34df67d3fa4d5432c16fa3be5fec9e63d8f14512ab6732f module/health
|
||||
60d126e420e212415f25b6e90c2aca7513a1275e14b2bad27b06aeb6ab12eaa5 module/hook-runtime
|
||||
da9d83537008db2c9339f3a4235ed2b7ee3ac986cacdc8bf8fdfaf1a025bd6a9 module/inbound-envelope
|
||||
77726dc396a269ca62fe958c4c797de54d94aba47b7b166e0b66052b604c4b93 module/health
|
||||
74252b820ea1d21457f01a4e3b868233737e83b134f3d8b8cecfb2392a9647e7 module/hook-runtime
|
||||
4d9f7d3c3e59113b493f3cac812b3c117968035af10a751a92f2aca8e2ec8806 module/inbound-envelope
|
||||
4928af5d2509f696b896f53ac790303a0742202dbcdae3e44fe6d1b434a9c1ba module/inbound-event-delivery
|
||||
b15ed4974df49e68b0b250429a16eaa2356b1b2b5053d6cd1e09eb27b3bb58fd module/inbound-reply-dispatch
|
||||
539921d6c5596381beb17505394c67be44edb4a5ac533d555f07c43c4c991e8f module/infra-runtime
|
||||
a551c9ef5540138c0e0b40c341860150688e35505d5a06f5047a0b5be5e80375 module/inbound-reply-dispatch
|
||||
7f75e1166081708656b561095753ab05480265a55bc40a8e5ca34e221a4798e2 module/infra-runtime
|
||||
ce73721421f1b903dd04ead4df173582e59ea3e9990248102c448b419cc6d272 module/ingress-effect-once
|
||||
31449afd7ea7f0c8dc7dee439a3693c3d1092af67c6bed67dd34e7b7b50bc74e module/interactive-runtime
|
||||
2fdc37835ccc4651c580847250adf8e42c165483eb239907aea981a6552e24b4 module/interactive-runtime
|
||||
408d257ab5cc4b88a22b7e7595039cb8fc524b261c44141b294fbd0100ba62ee module/json-store
|
||||
e907fd3a98185f2c261f2aafcaa5a19ee1d7b459d519a498397d629f84c68312 module/lazy-runtime
|
||||
09735d3ed2b372352a4e32aa8e80d7b8ccf7f78768d2fdb76d00bbe9aa659f7f module/logging-core
|
||||
e5acf130491ef188b193a3e3dc5284e53a29f7e08278caa188f5ddb96e860dc4 module/logging-core
|
||||
f1ca4ced4305d0769c2d8cc1291137ac7002fe0e6eaec2c1a71edad2204c8311 module/matrix
|
||||
c0a6a1e65d02a5cc8727881a69d8f4ec692a33dc78fe13320d8372c33b4b75ed module/media-local-roots
|
||||
8cef237e683b323062fa4134a88a0b5b8ff97f1c5d356654c8062fdbf5eed87c module/media-local-roots
|
||||
f74d7295fe716aa140aa0bc9300d6259d71dab826de0808fca6bb02592bf5d6e module/media-mime
|
||||
819c3b13113fe8cce4d9792f1b649d3ee48e9a9ab2f44936f553c294fc4b8405 module/media-runtime
|
||||
d8e039f3ebb4c4bf7e30217105e2aae84934f73df9e05be27db4f746ab45f1a9 module/media-runtime
|
||||
6a52f93107335f88751704352cc01e62add06f854a5b7d765e2a5ee87c0313b6 module/media-store
|
||||
b3fdb9f96d2724d5824063f434f3e482b605e4c06cad9770a94c78079dc36454 module/media-understanding
|
||||
544a6d47a391e64574b146f649e8065b70a57955088b85cbdbdacb872889475a module/media-understanding-runtime
|
||||
e84b586895c8c25f1d2f3a3141859f5bcc250addf38e88bb5031dff35233e23d module/meeting-runtime
|
||||
a6aac1a3f85d3ee7dc9fbc7a1c6ef7dd00a9c2d3bdb6dc8d57a9704a821f806d module/memory-core-host-engine-foundation
|
||||
16884f67221837372dbda6ff0487fa514c81609b0ea9ae96480ffc139dc80067 module/memory-host-core
|
||||
6e5b09a983a69041873ece5bc3b719c0dc913e5a69e2afdf6c7958889e711097 module/media-understanding
|
||||
7f74e947cca0cf894869bede29815863d5eacca016d04d6262e9d41b3999ae94 module/media-understanding-runtime
|
||||
cf4f49e9e1c45913acd6a9332f8bb35a8b7de102edcc6eeebacf3fa666101552 module/meeting-runtime
|
||||
e5d2b540090c17602a1c36b42559f7b516e20730e06bbdd8606597c66ef2ef95 module/memory-core-host-engine-foundation
|
||||
451082d20bde79f9e50570a3e38dd4f7f880337055840774ca1df02381cad98b module/memory-host-core
|
||||
1efa0aadc4261d1c6073058cbf3dcc9fa681424819bdd14333e19b249bbc4b18 module/messaging-targets
|
||||
8647dda41c0c3ab67191c397aa405de914fd8f277f55f550a307524e3c20d98d module/model-session-runtime
|
||||
488be4321795c9135a2d47ce89a2b71b8e3c957cf288ea1ef0dd7a9a0687b631 module/models-provider-runtime
|
||||
504c61546d566814cda2d8126b458efc9ad70b75f1406ab9d6635ac10225200c module/native-command-config-runtime
|
||||
d808e6681668e70b2dbb2ac590b455072f98d54970593e25ef6ad5c2322d3a51 module/native-command-registry
|
||||
ca6ee4fa75f976d590210b9ff6dc66374bb829a05d4f70972eef9137f5548b88 module/param-readers
|
||||
3b35256be44a7f2f69a1c2ebb33b1028d6bdc8c714a426f3b466a61056c5c5d7 module/model-session-runtime
|
||||
0b554d2ebaddc7d4cddc3fb31585371b036381170ce1c5640741c7858c86f960 module/models-provider-runtime
|
||||
b518b4caa7f138d448fe08f5fc9e5f67da5d4b26aafaee2370feb2300e1baa21 module/native-command-config-runtime
|
||||
1e8b5e7bd8234919d339e750368cbe6425e7463d7e63eb5200d728cc17d6e959 module/native-command-registry
|
||||
15d1d490e4f7909d2d563b83988258a9cc5fa1344cffb0cd3dcd7592c0467632 module/param-readers
|
||||
ca7a56bb1a6169b4cf9befbf5aa21da280a8086fdc49fca4eec520a7a7c98549 module/persistent-dedupe
|
||||
3c73ab232d86b49ebb6e5f302da2a2a4b5c7f62bff5e4cc0d3505edc5ba4f5cd module/plugin-config-runtime
|
||||
3c0c121d55b828d3c41d7814b95daeec19fc000e8a6c76195091549b59c4db66 module/plugin-entry
|
||||
11058d6921227019f45f89986cef65611816500153eb3a85e81cb9c3e9c978aa module/plugin-runtime
|
||||
d22f0b4bd95f098d1759c22347b31580d7ce045aefd490a4cbea47d8f6320465 module/provider-auth
|
||||
526efe506796df2330a7f927f2c77e04e98eecd4725f2353013f5ce358b3414f module/provider-catalog-runtime
|
||||
f8b250da3eab2f85b2bf358872f910b29e541ea04358498a6deab353fa0520fc module/plugin-config-runtime
|
||||
405b226c64bcdcd6c4d630cf4f223b00028cd9cddb111e82b24d406a3fc7e202 module/plugin-entry
|
||||
ee2c2ccde86934c67a0d39ec2203a30fddd16b3a3ef7aec338b45694f1f7a375 module/plugin-runtime
|
||||
9a84a66ddde2759e863b6ecf34e97cb99825e0911f36a0af14fc7e80869f03be module/provider-auth
|
||||
474c4a92d7b4050f023bb5586617cc978c2c31578d5723e64c03ea127ac70e0d module/provider-catalog-runtime
|
||||
8131147d699394bd06503e2ea2f5f1a50b1594a87dded6d118b74a8d0328c8f6 module/proxy-capture
|
||||
8f01e5ed6bccbb1e13cda4dd0a0b18f39af08ad36d7f0080b12217565f2e576c module/question-gateway-runtime
|
||||
a479cd5c96a34c6f0a2ed4d4239aad63b56cb0e47a970848a141ad7fb67b9e11 module/reply-chunking
|
||||
f2219c0b81efdc590828bc79c6005767d10817de779a0ecc20b3e89fd7c0335a module/reply-dispatch-runtime
|
||||
913c32adcd94ea9c8b7efac0f7c65231500c49129e638c0682eb632da3d5a0c8 module/question-gateway-runtime
|
||||
f319c52a0f1655de752efa55a983893e56e4afa0727f6cc4896c3238c984e91c module/reply-chunking
|
||||
1fde835f8398ce0f627c794fedfa735db750685909dbba1552b005834b59f9e7 module/reply-dispatch-runtime
|
||||
73f861fa3179d5af1159853c5acab0eec7a6c8f9398dcb75ea770e784fca6727 module/reply-history
|
||||
d3bf7e4a7fcaebc1cf3173f4fbb60203e6ec46257cddcca7096800df946708fc module/reply-payload
|
||||
72e31359f00c5a3d0fc86ddd57705bd37884e383d1b66d8e033d03d76fe00e39 module/reply-runtime
|
||||
ecc6e3190892ff416ce69d26c8ce0aa08a468c8689ca3606c18b0271e2d87597 module/reply-payload
|
||||
33479b0cc629940332a7724ee380912be19614894b70a369d1d85ceda47fa6b6 module/reply-runtime
|
||||
aa07d85d99fdd2b1e0cbe9975fb6dcae66b8bdce2607c6bd5402ae68bb15118c module/root-walk
|
||||
02c0e5cae6772159a1c5ff43209de542ac7976dc30541d406f9dc6c37141a69d module/routing
|
||||
292405cda407e2569f731212f6f981b12bfe682836d5f6e58c6ae194ba27f144 module/routing
|
||||
7877a7e58fa32a64107154e5b714c6d165e96989d4aa5f43e0afac085a187af0 module/run-command
|
||||
bd15eb9689fd7070dc942cb4bd0db6c2c1d4fb1a971ce9d0a6349a5679309535 module/runtime
|
||||
ccb6aad96b4ea156738f45bac04b6eda5826e17b36eb8b5e26567f400b3be217 module/runtime-config-snapshot
|
||||
2e3c692a9f911ab227e34a9a3870f8e139f90ceaff631301b55ce712dec53736 module/runtime-env
|
||||
14c9f7ce9f8b8a47799f6a870110d1d8469223a01a2dba01fc8bc1f11a35cfe0 module/runtime
|
||||
f7900c5ff5827e1b53a926367d807bd4c81b313ab74fd0409d571ae881226a60 module/runtime-config-snapshot
|
||||
8e33ccbbe610c6c3ad04a140f7511a16cf406733c8459216b1fb4b1e7f0208d9 module/runtime-env
|
||||
7e871b7319745678bb83fcfc1b54c8751b0ab1af92ff06c01d0659ac92863c11 module/runtime-group-policy
|
||||
274fbf53ae00c89455a5f6a812d5bc241b661fa4688957ba4940cdb0876f849d module/runtime-store
|
||||
0d38198bd13bd5b0ed2f04ccefd805a6dbe9213b3b1a1c75bde1ce42c87b08c7 module/runtime-store
|
||||
d17862c40825af1ddf0257b44f1e1cbb9c375e8e5ed668fae75d530d1a465cf9 module/secret-file
|
||||
8e2ac4d3973d8d8ce4478e3440d66ee5c0d9213b0fe9e927c421d14fd31e5e86 module/secret-input
|
||||
1806dba733bb6d88b4e997d3dc989761f0d7dde7638d96e1218705fd27f0921b module/secret-input-runtime
|
||||
8e0e6d67db89eeee760a33ac984e7141bde171080365dddf534858b6abb3d56d module/secret-ref-runtime
|
||||
c810981c42d32923e84c42d20137c9dc393aabc0ea079786e01d19bd59fa5277 module/security-runtime
|
||||
ae1cfe5ad81718ad64381d6bcebb9746f94291d3340d002e789d262dd234d663 module/session-catalog
|
||||
2c72b851264af36b6720c796a14da1f5b3653b3cc4e69a4d94a5fc9ccab48280 module/session-discussion
|
||||
fd2dcb08a59df7bb68c8f36f272d134f6904d12b9e192bc8d7a4858d3039b595 module/session-store-runtime
|
||||
73f4a776c027d974f010250360822694486a8033bf1b51e96a0ff2440b05dbfe module/setup
|
||||
f1ec91331ad72c3fd9edb2cb11b4c6a35acd55ef3e4f541c5907149eed21b15f module/setup-runtime
|
||||
5958846179128ea6b29135fb90ce4e40b8df7de63d1c65d1696e52294cbc0f52 module/secret-input-runtime
|
||||
72ca4634e0038ad6dc80c4ce12eb34014e1fa990c7eabe19bb4501b2722ee685 module/secret-ref-runtime
|
||||
6db2af355b0d1d46f493a5d34a63a6ea5231dafbd8b896d976a52da681868f67 module/security-runtime
|
||||
f700943c355142a66fe0a5033ff1c04c60acdc963529662f60ca5d1370a1e557 module/session-catalog
|
||||
48b55e67e967e5ff8aa5b258b8dff308729945b2d024ddec7a5f920a81a4382a module/session-discussion
|
||||
cd31b3430b196d78604de2682802f95453ce8e32c2222ae5f94241e422a2e1e6 module/session-store-runtime
|
||||
e54f5c86a55683028a8c4a7c821cf9b9f11b48f91ae9b08e79f8e94b91eb5fde module/setup
|
||||
41c4790efda179c600fcc8cc4f2cd9c0505efd86df15a1a840aeb99c839bc627 module/setup-runtime
|
||||
44d37e0d9131ad2859f41068f2604090c784e65f1bd6ebda8e051b6f2e5e1660 module/setup-tools
|
||||
00e8794c6e7aabbdeec14189d885d4021f5118fcb08f7e34b85fafc473e62bed module/skill-commands-runtime
|
||||
583b4d4357b38d5db45cbb0b7799da27e81cc66c2b3ef59d6a19ad7d4a278ef7 module/speech-settings
|
||||
f01b661de86de0d0b1d3bff395330092f3fe1114f4516968ca5fca79cf7eac50 module/ssrf-policy
|
||||
0296f2c837f8116aa3a7e8b02d3de8b756d44e0a004842dbd24a5aa627f74a54 module/ssrf-runtime
|
||||
eace34246d7a827b00a67bde258f8401bab35f527dcad29e6f982e4ae39cf013 module/state-paths
|
||||
8576354be3aa9ea9d4429c3ef048753a4b02c3d1771538a9c563d2504998f1ae module/status-helpers
|
||||
dc901275501e473a69a8e9435e1c95096c47f1bb569cf3240c1e82c769fffaa0 module/skill-commands-runtime
|
||||
68a81d3ec617556543ecc74189a4b01ca4d5431461e82029c0fa6e1d07b32648 module/speech-settings
|
||||
1e4df0f48b50a8eb546459af1e947ba143c287e3d9c13706c45b67d411c46ad5 module/ssrf-policy
|
||||
e4940099376e18e34c234d4d90d836cbe234c3e891e40b7b4879168b386b1121 module/ssrf-runtime
|
||||
3c3c812d2d0c997ec81d117346347b802a3445e8e0d76aa6d0e3259f3cf55f98 module/state-paths
|
||||
0f912449caf8203fb876fd308e209f13aa50447b2fbd47f826e42766342159e0 module/status-helpers
|
||||
f097d0096b21c8a052f0f649b7512ecf2aba4744ae6956f001950e053828b309 module/string-coerce-runtime
|
||||
c6ea76a9fa7f56771cbfc54617aa9ceec3d87241bab38a094622ea9351f3cc87 module/telegram-account
|
||||
5b0e9ebde0a5a65f728434aca1bf09ad0579fbd02921fd6f7c0811665eda9b9e module/telegram-account
|
||||
aef35bee2502cd6ed8765409b758e452aff8ac9469fd773e6a2a44c9a1bc3f66 module/temp-path
|
||||
87fa81b9e58d8fc04a4b4202d2d37fca339615f5225687d9db905151439e0f4d module/text-chunking
|
||||
2ce8b180da90b5b1665bc65dd9cae9af7eb4e0988cc2a64685e8e6f9cb9a87c3 module/text-runtime
|
||||
a82a2087f07ad3f8ba933d9af65af568e54e9f3595a3e0606aa3a8fc9575763a module/tool-plugin
|
||||
50d2fa56b2ad57edd12d74201d18ab43045b5c9da0ab98cc158cebe9bd8b768b module/text-runtime
|
||||
9bdbe8da529a24ee4413a50bfe92b239684e2499236a2e37c0b9961f52fb96ee module/tool-plugin
|
||||
dc1a073c59ab61e2789533b777b3f0cb9af689d64a97796b10e8aa82552510db module/tool-results
|
||||
3c97f778d2844ba1bfd3e77fbccd3bfed94bc102053c091b00a0d3d2aaff6a99 module/tool-send
|
||||
5d4f1dbde1238799e5244b9c0b799aa2fb7d607d0c072d9a70659978825ef4ee module/tool-send
|
||||
cda105b721d498df23a554c6b68be150b8fe66b8b9172185c31a0b3b0646b1dc module/web-media
|
||||
284c7c41168c8361dd9b5ddae517bbfc5c32245595d9b7e11e4acbfbdccc07c8 module/webhook-ingress
|
||||
216e54c25ec0985fc483899d4de3d2582053a4fc3a2d1c98a8b5e7cdd2f136e1 module/webhook-request-guards
|
||||
18f0f10817717fba6d3a4c660c62e58d83c3b75a15be848b24da30ee31399075 module/webhook-ingress
|
||||
ddd05cb74246c393fef4d3fd8331eddd4db72a000fe45e9e515357b757636854 module/webhook-request-guards
|
||||
de59e86e126b75d13251cba7ebbe27b44d9b5588785d98df5ff4d6722374c81f module/widget-html
|
||||
9161b36ec0ab062ea41b363c894fcd672a7727f21cb726739f99f9c184fce69d module/zod
|
||||
|
||||
@@ -12,9 +12,8 @@ The optional QMD memory backend has been removed. Builtin memory is now the only
|
||||
Run `openclaw doctor --fix` to remove retired `memory.backend`, `memory.qmd.*`, and
|
||||
`memory.search.qmd.*` settings, including agent-scoped variants. Your Markdown memory sources are
|
||||
indexed by the builtin engine on its next sync. Doctor preserves configured QMD paths and extra
|
||||
collections in the corresponding `memory.search.extraPaths` setting. Custom QMD glob patterns are
|
||||
removed; builtin memory indexes its supported files under each preserved path and reports that
|
||||
change during migration. QMD indexes, exported session Markdown, downloaded models, and collection
|
||||
metadata are derived state and do not require migration.
|
||||
collections in the corresponding `memory.search.extraPaths` setting, including root-relative glob
|
||||
patterns. QMD indexes, exported session Markdown, downloaded models, and collection metadata are
|
||||
derived state and do not require migration.
|
||||
|
||||
See [Memory](/concepts/memory) for the current architecture and configuration.
|
||||
|
||||
@@ -384,21 +384,23 @@ auto-injected.
|
||||
|
||||
## Additional memory paths
|
||||
|
||||
| Key | Type | Description |
|
||||
| ------------ | ---------- | ---------------------------------------- |
|
||||
| `extraPaths` | `string[]` | Additional directories or files to index |
|
||||
| Key | Type | Description |
|
||||
| ------------ | ----------------------------------------------------- | ---------------------------------------- |
|
||||
| `extraPaths` | `Array<string \| { path: string; pattern?: string }>` | Additional directories or files to index |
|
||||
|
||||
```json5
|
||||
{
|
||||
memory: {
|
||||
search: {
|
||||
extraPaths: ["../team-docs", "/srv/shared-notes"],
|
||||
extraPaths: ["../team-docs", { path: "/srv/shared-notes", pattern: "runbooks/**/*.md" }],
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Paths can be absolute or workspace-relative. Directories are scanned recursively for `.md` files. The builtin engine skips symlinks.
|
||||
Paths can be absolute or workspace-relative. Directories are scanned recursively for supported
|
||||
files. Object entries narrow a directory with a root-relative glob using `/` separators; direct
|
||||
file entries are indexed exactly. The builtin engine skips symlinks.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -3,6 +3,10 @@ import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { listAgentIds } from "openclaw/plugin-sdk/agent-runtime";
|
||||
import { isUsageCountedSessionTranscriptFileName } from "openclaw/plugin-sdk/memory-core-host-engine-sessions";
|
||||
import {
|
||||
normalizeExtraMemoryPathEntries,
|
||||
type MemoryExtraPath,
|
||||
} from "openclaw/plugin-sdk/memory-core-host-engine-storage";
|
||||
import { buildAgentSessionKey } from "openclaw/plugin-sdk/routing";
|
||||
import {
|
||||
defaultRuntime,
|
||||
@@ -150,8 +154,11 @@ function resolveAgentIds(cfg: OpenClawConfig, agent?: string): string[] {
|
||||
}
|
||||
return listAgentIds(cfg);
|
||||
}
|
||||
export function formatExtraPaths(workspaceDir: string, extraPaths: string[]): string[] {
|
||||
return normalizeExtraMemoryPaths(workspaceDir, extraPaths).map((entry) => shortenHomePath(entry));
|
||||
export function formatExtraPaths(workspaceDir: string, extraPaths: MemoryExtraPath[]): string[] {
|
||||
return normalizeExtraMemoryPathEntries(workspaceDir, extraPaths).map((entry) => {
|
||||
const root = shortenHomePath(entry.path);
|
||||
return entry.pattern ? `${root} (pattern: ${entry.pattern})` : root;
|
||||
});
|
||||
}
|
||||
async function withMemoryManagerForAgent(params: {
|
||||
cfg: OpenClawConfig;
|
||||
@@ -258,7 +265,7 @@ async function scanSessionFiles(agentId: string): Promise<SourceScan> {
|
||||
}
|
||||
async function scanMemoryFiles(
|
||||
workspaceDir: string,
|
||||
extraPaths: string[] = [],
|
||||
extraPaths: MemoryExtraPath[] = [],
|
||||
): Promise<SourceScan> {
|
||||
const issues: string[] = [];
|
||||
const memoryFile = path.join(workspaceDir, "MEMORY.md");
|
||||
@@ -308,7 +315,7 @@ async function scanMemoryFiles(
|
||||
let listed: string[] = [];
|
||||
let listedOk = false;
|
||||
try {
|
||||
listed = await listMemoryFiles(workspaceDir, resolvedExtraPaths);
|
||||
listed = await listMemoryFiles(workspaceDir, extraPaths);
|
||||
listedOk = true;
|
||||
} catch (err) {
|
||||
const code = (err as NodeJS.ErrnoException).code;
|
||||
@@ -340,7 +347,7 @@ export async function scanMemorySources(params: {
|
||||
workspaceDir: string;
|
||||
agentId: string;
|
||||
sources: MemorySourceName[];
|
||||
extraPaths?: string[];
|
||||
extraPaths?: MemoryExtraPath[];
|
||||
}): Promise<MemorySourceScan> {
|
||||
const scans: SourceScan[] = [];
|
||||
const extraPaths = params.extraPaths ?? [];
|
||||
|
||||
@@ -505,6 +505,23 @@ describe("memory cli", () => {
|
||||
expect(close).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("prints extra path glob patterns in status output", async () => {
|
||||
const close = vi.fn(async () => {});
|
||||
mockManager({
|
||||
status: () =>
|
||||
makeMemoryStatus({
|
||||
extraPaths: [{ path: "notes", pattern: "runbooks/**/*.md" }],
|
||||
}),
|
||||
close,
|
||||
});
|
||||
|
||||
const log = spyRuntimeLogs(defaultRuntime);
|
||||
await runMemoryCli(["status"]);
|
||||
|
||||
expectLogged(log, "Extra paths: /tmp/openclaw/notes (pattern: runbooks/**/*.md)");
|
||||
expect(close).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("still aborts status when its own memory SecretRef cannot be resolved", async () => {
|
||||
getRuntimeConfig.mockReturnValue({
|
||||
memory: {
|
||||
|
||||
@@ -255,6 +255,54 @@ describe("memory reindex state", () => {
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("includes extra path patterns in stable scope identity", () => {
|
||||
const workspaceDir = "/tmp/workspace";
|
||||
const multimodal = {
|
||||
enabled: false,
|
||||
modalities: [],
|
||||
maxFileBytes: 20 * 1024 * 1024,
|
||||
};
|
||||
const firstScopeHash = resolveConfiguredScopeHash({
|
||||
workspaceDir,
|
||||
extraPaths: [
|
||||
{ path: "notes", pattern: "runbooks/**/*.md" },
|
||||
{ path: "notes", pattern: "decisions/**/*.md" },
|
||||
],
|
||||
multimodal,
|
||||
});
|
||||
const reorderedScopeHash = resolveConfiguredScopeHash({
|
||||
workspaceDir,
|
||||
extraPaths: [
|
||||
{ path: "notes", pattern: "decisions/**/*.md" },
|
||||
{ path: "notes", pattern: "runbooks/**/*.md" },
|
||||
],
|
||||
multimodal,
|
||||
});
|
||||
const changedScopeHash = resolveConfiguredScopeHash({
|
||||
workspaceDir,
|
||||
extraPaths: [{ path: "notes", pattern: "archive/**/*.md" }],
|
||||
multimodal,
|
||||
});
|
||||
|
||||
expect(reorderedScopeHash).toBe(firstScopeHash);
|
||||
expect(changedScopeHash).not.toBe(firstScopeHash);
|
||||
expect(resolveConfiguredScopeHash({ workspaceDir, extraPaths: ["notes"], multimodal })).toBe(
|
||||
resolveConfiguredScopeHash({
|
||||
workspaceDir,
|
||||
extraPaths: [{ path: "notes" }],
|
||||
multimodal,
|
||||
}),
|
||||
);
|
||||
expect(
|
||||
isMemoryIndexIdentityDirty(
|
||||
createIdentityParams({
|
||||
meta: createMeta({ scopeHash: firstScopeHash }),
|
||||
configuredScopeHash: changedScopeHash,
|
||||
}),
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("marks identity dirty when configured sources add sessions", () => {
|
||||
expect(
|
||||
isMemoryIndexIdentityDirty(
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
import {
|
||||
hashText,
|
||||
MEMORY_CHUNKING_VERSION,
|
||||
normalizeExtraMemoryPaths,
|
||||
normalizeExtraMemoryPathEntries,
|
||||
type MemoryExtraPath,
|
||||
type MemorySource,
|
||||
} from "openclaw/plugin-sdk/memory-core-host-engine-storage";
|
||||
|
||||
@@ -107,16 +108,19 @@ function configuredMetaSourcesDiffer(params: {
|
||||
|
||||
export function resolveConfiguredScopeHash(params: {
|
||||
workspaceDir: string;
|
||||
extraPaths?: string[];
|
||||
extraPaths?: MemoryExtraPath[];
|
||||
multimodal: {
|
||||
enabled: boolean;
|
||||
modalities: string[];
|
||||
maxFileBytes: number;
|
||||
};
|
||||
}): string {
|
||||
const extraPaths = normalizeExtraMemoryPaths(params.workspaceDir, params.extraPaths)
|
||||
.map((value) => value.replace(/\\/g, "/"))
|
||||
.toSorted();
|
||||
const extraPaths = normalizeExtraMemoryPathEntries(params.workspaceDir, params.extraPaths)
|
||||
.map((entry) => {
|
||||
const path = entry.path.replaceAll("\\", "/");
|
||||
return entry.pattern ? { path, pattern: entry.pattern } : path;
|
||||
})
|
||||
.toSorted((left, right) => JSON.stringify(left).localeCompare(JSON.stringify(right)));
|
||||
return hashText(
|
||||
JSON.stringify({
|
||||
extraPaths,
|
||||
|
||||
@@ -7,7 +7,10 @@ import {
|
||||
createSubsystemLogger,
|
||||
type ResolvedMemorySearchConfig,
|
||||
} from "openclaw/plugin-sdk/memory-core-host-engine-foundation";
|
||||
import { normalizeExtraMemoryPaths } from "openclaw/plugin-sdk/memory-core-host-engine-storage";
|
||||
import {
|
||||
matchesExtraMemoryPathEntry,
|
||||
normalizeExtraMemoryPathEntries,
|
||||
} from "openclaw/plugin-sdk/memory-core-host-engine-storage";
|
||||
import { resolveTimerTimeoutMs } from "openclaw/plugin-sdk/number-runtime";
|
||||
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import { MemoryManagerSyncBase } from "./manager-sync-base.js";
|
||||
@@ -99,6 +102,11 @@ function shouldIgnoreMemoryWatchPath(
|
||||
return classifyMemoryMultimodalPath(normalized, multimodalSettings) === null;
|
||||
}
|
||||
|
||||
function isWithinMemoryWatchRoot(root: string, candidate: string): boolean {
|
||||
const relative = path.relative(root, candidate);
|
||||
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
||||
}
|
||||
|
||||
function runDetachedMemorySync(sync: () => Promise<void>, reason: "interval" | "watch") {
|
||||
void sync().catch((err: unknown) => {
|
||||
log.warn(`memory sync failed (${reason}): ${String(err)}`);
|
||||
@@ -123,30 +131,48 @@ export abstract class MemoryManagerWatchOps extends MemoryManagerSyncBase {
|
||||
path.join(this.workspaceDir, "MEMORY.md"),
|
||||
path.join(this.workspaceDir, "USER.md"),
|
||||
]);
|
||||
const dirWatchPaths = new Set<string>([path.join(this.workspaceDir, "memory")]);
|
||||
const additionalPaths = normalizeExtraMemoryPaths(this.workspaceDir, this.settings.extraPaths);
|
||||
const memoryDir = path.join(this.workspaceDir, "memory");
|
||||
const dirWatchPaths = new Set<string>([memoryDir]);
|
||||
const additionalPaths = normalizeExtraMemoryPathEntries(
|
||||
this.workspaceDir,
|
||||
this.settings.extraPaths,
|
||||
);
|
||||
for (const entry of additionalPaths) {
|
||||
try {
|
||||
const stat = fsSync.lstatSync(entry);
|
||||
const stat = fsSync.lstatSync(entry.path);
|
||||
if (stat.isSymbolicLink()) {
|
||||
continue;
|
||||
}
|
||||
if (stat.isDirectory()) {
|
||||
dirWatchPaths.add(entry);
|
||||
dirWatchPaths.add(entry.path);
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
stat.isFile() &&
|
||||
(normalizeLowercaseStringOrEmpty(entry).endsWith(".md") ||
|
||||
classifyMemoryMultimodalPath(entry, this.settings.multimodal) !== null)
|
||||
(normalizeLowercaseStringOrEmpty(entry.path).endsWith(".md") ||
|
||||
classifyMemoryMultimodalPath(entry.path, this.settings.multimodal) !== null)
|
||||
) {
|
||||
fileWatchPaths.add(entry);
|
||||
fileWatchPaths.add(entry.path);
|
||||
}
|
||||
} catch {
|
||||
// Skip missing/unreadable additional paths.
|
||||
}
|
||||
}
|
||||
const markDirty = (watchPath?: string, stats?: MemoryWatchEventStats) => {
|
||||
if (watchPath && stats && !stats.isDirectory?.()) {
|
||||
const normalizedWatchPath = path.resolve(watchPath);
|
||||
const matchingEntries = isWithinMemoryWatchRoot(memoryDir, normalizedWatchPath)
|
||||
? []
|
||||
: additionalPaths.filter((entry) =>
|
||||
isWithinMemoryWatchRoot(entry.path, normalizedWatchPath),
|
||||
);
|
||||
if (
|
||||
matchingEntries.length > 0 &&
|
||||
!matchingEntries.some((entry) => matchesExtraMemoryPathEntry(entry, normalizedWatchPath))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
recordMemoryWatchEventPath(this.pendingWatchPaths, watchPath, stats);
|
||||
this.dirty = true;
|
||||
this.scheduleWatchSync();
|
||||
|
||||
@@ -319,6 +319,40 @@ describe("memory watcher config", () => {
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("filters patterned extra path file events while watching the directory root", async () => {
|
||||
await setupWatcherWorkspace({ name: "seed.md", contents: "seed" });
|
||||
await fs.mkdir(path.join(extraDir, "notes"), { recursive: true });
|
||||
await fs.mkdir(path.join(extraDir, "drafts"), { recursive: true });
|
||||
await fs.writeFile(path.join(extraDir, "notes", "keep.md"), "keep");
|
||||
await fs.writeFile(path.join(extraDir, "drafts", "skip.md"), "skip");
|
||||
const cfg = createWatcherConfig({
|
||||
extraPaths: [{ path: extraDir, pattern: "notes/**/*.md" }],
|
||||
});
|
||||
|
||||
await expectWatcherManager(cfg);
|
||||
const extraWatcher = createdNativeWatchers.find(
|
||||
(watcher) => watcher.dir === extraDir && watcher.recursive,
|
||||
);
|
||||
expect(extraWatcher).toBeDefined();
|
||||
vi.useFakeTimers();
|
||||
const syncSpy = vi
|
||||
.spyOn(
|
||||
manager as unknown as {
|
||||
sync: (params?: { reason?: string }) => Promise<void>;
|
||||
},
|
||||
"sync",
|
||||
)
|
||||
.mockResolvedValue(undefined);
|
||||
|
||||
extraWatcher?.emit("change", path.join("drafts", "skip.md"));
|
||||
await vi.advanceTimersByTimeAsync(BUILT_IN_WATCH_DEBOUNCE_MS);
|
||||
expect(syncSpy).not.toHaveBeenCalled();
|
||||
|
||||
extraWatcher?.emit("change", path.join("notes", "keep.md"));
|
||||
await vi.advanceTimersByTimeAsync(BUILT_IN_WATCH_DEBOUNCE_MS);
|
||||
expect(syncSpy).toHaveBeenCalledWith({ reason: "watch" });
|
||||
});
|
||||
|
||||
it("does not start watchers for one-shot CLI managers", async () => {
|
||||
await setupWatcherWorkspace({ name: "notes.md", contents: "hello" });
|
||||
const cfg = createWatcherConfig();
|
||||
|
||||
@@ -10,7 +10,9 @@ export {
|
||||
hashText,
|
||||
INVALID_PROJECT_ANNOTATION_KEY,
|
||||
listMemoryFiles,
|
||||
matchesExtraMemoryPathEntry,
|
||||
MEMORY_CHUNKING_VERSION,
|
||||
normalizeExtraMemoryPathEntries,
|
||||
normalizeProjectAnnotationKey,
|
||||
normalizeExtraMemoryPaths,
|
||||
parseEmbedding,
|
||||
@@ -22,6 +24,7 @@ export {
|
||||
type CuratedProjectAnnotations,
|
||||
type MemoryChunk,
|
||||
type MemoryFileEntry,
|
||||
type NormalizedExtraMemoryPath,
|
||||
} from "./host/internal.js";
|
||||
export { readMemoryFile } from "./host/read-file.js";
|
||||
export { isTransientMemoryReadError, retryTransientMemoryRead } from "./host/read-retry.js";
|
||||
@@ -38,6 +41,7 @@ export type { ResolvedMemoryBackendConfig } from "./host/backend-config.js";
|
||||
export type {
|
||||
MemoryEmbeddingProbeResult,
|
||||
MemoryEntryProvenance,
|
||||
MemoryExtraPath,
|
||||
MemoryOriginClass,
|
||||
MemoryProviderStatus,
|
||||
MemorySearchManager,
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { resolveRememberAcrossConversations } from "./config-utils.js";
|
||||
import {
|
||||
normalizeConfiguredMemoryExtraPaths,
|
||||
resolveRememberAcrossConversations,
|
||||
} from "./config-utils.js";
|
||||
|
||||
describe("resolveRememberAcrossConversations", () => {
|
||||
it("honors keyed per-agent memory overrides", () => {
|
||||
@@ -15,3 +18,21 @@ describe("resolveRememberAcrossConversations", () => {
|
||||
expect(resolveRememberAcrossConversations(config, "support")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("normalizeConfiguredMemoryExtraPaths", () => {
|
||||
it("preserves distinct patterns and canonicalizes unpatterned objects", () => {
|
||||
expect(
|
||||
normalizeConfiguredMemoryExtraPaths([
|
||||
" notes ",
|
||||
{ path: "notes" },
|
||||
{ path: " notes ", pattern: " runbooks/**/*.md " },
|
||||
{ path: "notes", pattern: "runbooks/**/*.md" },
|
||||
{ path: "notes", pattern: "decisions/**/*.md" },
|
||||
]),
|
||||
).toEqual([
|
||||
"notes",
|
||||
{ path: "notes", pattern: "runbooks/**/*.md" },
|
||||
{ path: "notes", pattern: "decisions/**/*.md" },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,10 +7,7 @@ import {
|
||||
normalizeLowercaseStringOrEmpty,
|
||||
normalizeOptionalString,
|
||||
} from "@openclaw/normalization-core/string-coerce";
|
||||
import {
|
||||
normalizeStringEntries,
|
||||
uniqueStrings,
|
||||
} from "@openclaw/normalization-core/string-normalization";
|
||||
import type { MemoryExtraPath } from "./types.js";
|
||||
export { normalizeAgentId };
|
||||
|
||||
// Shared OpenClaw config helpers used by memory host and agent context code.
|
||||
@@ -29,9 +26,27 @@ type MemoryConfig = {
|
||||
type MemorySearchConfig = {
|
||||
enabled?: boolean;
|
||||
rememberAcrossConversations?: boolean;
|
||||
extraPaths?: string[];
|
||||
extraPaths?: MemoryExtraPath[];
|
||||
};
|
||||
|
||||
/** Trim and deduplicate configured extra-memory roots without losing pattern identity. */
|
||||
export function normalizeConfiguredMemoryExtraPaths(
|
||||
extraPaths?: MemoryExtraPath[],
|
||||
): MemoryExtraPath[] {
|
||||
const normalized = new Map<string, MemoryExtraPath>();
|
||||
for (const entry of extraPaths ?? []) {
|
||||
const configuredPath = (typeof entry === "string" ? entry : entry.path).trim();
|
||||
const pattern = typeof entry === "string" ? "" : entry.pattern?.trim() || "";
|
||||
if (configuredPath) {
|
||||
normalized.set(
|
||||
`${configuredPath}\0${pattern}`,
|
||||
pattern ? { path: configuredPath, pattern } : configuredPath,
|
||||
);
|
||||
}
|
||||
}
|
||||
return Array.from(normalized.values());
|
||||
}
|
||||
|
||||
/** Agent context limits that bound memory file reads. */
|
||||
type AgentContextLimitsConfig = {
|
||||
memoryGetMaxChars?: number;
|
||||
@@ -287,20 +302,24 @@ export function resolveMemoryHostAgentContextLimits(
|
||||
export function resolveMemoryHostSearchPathConfig(
|
||||
cfg: OpenClawConfig,
|
||||
agentId: string,
|
||||
): { enabled: boolean; rememberAcrossConversations: boolean; extraPaths: string[] } | null {
|
||||
): {
|
||||
enabled: boolean;
|
||||
rememberAcrossConversations: boolean;
|
||||
extraPaths: MemoryExtraPath[];
|
||||
} | null {
|
||||
const defaults = cfg.memory?.search;
|
||||
const overrides = resolveAgentConfig(cfg, agentId)?.memory?.search;
|
||||
const enabled = overrides?.enabled ?? defaults?.enabled ?? true;
|
||||
if (!enabled) {
|
||||
return null;
|
||||
}
|
||||
const rawPaths = normalizeStringEntries([
|
||||
const extraPaths = normalizeConfiguredMemoryExtraPaths([
|
||||
...(defaults?.extraPaths ?? []),
|
||||
...(overrides?.extraPaths ?? []),
|
||||
]);
|
||||
return {
|
||||
enabled,
|
||||
rememberAcrossConversations: resolveRememberAcrossConversations(cfg, agentId),
|
||||
extraPaths: uniqueStrings(rawPaths),
|
||||
extraPaths,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
ensureDir,
|
||||
isMemoryPath,
|
||||
listMemoryFiles,
|
||||
normalizeExtraMemoryPathEntries,
|
||||
normalizeExtraMemoryPaths,
|
||||
remapChunkLines,
|
||||
runWithConcurrency,
|
||||
@@ -57,6 +58,19 @@ function expectFileEntry(entry: Awaited<ReturnType<typeof buildFileEntry>>): Fil
|
||||
return entry;
|
||||
}
|
||||
|
||||
function tryCreateSymlink(target: string, linkPath: string, type?: "dir"): boolean {
|
||||
try {
|
||||
fsSync.symlinkSync(target, linkPath, type);
|
||||
return true;
|
||||
} catch (err) {
|
||||
const code = (err as NodeJS.ErrnoException).code;
|
||||
if (code === "EPERM" || code === "EACCES") {
|
||||
return false;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
function expectMultimodalIndexingChunk(
|
||||
built: Awaited<ReturnType<typeof buildMultimodalChunkForIndexing>>,
|
||||
): MultimodalIndexingChunk {
|
||||
@@ -150,6 +164,16 @@ describe("memory host SDK package internals", () => {
|
||||
path.join(os.homedir(), "shared-notes"),
|
||||
os.homedir(),
|
||||
]);
|
||||
expect(
|
||||
normalizeExtraMemoryPathEntries(workspaceDir, [
|
||||
{ path: " notes ", pattern: " runbooks/**/*.md " },
|
||||
{ path: "notes", pattern: "runbooks/**/*.md" },
|
||||
{ path: "notes", pattern: "archive/**/*.md" },
|
||||
]),
|
||||
).toEqual([
|
||||
{ path: path.resolve(workspaceDir, "notes"), pattern: "runbooks/**/*.md" },
|
||||
{ path: path.resolve(workspaceDir, "notes"), pattern: "archive/**/*.md" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("lists canonical markdown and enabled multimodal files", async () => {
|
||||
@@ -179,6 +203,34 @@ describe("memory host SDK package internals", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("filters extra directories by glob while preserving symlink skips", async () => {
|
||||
const tmpDir = getTmpDir();
|
||||
const extraDir = path.join(tmpDir, "extra");
|
||||
const outsideDir = path.join(tmpDir, "outside");
|
||||
fsSync.mkdirSync(path.join(extraDir, "notes", "nested"), { recursive: true });
|
||||
fsSync.mkdirSync(path.join(extraDir, "drafts"), { recursive: true });
|
||||
fsSync.mkdirSync(outsideDir, { recursive: true });
|
||||
fsSync.writeFileSync(path.join(extraDir, "root.md"), "root");
|
||||
fsSync.writeFileSync(path.join(extraDir, "notes", "keep.md"), "keep");
|
||||
fsSync.writeFileSync(path.join(extraDir, "notes", "nested", "keep.md"), "nested");
|
||||
fsSync.writeFileSync(path.join(extraDir, "drafts", "skip.md"), "skip");
|
||||
fsSync.writeFileSync(path.join(extraDir, "notes", "ignore.txt"), "ignore");
|
||||
fsSync.writeFileSync(path.join(outsideDir, "linked.md"), "linked");
|
||||
tryCreateSymlink(path.join(outsideDir, "linked.md"), path.join(extraDir, "notes", "linked.md"));
|
||||
tryCreateSymlink(outsideDir, path.join(extraDir, "notes", "linked-dir"), "dir");
|
||||
|
||||
const files = await listMemoryFiles(tmpDir, [
|
||||
{ path: extraDir, pattern: "root.md" },
|
||||
{ path: extraDir, pattern: "notes/**/*.md" },
|
||||
]);
|
||||
|
||||
expect(files.map((file) => path.relative(extraDir, file)).toSorted()).toEqual([
|
||||
path.join("notes", "keep.md"),
|
||||
path.join("notes", "nested", "keep.md"),
|
||||
"root.md",
|
||||
]);
|
||||
});
|
||||
|
||||
it("allows top-level dreams path casing variants", () => {
|
||||
expect(isMemoryPath("USER.md")).toBe(true);
|
||||
expect(isMemoryPath("dreams.md")).toBe(true);
|
||||
|
||||
@@ -4,9 +4,8 @@ import fsSync from "node:fs";
|
||||
import fs from "node:fs/promises";
|
||||
import { homedir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { normalizeStringEntries, uniqueStrings } from "@openclaw/normalization-core";
|
||||
import { runWithConcurrency as runWithConcurrencyImpl } from "./concurrency.js";
|
||||
import { MEMORY_HOST_ROOT_FILENAME } from "./config-utils.js";
|
||||
import { MEMORY_HOST_ROOT_FILENAME, normalizeConfiguredMemoryExtraPaths } from "./config-utils.js";
|
||||
import { estimateStructuredEmbeddingInputBytes } from "./embedding-input-limits.js";
|
||||
import { buildTextEmbeddingInput, type EmbeddingInput } from "./embedding-inputs.js";
|
||||
import {
|
||||
@@ -33,7 +32,7 @@ import {
|
||||
shouldSkipRootMemoryAuxiliaryPath,
|
||||
} from "./openclaw-runtime-memory.js";
|
||||
import { retryTransientMemoryRead } from "./read-retry.js";
|
||||
import type { MemoryEntryProvenance } from "./types.js";
|
||||
import type { MemoryEntryProvenance, MemoryExtraPath } from "./types.js";
|
||||
|
||||
export { hashText } from "./hash.js";
|
||||
import { hashText } from "./hash.js";
|
||||
@@ -98,16 +97,52 @@ function expandHomePath(value: string): string {
|
||||
return value;
|
||||
}
|
||||
|
||||
export function normalizeExtraMemoryPaths(workspaceDir: string, extraPaths?: string[]): string[] {
|
||||
if (!extraPaths?.length) {
|
||||
return [];
|
||||
export type NormalizedExtraMemoryPath = { path: string; pattern?: string };
|
||||
|
||||
export function normalizeExtraMemoryPathEntries(
|
||||
workspaceDir: string,
|
||||
extraPaths?: MemoryExtraPath[],
|
||||
): NormalizedExtraMemoryPath[] {
|
||||
return normalizeConfiguredMemoryExtraPaths(extraPaths).map((entry) => {
|
||||
const configuredPath = typeof entry === "string" ? entry : entry.path;
|
||||
const normalized: NormalizedExtraMemoryPath = {
|
||||
path: path.resolve(workspaceDir, expandHomePath(configuredPath)),
|
||||
};
|
||||
if (typeof entry !== "string") {
|
||||
normalized.pattern = entry.pattern?.replaceAll("\\", "/");
|
||||
}
|
||||
return normalized;
|
||||
});
|
||||
}
|
||||
|
||||
export function normalizeExtraMemoryPaths(
|
||||
workspaceDir: string,
|
||||
extraPaths?: MemoryExtraPath[],
|
||||
): string[] {
|
||||
return Array.from(
|
||||
new Set(normalizeExtraMemoryPathEntries(workspaceDir, extraPaths).map((entry) => entry.path)),
|
||||
);
|
||||
}
|
||||
|
||||
export function matchesExtraMemoryPathEntry(
|
||||
entry: NormalizedExtraMemoryPath,
|
||||
candidatePath: string,
|
||||
): boolean {
|
||||
if (!entry.pattern) {
|
||||
return true;
|
||||
}
|
||||
const relativePath = path.relative(entry.path, candidatePath);
|
||||
if (!relativePath) {
|
||||
return true;
|
||||
}
|
||||
if (relativePath.startsWith("..") || path.isAbsolute(relativePath)) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
return path.posix.matchesGlob(relativePath.replaceAll(path.sep, "/"), entry.pattern);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
const resolved = normalizeStringEntries(extraPaths)
|
||||
.map((value) => expandHomePath(value))
|
||||
.map((value) =>
|
||||
path.isAbsolute(value) ? path.resolve(value) : path.resolve(workspaceDir, value),
|
||||
);
|
||||
return uniqueStrings(resolved);
|
||||
}
|
||||
|
||||
export function isMemoryPath(relPath: string): boolean {
|
||||
@@ -149,6 +184,7 @@ async function collectMemoryFilesFromDir(
|
||||
files: string[],
|
||||
multimodal?: MemoryMultimodalSettings,
|
||||
shouldSkipPath?: (absPath: string) => boolean,
|
||||
extraPathEntry?: NormalizedExtraMemoryPath,
|
||||
): Promise<void> {
|
||||
const scan = await walkDirectory(dir, {
|
||||
symlinks: "skip",
|
||||
@@ -156,14 +192,15 @@ async function collectMemoryFilesFromDir(
|
||||
include: (entry) =>
|
||||
!shouldSkipPath?.(entry.path) &&
|
||||
entry.kind === "file" &&
|
||||
isAllowedMemoryFilePath(entry.path, multimodal),
|
||||
isAllowedMemoryFilePath(entry.path, multimodal) &&
|
||||
(!extraPathEntry || matchesExtraMemoryPathEntry(extraPathEntry, entry.path)),
|
||||
});
|
||||
files.push(...scan.entries.map((entry) => entry.path));
|
||||
}
|
||||
|
||||
export async function listMemoryFiles(
|
||||
workspaceDir: string,
|
||||
extraPaths?: string[],
|
||||
extraPaths?: MemoryExtraPath[],
|
||||
multimodal?: MemoryMultimodalSettings,
|
||||
): Promise<string[]> {
|
||||
const result: string[] = [];
|
||||
@@ -197,9 +234,10 @@ export async function listMemoryFiles(
|
||||
}
|
||||
} catch {}
|
||||
|
||||
const normalizedExtraPaths = normalizeExtraMemoryPaths(workspaceDir, extraPaths);
|
||||
const normalizedExtraPaths = normalizeExtraMemoryPathEntries(workspaceDir, extraPaths);
|
||||
if (normalizedExtraPaths.length > 0) {
|
||||
for (const inputPath of normalizedExtraPaths) {
|
||||
for (const entry of normalizedExtraPaths) {
|
||||
const inputPath = entry.path;
|
||||
if (shouldSkipWorkspaceMemoryPath(inputPath)) {
|
||||
continue;
|
||||
}
|
||||
@@ -214,6 +252,7 @@ export async function listMemoryFiles(
|
||||
result,
|
||||
multimodal,
|
||||
shouldSkipWorkspaceMemoryPath,
|
||||
entry,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { readMemoryFile } from "./read-file.js";
|
||||
import { readAgentMemoryFile, readMemoryFile } from "./read-file.js";
|
||||
|
||||
async function createDirectorySymlink(target: string, linkPath: string): Promise<boolean> {
|
||||
try {
|
||||
@@ -111,6 +111,34 @@ describe("readMemoryFile", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("enforces configured extra path glob patterns through agent reads", async () => {
|
||||
const tmpRoot = await fs.mkdtemp(path.join(os.tmpdir(), "memory-read-file-"));
|
||||
try {
|
||||
const workspaceDir = path.join(tmpRoot, "workspace");
|
||||
const extraDir = path.join(tmpRoot, "extra");
|
||||
const allowedPath = path.join(extraDir, "runbooks", "team", "allowed.md");
|
||||
const excludedPath = path.join(extraDir, "private.md");
|
||||
await fs.mkdir(workspaceDir, { recursive: true });
|
||||
await fs.mkdir(path.dirname(allowedPath), { recursive: true });
|
||||
await fs.writeFile(allowedPath, "allowed", "utf-8");
|
||||
await fs.writeFile(excludedPath, "private", "utf-8");
|
||||
|
||||
const extraPaths = [{ path: extraDir, pattern: "runbooks/**/*.md" }];
|
||||
const cfg = {
|
||||
agents: { entries: { main: { workspace: workspaceDir } } },
|
||||
memory: { search: { extraPaths } },
|
||||
};
|
||||
await expect(
|
||||
readAgentMemoryFile({ cfg, agentId: "main", relPath: allowedPath }),
|
||||
).resolves.toMatchObject({ text: "allowed" });
|
||||
await expect(
|
||||
readAgentMemoryFile({ cfg, agentId: "main", relPath: excludedPath }),
|
||||
).rejects.toThrow("path required");
|
||||
} finally {
|
||||
await fs.rm(tmpRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("retries transient read errors for workspace memory files", async () => {
|
||||
const tmpRoot = await fs.mkdtemp(path.join(os.tmpdir(), "memory-read-file-"));
|
||||
try {
|
||||
|
||||
@@ -16,13 +16,18 @@ import {
|
||||
root,
|
||||
statRegularFile,
|
||||
} from "./fs-utils.js";
|
||||
import { isMemoryPath, normalizeExtraMemoryPaths } from "./internal.js";
|
||||
import {
|
||||
isMemoryPath,
|
||||
matchesExtraMemoryPathEntry,
|
||||
normalizeExtraMemoryPathEntries,
|
||||
} from "./internal.js";
|
||||
import {
|
||||
buildMemoryReadResult,
|
||||
DEFAULT_MEMORY_READ_LINES,
|
||||
type MemoryReadResult,
|
||||
} from "./read-file-shared.js";
|
||||
import { retryTransientMemoryRead } from "./read-retry.js";
|
||||
import type { MemoryExtraPath } from "./types.js";
|
||||
|
||||
// Secure markdown memory-file reader for workspace and configured extra paths.
|
||||
|
||||
@@ -66,7 +71,7 @@ function isFileDisappearedDuringReadError(err: unknown): boolean {
|
||||
/** Read a validated memory markdown file from workspace or configured extra paths. */
|
||||
export async function readMemoryFile(params: {
|
||||
workspaceDir: string;
|
||||
extraPaths?: string[];
|
||||
extraPaths?: MemoryExtraPath[];
|
||||
relPath: string;
|
||||
from?: number;
|
||||
lines?: number;
|
||||
@@ -85,15 +90,18 @@ export async function readMemoryFile(params: {
|
||||
const allowedWorkspace = inWorkspace && isMemoryPath(relPath);
|
||||
let allowedAdditional = false;
|
||||
if (!allowedWorkspace && (params.extraPaths?.length ?? 0) > 0) {
|
||||
const additionalPaths = normalizeExtraMemoryPaths(params.workspaceDir, params.extraPaths);
|
||||
const additionalPaths = normalizeExtraMemoryPathEntries(params.workspaceDir, params.extraPaths);
|
||||
for (const additionalPath of additionalPaths) {
|
||||
try {
|
||||
const stat = await fs.lstat(additionalPath);
|
||||
const stat = await fs.lstat(additionalPath.path);
|
||||
if (stat.isSymbolicLink()) {
|
||||
continue;
|
||||
}
|
||||
if (stat.isDirectory()) {
|
||||
if (await isAllowedAdditionalDirectoryPath(additionalPath, absPath)) {
|
||||
if (
|
||||
matchesExtraMemoryPathEntry(additionalPath, absPath) &&
|
||||
(await isAllowedAdditionalDirectoryPath(additionalPath.path, absPath))
|
||||
) {
|
||||
const candidateStat = await fs.lstat(absPath).catch(() => null);
|
||||
if (candidateStat?.isSymbolicLink()) {
|
||||
continue;
|
||||
@@ -103,7 +111,7 @@ export async function readMemoryFile(params: {
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (stat.isFile() && absPath === additionalPath && absPath.endsWith(".md")) {
|
||||
if (stat.isFile() && absPath === additionalPath.path && absPath.endsWith(".md")) {
|
||||
allowedAdditional = true;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,9 @@ export type MemoryOriginClass = "owner" | "agent" | "untrusted" | "system";
|
||||
|
||||
export type MemorySessionKind = "interactive" | "cron" | "heartbeat" | "subagent" | "unknown";
|
||||
|
||||
/** Additional memory root, optionally narrowed by a root-relative glob. */
|
||||
export type MemoryExtraPath = string | { path: string; pattern?: string };
|
||||
|
||||
export type MemoryEntryProvenance = {
|
||||
originClass: MemoryOriginClass;
|
||||
sessionKind: MemorySessionKind;
|
||||
@@ -108,7 +111,7 @@ export type MemoryProviderStatus = {
|
||||
dirty?: boolean;
|
||||
workspaceDir?: string;
|
||||
dbPath?: string;
|
||||
extraPaths?: string[];
|
||||
extraPaths?: MemoryExtraPath[];
|
||||
sources?: MemorySource[];
|
||||
sourceCounts?: Array<{ source: MemorySource; files: number; chunks: number }>;
|
||||
cache?: { enabled: boolean; entries?: number; maxEntries?: number };
|
||||
|
||||
@@ -579,7 +579,11 @@ describe("memory search config", () => {
|
||||
const cfg = asConfig({
|
||||
memory: {
|
||||
search: {
|
||||
extraPaths: ["/shared/notes", " docs "],
|
||||
extraPaths: [
|
||||
"/shared/notes",
|
||||
" docs ",
|
||||
{ path: "../team-notes", pattern: "runbooks/**/*.md" },
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -591,7 +595,11 @@ describe("memory search config", () => {
|
||||
default: true,
|
||||
memory: {
|
||||
search: {
|
||||
extraPaths: ["/shared/notes", "../team-notes"],
|
||||
extraPaths: [
|
||||
"/shared/notes",
|
||||
{ path: "../team-notes", pattern: "runbooks/**/*.md" },
|
||||
{ path: "../team-notes", pattern: "decisions/**/*.md" },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -599,7 +607,12 @@ describe("memory search config", () => {
|
||||
},
|
||||
});
|
||||
const resolved = resolveMemorySearchConfig(cfg, "main");
|
||||
expect(resolved?.extraPaths).toEqual(["/shared/notes", "docs", "../team-notes"]);
|
||||
expect(resolved?.extraPaths).toEqual([
|
||||
"/shared/notes",
|
||||
"docs",
|
||||
{ path: "../team-notes", pattern: "runbooks/**/*.md" },
|
||||
{ path: "../team-notes", pattern: "decisions/**/*.md" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("normalizes multimodal settings", () => {
|
||||
|
||||
@@ -6,13 +6,13 @@ import {
|
||||
MAX_TIMER_TIMEOUT_MS,
|
||||
resolvePositiveTimerTimeoutMs,
|
||||
} from "@openclaw/normalization-core/number-coercion";
|
||||
import {
|
||||
normalizeStringEntries,
|
||||
uniqueStrings,
|
||||
} from "@openclaw/normalization-core/string-normalization";
|
||||
import type { OpenClawConfig, MemorySearchConfig } from "../config/config.js";
|
||||
import type { SecretInput } from "../config/types.secrets.js";
|
||||
import { resolveRememberAcrossConversations } from "../memory-host-sdk/host/config-utils.js";
|
||||
import {
|
||||
normalizeConfiguredMemoryExtraPaths,
|
||||
resolveRememberAcrossConversations,
|
||||
} from "../memory-host-sdk/host/config-utils.js";
|
||||
import type { MemoryExtraPath } from "../memory-host-sdk/host/types.js";
|
||||
import {
|
||||
isMemoryMultimodalEnabled,
|
||||
normalizeMemoryMultimodalSettings,
|
||||
@@ -32,7 +32,7 @@ export type ResolvedMemorySearchConfig = {
|
||||
sources: Array<"memory" | "sessions">;
|
||||
/** Sources searched when memory_search omits an explicit corpus. */
|
||||
searchSources: Array<"memory" | "sessions">;
|
||||
extraPaths: string[];
|
||||
extraPaths: MemoryExtraPath[];
|
||||
multimodal: MemoryMultimodalSettings;
|
||||
provider: string;
|
||||
remote?: {
|
||||
@@ -270,11 +270,10 @@ function mergeConfig(
|
||||
rememberAcrossConversations ? [...searchSources, "sessions"] : configuredSources,
|
||||
sessionMemory,
|
||||
);
|
||||
const rawPaths = normalizeStringEntries([
|
||||
const extraPaths = normalizeConfiguredMemoryExtraPaths([
|
||||
...(defaults?.extraPaths ?? []),
|
||||
...(overrides?.extraPaths ?? []),
|
||||
]);
|
||||
const extraPaths = uniqueStrings(rawPaths);
|
||||
const multimodal = normalizeMemoryMultimodalSettings({
|
||||
enabled: overrides?.multimodal?.enabled ?? defaults?.multimodal?.enabled,
|
||||
modalities: overrides?.multimodal?.modalities ?? defaults?.multimodal?.modalities,
|
||||
|
||||
@@ -5,6 +5,8 @@ import {
|
||||
type LegacyConfigMigrationSpec,
|
||||
type LegacyConfigRule,
|
||||
} from "../../../config/legacy.shared.js";
|
||||
import { normalizeConfiguredMemoryExtraPaths } from "../../../memory-host-sdk/host/config-utils.js";
|
||||
import type { MemoryExtraPath } from "../../../memory-host-sdk/host/types.js";
|
||||
import { visitAgentConfigScopes } from "./legacy-config-migrations.runtime.tier-eval.js";
|
||||
import { deleteRetiredPath } from "./legacy-config-record-shared.js";
|
||||
|
||||
@@ -58,22 +60,28 @@ function migrateRetiredQmdExternalPaths(params: {
|
||||
}
|
||||
const memory = ensureRecord(params.scope, "memory");
|
||||
const search = ensureRecord(memory, "search");
|
||||
const existingPaths = Array.isArray(search.extraPaths)
|
||||
? search.extraPaths.filter((value): value is string => typeof value === "string")
|
||||
: [];
|
||||
const nextPaths = [...existingPaths];
|
||||
const seen = new Set(existingPaths.map((value) => value.trim()).filter(Boolean));
|
||||
const existingPaths = normalizeConfiguredMemoryExtraPaths(
|
||||
Array.isArray(search.extraPaths)
|
||||
? search.extraPaths.filter(
|
||||
(entry): entry is MemoryExtraPath =>
|
||||
typeof entry === "string" || typeof getRecord(entry)?.path === "string",
|
||||
)
|
||||
: [],
|
||||
);
|
||||
const nextPaths: MemoryExtraPath[] = [...existingPaths];
|
||||
const entryKey = (entry: MemoryExtraPath) =>
|
||||
typeof entry === "string" ? `${entry}\0` : `${entry.path}\0${entry.pattern?.trim() ?? ""}`;
|
||||
const seen = new Set(existingPaths.map(entryKey));
|
||||
let added = 0;
|
||||
let unsupportedPatterns = 0;
|
||||
for (const entry of params.entries) {
|
||||
if (!seen.has(entry.path)) {
|
||||
seen.add(entry.path);
|
||||
nextPaths.push(entry.path);
|
||||
added += 1;
|
||||
}
|
||||
if (entry.pattern && entry.pattern !== "**/*.md") {
|
||||
unsupportedPatterns += 1;
|
||||
const nextEntry: MemoryExtraPath = entry.pattern ? entry : entry.path;
|
||||
const key = entryKey(nextEntry);
|
||||
if (seen.has(key)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(key);
|
||||
nextPaths.push(nextEntry);
|
||||
added += 1;
|
||||
}
|
||||
if (added > 0) {
|
||||
search.extraPaths = nextPaths;
|
||||
@@ -81,11 +89,6 @@ function migrateRetiredQmdExternalPaths(params: {
|
||||
`Migrated ${added} external QMD path${added === 1 ? "" : "s"} from ${params.sourcePath} → ${params.targetPath}.`,
|
||||
);
|
||||
}
|
||||
if (unsupportedPatterns > 0) {
|
||||
params.changes.push(
|
||||
`Removed ${unsupportedPatterns} QMD path pattern filter${unsupportedPatterns === 1 ? "" : "s"} from ${params.sourcePath}; builtin memory indexes supported files under the preserved paths.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function migrateRetiredMemoryQmd(raw: Record<string, unknown>, changes: string[]): void {
|
||||
|
||||
+4
-9
@@ -103,7 +103,8 @@ describe("retired QMD memory config migration", () => {
|
||||
"notes",
|
||||
"/tmp/shared",
|
||||
"/tmp/global",
|
||||
"/tmp/patterned",
|
||||
{ path: "/tmp/patterned", pattern: "notes/*.md" },
|
||||
{ path: "/tmp/shared", pattern: "**/*.md" },
|
||||
"/tmp/search",
|
||||
]);
|
||||
expect(result.raw).toHaveProperty("agents.defaults.memory.search.extraPaths", [
|
||||
@@ -113,17 +114,11 @@ describe("retired QMD memory config migration", () => {
|
||||
expect(result.raw).toHaveProperty("agents.entries.research.memory.search.enabled", false);
|
||||
expect(result.raw).toHaveProperty("agents.entries.research.memory.search.extraPaths", [
|
||||
"/tmp/existing",
|
||||
"/tmp/research",
|
||||
{ path: "/tmp/research", pattern: "*.md" },
|
||||
]);
|
||||
expect(result.raw).toHaveProperty("agents.list.0.memory.search.extraPaths", ["/tmp/list"]);
|
||||
expect(result.changes).toContain(
|
||||
"Migrated 3 external QMD paths from memory.qmd.paths and memory.search.qmd.extraCollections → memory.search.extraPaths.",
|
||||
);
|
||||
expect(result.changes).toContain(
|
||||
"Removed 1 QMD path pattern filter from memory.qmd.paths and memory.search.qmd.extraCollections; builtin memory indexes supported files under the preserved paths.",
|
||||
);
|
||||
expect(result.changes).toContain(
|
||||
"Removed 1 QMD path pattern filter from agents.entries.research.memory.search.qmd.extraCollections; builtin memory indexes supported files under the preserved paths.",
|
||||
"Migrated 4 external QMD paths from memory.qmd.paths and memory.search.qmd.extraCollections → memory.search.extraPaths.",
|
||||
);
|
||||
expect(result.changes).toContain(
|
||||
"Removed retired QMD memory configuration; builtin memory is now the only memory engine.",
|
||||
|
||||
@@ -148,6 +148,32 @@ describe("config schema regressions", () => {
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("accepts mixed extra memory path entries", () => {
|
||||
expect(
|
||||
validateConfigObject({
|
||||
memory: {
|
||||
search: {
|
||||
extraPaths: ["../team-notes", { path: "../shared", pattern: "runbooks/**/*.md" }],
|
||||
},
|
||||
},
|
||||
agents: { defaults: {} },
|
||||
}).ok,
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ pattern: "**/*.md" },
|
||||
{ path: "../shared", pattern: 42 },
|
||||
{ path: "../shared", name: "legacy-qmd-name" },
|
||||
])("rejects invalid extra memory path object %j", (entry) => {
|
||||
expect(
|
||||
validateConfigObject({
|
||||
memory: { search: { extraPaths: [entry] } },
|
||||
agents: { defaults: {} },
|
||||
}).ok,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects local memorySearch GPU policy", () => {
|
||||
const res = validateConfigObject({
|
||||
memory: {
|
||||
|
||||
@@ -203,7 +203,11 @@ export const MODEL_FIELD_HELP: Record<string, string> = {
|
||||
"memory.search.sources":
|
||||
'Chooses which sources are indexed: "memory" reads MEMORY.md + memory files, and "sessions" includes transcript history. Keep ["memory"] unless you need recall from prior chat transcripts.',
|
||||
"memory.search.extraPaths":
|
||||
"Adds extra directories or .md files to the memory index beyond default memory files. Use this when key reference docs live elsewhere in your repo; when multimodal memory is enabled, matching image/audio files under these paths are also eligible for indexing.",
|
||||
"Adds extra directories or .md files to the memory index beyond default memory files. Entries may be path strings or objects with a root-relative glob pattern. When multimodal memory is enabled, matching image/audio files under these paths are also eligible for indexing.",
|
||||
"memory.search.extraPaths.*.path":
|
||||
"Sets the extra memory directory or file. Relative paths resolve from the agent workspace; direct file entries are indexed exactly.",
|
||||
"memory.search.extraPaths.*.pattern":
|
||||
'Limits a directory entry to supported files matching this root-relative glob, for example "runbooks/**/*.md". Omit it to scan all supported files recursively.',
|
||||
"memory.search.multimodal":
|
||||
'Optional multimodal memory settings for indexing image and audio files from configured extra paths. Keep this off unless your embedding model explicitly supports cross-modal embeddings, and set `memory.search.fallback` to "none" while it is enabled. Matching files are uploaded to the configured remote embedding provider during indexing.',
|
||||
"memory.search.multimodal.enabled":
|
||||
|
||||
@@ -443,6 +443,8 @@ export const FIELD_LABELS: Record<string, string> = {
|
||||
"memory.search.rememberAcrossConversations": "Remember Across Conversations",
|
||||
"memory.search.sources": "Memory Search Sources",
|
||||
"memory.search.extraPaths": "Extra Memory Paths",
|
||||
"memory.search.extraPaths.*.path": "Extra Memory Path",
|
||||
"memory.search.extraPaths.*.pattern": "Extra Memory Path Pattern",
|
||||
"memory.search.multimodal": "Memory Search Multimodal",
|
||||
"memory.search.multimodal.enabled": "Enable Memory Search Multimodal",
|
||||
"memory.search.multimodal.modalities": "Memory Search Multimodal Modalities",
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import type { MemoryExtraPath } from "../memory-host-sdk/host/types.js";
|
||||
/**
|
||||
* Memory config types shared by core context-engine paths and memory host/plugin runtimes.
|
||||
* Builtin memory stays core-owned.
|
||||
*/
|
||||
import type { SecretInput } from "./types.secrets.js";
|
||||
|
||||
export type { MemoryExtraPath } from "../memory-host-sdk/host/types.js";
|
||||
|
||||
/** Citation rendering mode for memory-injected context. */
|
||||
export type MemoryCitationsMode = "auto" | "on" | "off";
|
||||
|
||||
@@ -21,8 +24,8 @@ export type MemorySearchConfig = {
|
||||
rememberAcrossConversations?: boolean;
|
||||
/** Sources to index and search (default: ["memory"]). */
|
||||
sources?: Array<"memory" | "sessions">;
|
||||
/** Extra paths to include in memory search (directories or .md files). */
|
||||
extraPaths?: string[];
|
||||
/** Extra paths to include in memory search, optionally filtered by a glob. */
|
||||
extraPaths?: MemoryExtraPath[];
|
||||
/** Optional multimodal file indexing for selected extra paths. */
|
||||
multimodal?: {
|
||||
/** Enable image/audio embeddings from extraPaths. */
|
||||
|
||||
@@ -752,7 +752,14 @@ export const MemorySearchSchema = z
|
||||
enabled: z.boolean().optional(),
|
||||
rememberAcrossConversations: z.boolean().optional(),
|
||||
sources: z.array(z.union([z.literal("memory"), z.literal("sessions")])).optional(),
|
||||
extraPaths: z.array(z.string()).optional(),
|
||||
extraPaths: z
|
||||
.array(
|
||||
z.union([
|
||||
z.string(),
|
||||
z.object({ path: z.string(), pattern: z.string().optional() }).strict(),
|
||||
]),
|
||||
)
|
||||
.optional(),
|
||||
multimodal: z
|
||||
.object({
|
||||
enabled: z.boolean().optional(),
|
||||
|
||||
@@ -1,2 +1,5 @@
|
||||
/** Core-facing memory host config helpers. */
|
||||
export { resolveRememberAcrossConversations } from "../../../packages/memory-host-sdk/src/host/config-utils.js";
|
||||
export {
|
||||
normalizeConfiguredMemoryExtraPaths,
|
||||
resolveRememberAcrossConversations,
|
||||
} from "../../../packages/memory-host-sdk/src/host/config-utils.js";
|
||||
|
||||
@@ -25,6 +25,7 @@ export {
|
||||
isTransientMemoryReadError,
|
||||
listMemoryFiles,
|
||||
loadSqliteVecExtension,
|
||||
matchesExtraMemoryPathEntry,
|
||||
MEMORY_CHUNKING_VERSION,
|
||||
MEMORY_EMBEDDING_CACHE_TABLE,
|
||||
MEMORY_INDEX_CHUNKS_TABLE,
|
||||
@@ -37,6 +38,7 @@ export {
|
||||
MEMORY_INDEX_STATE_TABLE,
|
||||
MEMORY_INDEX_VECTOR_TABLE,
|
||||
normalizeProjectAnnotationKey,
|
||||
normalizeExtraMemoryPathEntries,
|
||||
normalizeExtraMemoryPaths,
|
||||
parseEmbedding,
|
||||
readMemoryFile,
|
||||
@@ -58,6 +60,7 @@ export type {
|
||||
CuratedMarkdownEntry,
|
||||
CuratedProjectAnnotations,
|
||||
MemoryEntryProvenance,
|
||||
MemoryExtraPath,
|
||||
MemoryOriginClass,
|
||||
MemorySearchResult,
|
||||
MemorySessionKind,
|
||||
|
||||
Reference in New Issue
Block a user