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:
Peter Steinberger
2026-08-09 13:05:24 -07:00
committed by GitHub
parent db61480056
commit 8dbd30ba74
30 changed files with 589 additions and 220 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
{
"core": 2284,
"core": 2288,
"channel": 3718,
"plugin": 4053
}
+2 -2
View File
@@ -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
+114 -114
View File
@@ -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
+3 -4
View File
@@ -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.
+7 -5
View File
@@ -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 ?? [];
+17
View File
@@ -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);
+55 -16
View File
@@ -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 {
+14 -6
View File
@@ -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;
}
+4 -1
View File
@@ -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 };
+16 -3
View File
@@ -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", () => {
+7 -8
View File
@@ -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 {
@@ -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: {
+5 -1
View File
@@ -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":
+2
View File
@@ -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",
+5 -2
View File
@@ -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. */
+8 -1
View File
@@ -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(),
+4 -1
View File
@@ -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,