mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
refactor(plugins)!: remove the plugin state lease API (#121140)
* refactor(plugins)!: remove plugin state lease API * docs(plugins): document state lease removal
This commit is contained in:
committed by
GitHub
parent
a828bfe04e
commit
9809375fda
@@ -3,10 +3,10 @@ d20eb6a6a77f72566266b88d191b30155757a1cc3d3886d3b6c437176bbac771 module/account
|
||||
71522995185b956a0cc4927a472cc8d1153e5e998874bfd9a750513175174713 module/account-id
|
||||
006e581db74e461dcf4d7fd299b09d285f32c91ac12a56de1d21307d272e5a0a module/account-resolution
|
||||
3fe118210b885af40088457ed81ffa5ede18c8e695295731a2ee059af46843cc module/agent-config-primitives
|
||||
81bc83b3e08b61ed273c93434a4261714ad53e44c62cb7292f4d9689b445f0b2 module/agent-harness
|
||||
643782e8ab06ef3d5dd86912e6a554022be0e373e769d6dcef5c4c1929a60642 module/agent-harness-runtime
|
||||
3edab39f3f3970f9fef3416576737a8945a1b8282d8363f906b1bc1c19bca7e4 module/agent-harness
|
||||
8b5c61894591a4d91435a8af39128be75cfd5b97a532d1d8df2bf40576c6ccf6 module/agent-harness-runtime
|
||||
595ef30046b8ba4bfd427e5a33b65ca28ab1a699ce7e90d6afa2ba2b187e22c0 module/agent-media-payload
|
||||
74377686ae931a3157a34a72c8b83e4e3db04165f6e209adfe1456ea923ed494 module/agent-runtime
|
||||
8966ac7d13e8cdc7639cce1cbc7a6bb1af0c8064281e62272fbe0301ea365fe4 module/agent-runtime
|
||||
72fa7e17dd1694a24f373dece9d3035c3fc25e1e113954328e16df03d9be7b55 module/agent-scope-runtime
|
||||
8fecb210e22bce4532b6ab649b09465f0bd2c857a44abf40db7d683d6491e6da module/allow-from
|
||||
ac6c71c3b7c3d1f6d3219d7514a1d1e0c44198358e6e9a4eae06e318d52a8433 module/allowlist-config-edit
|
||||
@@ -26,19 +26,19 @@ d7e53de63b0ac11a266e4abdc18ba6e9401b80309f5c8f5f6a72a00f65dfe3bd module/boolean
|
||||
c2cc71d5070b6071c51248b0648d1ad1a9468d3737df890adc77ec02025e8853 module/channel-config-primitives
|
||||
76ad615d374431580ea1755594e2ce3ce047ac1de9fd4621053dca0fbc3afc4d module/channel-config-schema
|
||||
63d4f5bc22d6e7779fcfa1f73e70df689189fcc876af26e05a4e3334b95675a9 module/channel-contract
|
||||
d6fa8e5a1a82c762e782d57cd9eef26a340a9640051c1e138869f62a82121f26 module/channel-core
|
||||
48ccb74aee307f0065ccef8dc4a9f6a7ec2c05108948c0b3eba892006eb088b5 module/channel-core
|
||||
f6ddf9086bc224b4b1a516cd7e056467a529d19435930b1c8ddff7052d890ffa module/channel-dm-policy
|
||||
91c51b7e4a8a418490101e22bc09d82608c5d9058a26b49a56b59efeccc0def1 module/channel-entry-contract
|
||||
c29f506b02c26df34c80924ef6fce4ca968cf2ab4b24b03402728e6c1dec99de module/channel-entry-contract
|
||||
b14ed3e3235fab5725a5eac86bb7910174a21ae1d7f2b4433891867396dcc566 module/channel-feedback
|
||||
75e8e9616729c15871ceedff19f1f080ff9fba7cfac83b8f7fd0bb6e50dc2bd2 module/channel-inbound
|
||||
fd5017da2359c39fe5919e5d7a5b2b8bf83b86af9c61cd5fcbb0a0cdb792bc6d module/channel-inbound
|
||||
f58349b93db16be763c7ba4c00cbc13b1b64911c8eefa5a1bfba1e95809e81cf module/channel-inbound-debounce
|
||||
cb7f865c9953b5f0925c918b2bed29fb6629fbd643f4f69bd2bf345970864cbf module/channel-ingress-runtime
|
||||
af775123f5526907732b7bebd6e2258ffe4ec733b2435007361bf7aba5b4eaf4 module/channel-lifecycle
|
||||
0e47457e38d1df0bd572e1408cde2ca6a788b65205f43c585316b5ad3a8f2f16 module/channel-logging
|
||||
29486914ef96d6cf2aaac2407c40d5fff2a40db981b68818f970bde6fe830589 module/channel-message
|
||||
e8bbe4cffcc095099455c6aacc3a76242b6c74edffb51d8142c4ed1e6ad007b8 module/channel-outbound
|
||||
366ef5b3d4c44716f07a6d7a6e5b611e2f61d1d43c85cd86da2e0df6970d77a4 module/channel-pairing
|
||||
38fa839f963ce75020305184378bdec948f1e10fec3a88a91bf5a1cb511125fb module/channel-plugin-common
|
||||
a18aab1dde656c90f6a4ba701da69bcd90c4c8df93172f157c2b3c4e9456b025 module/channel-message
|
||||
02f4e38940bc66e6dfeb91920b3c9a2ad6399205c0b023876768391c8dea4ffa module/channel-outbound
|
||||
8193aaae8a707e7e7b4631bb65aab4af05dfa7d95b9cbd42edd3320ddfacbd2e module/channel-pairing
|
||||
e866434a1be8aa6a154c6660c2d9defe918a4fef7607bac9964eb3822a64fd69 module/channel-plugin-common
|
||||
113d135501f2777f308c4c3a59dc29f722f8c385ddfe5628994f14ed37246442 module/channel-policy
|
||||
c3bdfac92ace16eccca1bd44c95a3e73e39d7f996f1714cdfdf102647305df55 module/channel-reply-pipeline
|
||||
482370e60135db9bfaf07f24bab549e5fde09ab265a6061a1f587c5d93929e91 module/channel-runtime-context
|
||||
@@ -51,8 +51,8 @@ b89224a72be296c82eeb1933b179ec0479627e80e1c61717b6cc7a778dbfa5b2 module/channel
|
||||
67df67da5ae72e9eaeb19d41b6bd2432ec4fd8b7b63b2b616fb98f3b4e0ec41d module/channel-streaming-config
|
||||
1303df5cc58539c6941e2cd159c93259804c925795219f1630f4d740896a77c1 module/cli-argv
|
||||
ad12670dbfe538f8d0ebf4fb2b68080e93a760278278e6b1ce9bb129d4b2d533 module/collection-runtime
|
||||
a493eb2d0ce8af534a7f6ede308e2a5ee501bac0ae9a5b457ff51e58bfd0d081 module/command-auth
|
||||
b075c5fbaff0f5c163365da4612bcda391099460f4b5436bb1f5d2bed84f0f2f module/command-auth-native
|
||||
839668672f6b4694209c67fbb6d6a529ec3581f830c81454565e5ef0d1c1b71c module/command-auth
|
||||
77e955f3aa114cecff5d138a06471dbf304570cf90a1742ea27541bae414151b module/command-auth-native
|
||||
4db2a3623b116976a2e4cb366163194a8073c8d56a9c74105127bee59c7d07f4 module/command-detection
|
||||
0f6cf0b06cd65f2bfded9d1054873166fa6ad22c71f4f1cc5afb7a49711cc365 module/command-primitives-runtime
|
||||
03eb97e7b47a79ad4b40b37a2bac418c14f85e90969f70fd6fc5c80bb27b8c49 module/command-status
|
||||
@@ -60,12 +60,12 @@ b56349e8d8034152be55161d4bb4c9bf69d0f68f7ea07431f28a4ee7a707d576 module/config-
|
||||
9d2c42377ef981ea6732f96bd3a9c1a16fa2852936516a3fca5adc2b6b0b736e module/config-mutation
|
||||
6c70300fd4bf84808f742687eaad92339fc942abd5cfa2378807a0756decdbd1 module/config-runtime
|
||||
d5a157da395c2b548347b7fae054af0a90afbcae71c7171d0be5c66d92271328 module/conversation-runtime
|
||||
02ead333baf948ac3d565aed0a659ddf26aaadc894871219129a5c190580b90d module/core
|
||||
5a4ef21b21e2b9539e8dfa95512747d7bc7d6c04cb8adb2145cdd00b40060e5f module/dedupe-runtime
|
||||
6b4dd07e9be7e418d60de7f90d667fa3b2711bd3fada941ea104be0e58d49e7e module/core
|
||||
2a6178f6eb84fd9626e19b3027fd2e734b2688e14b0c554c2f0ffaed444f8e33 module/dedupe-runtime
|
||||
ebef0e650ab45e44c9335e2b3e15588c968cea6dadd125364a076f9c50ad1e8c module/device-bootstrap
|
||||
4fe9beab67598950c144d86add5fd3b1ce475aaf37644d2f9798703052d98210 module/diagnostic-runtime
|
||||
734898717c8669f1c3a35dca93db121508368edac7dafcd1ccce71f432680979 module/directory-runtime
|
||||
a0ff472d3c6a77fc46d4ed2804f408e8e3886dd3c34c0a95fbff4f1f6943dff2 module/discord
|
||||
ccb5f06eaf59e5857d5d693a5274c1fdbb646148a50a5b9d5049b6a6d4294e96 module/discord
|
||||
2b01f2e5a52713158665372b358fb6059903f68b6efbedff700741d9feca7696 module/error-runtime
|
||||
03eeafd10471b94a2651e0d42380a32f5ffd4f33c47e4b0161205093163e9407 module/extension-shared
|
||||
dd9f6e0fd33cc88b22543c1ee30cc09cf4de4d8f30dff7b7f9cebef885c21543 module/gateway-method-runtime
|
||||
@@ -75,7 +75,7 @@ f3595668fc4c20b2b34df67d3fa4d5432c16fa3be5fec9e63d8f14512ab6732f module/health
|
||||
60d126e420e212415f25b6e90c2aca7513a1275e14b2bad27b06aeb6ab12eaa5 module/hook-runtime
|
||||
da9d83537008db2c9339f3a4235ed2b7ee3ac986cacdc8bf8fdfaf1a025bd6a9 module/inbound-envelope
|
||||
4928af5d2509f696b896f53ac790303a0742202dbcdae3e44fe6d1b434a9c1ba module/inbound-event-delivery
|
||||
e5156b953002c4a961b6f435a69928649c7df72a0fb41ad73c239fa0d2f4061b module/inbound-reply-dispatch
|
||||
769953c9e1c77348f831bb569911de0b390dbac0ff2cf6d3dda98bf6110373c1 module/inbound-reply-dispatch
|
||||
e5fdb21e7d557fb6830f9b7e9860bdd9b11f9ceb8c34e59b85e12a5ec5b022a3 module/infra-runtime
|
||||
ce73721421f1b903dd04ead4df173582e59ea3e9990248102c448b419cc6d272 module/ingress-effect-once
|
||||
31449afd7ea7f0c8dc7dee439a3693c3d1092af67c6bed67dd34e7b7b50bc74e module/interactive-runtime
|
||||
@@ -89,28 +89,28 @@ f74d7295fe716aa140aa0bc9300d6259d71dab826de0808fca6bb02592bf5d6e module/media-m
|
||||
6a52f93107335f88751704352cc01e62add06f854a5b7d765e2a5ee87c0313b6 module/media-store
|
||||
b3fdb9f96d2724d5824063f434f3e482b605e4c06cad9770a94c78079dc36454 module/media-understanding
|
||||
544a6d47a391e64574b146f649e8065b70a57955088b85cbdbdacb872889475a module/media-understanding-runtime
|
||||
ab5c34f7d4624db3d27230f9c2d1ca16f18009c728fbb781cd9e01847b708ac3 module/meeting-runtime
|
||||
4697b7324abf8e65f38411679252a392313aadd35ab8a1f85e32bf24e02dd264 module/meeting-runtime
|
||||
a6aac1a3f85d3ee7dc9fbc7a1c6ef7dd00a9c2d3bdb6dc8d57a9704a821f806d module/memory-core-host-engine-foundation
|
||||
fa378284ea3a9fa13d761c25ce0c08a0a476e5e91b8c08b010ea68eff48a3331 module/memory-host-core
|
||||
1487d03c089fb655f41e21404582a6f1b2d32674a9c78bf2d71f4a00e89d076d module/memory-host-core
|
||||
1efa0aadc4261d1c6073058cbf3dcc9fa681424819bdd14333e19b249bbc4b18 module/messaging-targets
|
||||
8647dda41c0c3ab67191c397aa405de914fd8f277f55f550a307524e3c20d98d module/model-session-runtime
|
||||
db4fd9d77cee593003f2ea0bab72445f6b56ab4ad27bed772d57684b7fa4b0b2 module/models-provider-runtime
|
||||
506824e153d9bfed0b77365242bd3f01349b36fbfb3ef9dcd336dc09d78b2271 module/models-provider-runtime
|
||||
504c61546d566814cda2d8126b458efc9ad70b75f1406ab9d6635ac10225200c module/native-command-config-runtime
|
||||
d808e6681668e70b2dbb2ac590b455072f98d54970593e25ef6ad5c2322d3a51 module/native-command-registry
|
||||
ca6ee4fa75f976d590210b9ff6dc66374bb829a05d4f70972eef9137f5548b88 module/param-readers
|
||||
ca7a56bb1a6169b4cf9befbf5aa21da280a8086fdc49fca4eec520a7a7c98549 module/persistent-dedupe
|
||||
3c73ab232d86b49ebb6e5f302da2a2a4b5c7f62bff5e4cc0d3505edc5ba4f5cd module/plugin-config-runtime
|
||||
37e9cb671d42994b6199e8e4acb352ff119c5efc84578e85a369a632874886d2 module/plugin-entry
|
||||
213270a4ab7c2eda7fbebbebdeb72b57a462ebe8e3db64cd2f7dd3a8dba10a9b module/plugin-runtime
|
||||
6edf5a15775f4074c29f9ee59d008c6d4ed8c08278fdc52cbf39d62631518436 module/provider-auth
|
||||
c770c088de0bed71deedce3d39cc13101563311f3e8eb9df1a96fe4e8064d14d module/provider-catalog-runtime
|
||||
3ba578b41a1ee1455b33d9ae3dac34a94d7b1d18d688e564a72192bdee334fb2 module/plugin-entry
|
||||
dc7545db470a9dab7efbf68775e4890066c7282383f5e502b786c9745d058634 module/plugin-runtime
|
||||
5559e83f4f274975bb80f62711497ae43d1853631fb9cca7b694589823437cc5 module/provider-auth
|
||||
3949cd3b99310a70f447974dec2cae97adb2845b6070fe9340cc3519888708fc module/provider-catalog-runtime
|
||||
8131147d699394bd06503e2ea2f5f1a50b1594a87dded6d118b74a8d0328c8f6 module/proxy-capture
|
||||
4949fe3958d3b92b8d2e13ec0af9e65c88e1c7d3e39e70c306fd78c93b094e87 module/question-gateway-runtime
|
||||
a479cd5c96a34c6f0a2ed4d4239aad63b56cb0e47a970848a141ad7fb67b9e11 module/reply-chunking
|
||||
a499123f36bf020850b5485a883e1f51c153996b8d6f6eee2ea6e4c2d657a169 module/reply-dispatch-runtime
|
||||
6d2f0aca30c64d03030b4896dfcbf49cebb549015a87cd5c701e2d6f68a7684d module/reply-dispatch-runtime
|
||||
73f861fa3179d5af1159853c5acab0eec7a6c8f9398dcb75ea770e784fca6727 module/reply-history
|
||||
d3bf7e4a7fcaebc1cf3173f4fbb60203e6ec46257cddcca7096800df946708fc module/reply-payload
|
||||
2ff868805e5485e6a4edc2a202c05655e058276601114dd3584ca7703f4356b1 module/reply-runtime
|
||||
90b30bca89c71a6718b0a6035112154e9b12fd5f399cc310ef21b57058db1be0 module/reply-runtime
|
||||
aa07d85d99fdd2b1e0cbe9975fb6dcae66b8bdce2607c6bd5402ae68bb15118c module/root-walk
|
||||
02c0e5cae6772159a1c5ff43209de542ac7976dc30541d406f9dc6c37141a69d module/routing
|
||||
7877a7e58fa32a64107154e5b714c6d165e96989d4aa5f43e0afac085a187af0 module/run-command
|
||||
@@ -118,20 +118,20 @@ bd15eb9689fd7070dc942cb4bd0db6c2c1d4fb1a971ce9d0a6349a5679309535 module/runtime
|
||||
ccb6aad96b4ea156738f45bac04b6eda5826e17b36eb8b5e26567f400b3be217 module/runtime-config-snapshot
|
||||
2e3c692a9f911ab227e34a9a3870f8e139f90ceaff631301b55ce712dec53736 module/runtime-env
|
||||
7e871b7319745678bb83fcfc1b54c8751b0ab1af92ff06c01d0659ac92863c11 module/runtime-group-policy
|
||||
92d0b99ff9b3bf582ae9ade7444c1612789421b703f6cf034dc3c8a6a201f676 module/runtime-store
|
||||
78653fea395459977bbd5a05ae2516b7bc4adad6ddbeebc79e5a31eb43d4fb58 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
|
||||
4a19480225c797ddd08cfd36ac13824b83d9274806a325576079a2da8b670e4b module/session-catalog
|
||||
84269e3f87fd50c45f65b9b91afe91e4f4f344d9e33a3b85e8b47204bcfd6f0d module/session-discussion
|
||||
ee7be6b059fdbe7c47ad7978f1b21773fe0b21afa91a98846f620229a4a6632a module/session-catalog
|
||||
4ce3c027bd609f06be69dfd36131950fbda7c7e548efa3e9ed49104cae9d29bc module/session-discussion
|
||||
fd2dcb08a59df7bb68c8f36f272d134f6904d12b9e192bc8d7a4858d3039b595 module/session-store-runtime
|
||||
73f4a776c027d974f010250360822694486a8033bf1b51e96a0ff2440b05dbfe module/setup
|
||||
f1ec91331ad72c3fd9edb2cb11b4c6a35acd55ef3e4f541c5907149eed21b15f module/setup-runtime
|
||||
44d37e0d9131ad2859f41068f2604090c784e65f1bd6ebda8e051b6f2e5e1660 module/setup-tools
|
||||
00e8794c6e7aabbdeec14189d885d4021f5118fcb08f7e34b85fafc473e62bed module/skill-commands-runtime
|
||||
4950be8dcfe5d73c2923657e1e07929b91933763fdc6eb085f30776d4808af25 module/speech-settings
|
||||
11e4798d4ffb4e7878809fef784eaff2344e876a066f5703e470689aae9031d7 module/speech-settings
|
||||
f01b661de86de0d0b1d3bff395330092f3fe1114f4516968ca5fca79cf7eac50 module/ssrf-policy
|
||||
0296f2c837f8116aa3a7e8b02d3de8b756d44e0a004842dbd24a5aa627f74a54 module/ssrf-runtime
|
||||
eace34246d7a827b00a67bde258f8401bab35f527dcad29e6f982e4ae39cf013 module/state-paths
|
||||
@@ -141,11 +141,11 @@ c6ea76a9fa7f56771cbfc54617aa9ceec3d87241bab38a094622ea9351f3cc87 module/telegra
|
||||
aef35bee2502cd6ed8765409b758e452aff8ac9469fd773e6a2a44c9a1bc3f66 module/temp-path
|
||||
87fa81b9e58d8fc04a4b4202d2d37fca339615f5225687d9db905151439e0f4d module/text-chunking
|
||||
2ce8b180da90b5b1665bc65dd9cae9af7eb4e0988cc2a64685e8e6f9cb9a87c3 module/text-runtime
|
||||
c77dac0fabc74abb2c1b177fa830078b0af3af8b6520d6026add7429e083ce28 module/tool-plugin
|
||||
bc507d97d5729da1f08b017dc5a63de5a7781da228a5ab58adfb437fd1fb07de module/tool-plugin
|
||||
dc1a073c59ab61e2789533b777b3f0cb9af689d64a97796b10e8aa82552510db module/tool-results
|
||||
3c97f778d2844ba1bfd3e77fbccd3bfed94bc102053c091b00a0d3d2aaff6a99 module/tool-send
|
||||
cda105b721d498df23a554c6b68be150b8fe66b8b9172185c31a0b3b0646b1dc module/web-media
|
||||
dc2a5888a40d4f372a0879224dd4c7c04064a0fcde94aea0173578954ed6c940 module/webhook-ingress
|
||||
fd738562fefb3e3ec67233982c1bf8404b5bbc4a2ab17741b900f438adf4a01b module/webhook-ingress
|
||||
216e54c25ec0985fc483899d4de3d2582053a4fc3a2d1c98a8b5e7cdd2f136e1 module/webhook-request-guards
|
||||
de59e86e126b75d13251cba7ebbe27b44d9b5588785d98df5ff4d6722374c81f module/widget-html
|
||||
9161b36ec0ab062ea41b363c894fcd672a7727f21cb726739f99f9c184fce69d module/zod
|
||||
|
||||
@@ -813,20 +813,6 @@ two-party event loops that do not go through the shared inbound reply runner.
|
||||
{ contentType: "text/plain" },
|
||||
);
|
||||
const blob = await blobs.lookup("artifact-1");
|
||||
|
||||
await api.runtime.state.withLease(
|
||||
{
|
||||
namespace: "my-feature",
|
||||
key: "writer",
|
||||
database: { scope: "agent", agentId },
|
||||
leaseMs: 5 * 60_000,
|
||||
waitMs: 30_000,
|
||||
},
|
||||
async ({ signal, assertOwned }) => {
|
||||
await runExternalWriter({ signal });
|
||||
assertOwned();
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
Keyed stores survive restarts and are isolated by the runtime-bound plugin id. Use `registerIfAbsent(...)` for atomic dedupe claims: it returns `true` when the key was missing or expired and registered, or `false` when a live value already exists without overwriting its value, creation time, or TTL. Use `deleteIf(...)` when cleanup must remove only the value previously observed; its synchronous predicate and deletion run in one SQLite transaction. Limits: `maxEntries` per namespace, 50,000 live rows per plugin, JSON values under 64KB, and optional TTL expiry. By default, a write at either row limit sheds the oldest live rows from the namespace being written; sibling namespaces are not evicted for that write, and the write still fails if the namespace cannot free enough rows. Set `overflowPolicy: "reject-new"` for durable ownership records that must never be evicted: new keys fail at either limit, while existing keys remain updateable.
|
||||
@@ -837,12 +823,12 @@ two-party event loops that do not go through the shared inbound reply runner.
|
||||
|
||||
`openChannelIngressQueue<TPayload>(...)` opens a persisted ingress queue scoped to the calling plugin, for buffering inbound events that need at-least-once processing across restarts. When stale-claim recovery uses `shouldRecover`, also provide `shouldRecoverCorrupt` if corrupt claimed payloads should be quarantined: its payload-independent claim identity lets the plugin preserve live owner and lane policy before the queue tombstones the row.
|
||||
|
||||
`withLease(...)` serializes cooperative plugin work across OpenClaw processes. Choose `database: { scope: "shared" }` for one global owner or `{ scope: "agent", agentId }` for independent per-agent ownership. Forward the callback's `AbortSignal` into every fallible operation. `assertOwned()` is a point-in-time checkpoint before starting another important step; the host also verifies ownership after the callback. Lease loss or caller cancellation aborts the signal. Acquisition waits and heartbeats happen outside short synchronous SQLite transactions; plugins never receive database paths or handles. This is cooperative cancellation, not a fencing token or authorization for unfenced external writes.
|
||||
Plugin-state leases were removed. Use short SQLite transactions for atomic database work and plugin-scoped keyed stores (`openKeyedStore` or `openSyncKeyedStore`) for bounded durable state.
|
||||
|
||||
`openChannelIngressDrain(...)` opens the core channel-agnostic worker over that queue (or creates a queue when none is supplied). The drain owns stale-claim recovery, per-lane claim serialization, complete-at-adoption or complete-on-dispatch-return, retry/dead-letter disposition, optional pre-adoption supersede, and claim→adoption stall timeout. Wire claim ownership into reply generation with `turnAdoptionLifecycle` (via `bindIngressLifecycleToReplyOptions` from `plugin-sdk/channel-outbound`). Channel plugins keep accept-side enqueue, lane derivation, non-retryable classification, and any supersede authorization policy.
|
||||
|
||||
<Warning>
|
||||
`openBlobStore`, `openKeyedStore`, `openSyncKeyedStore`, `withLease`, `openChannelIngressQueue`, and `openChannelIngressDrain` are available only to bundled plugins and trusted official plugin installations in this release. The rejection names the plugin id and the origin it loaded from; a channel plugin loaded from `plugins.load.paths` or an unofficial install is untrusted, so its ingress monitor fails channel start instead of running without a durable queue.
|
||||
`openBlobStore`, `openKeyedStore`, `openSyncKeyedStore`, `openChannelIngressQueue`, and `openChannelIngressDrain` are available only to bundled plugins and trusted official plugin installations in this release. The rejection names the plugin id and the origin it loaded from; a channel plugin loaded from `plugins.load.paths` or an unofficial install is untrusted, so its ingress monitor fails channel start instead of running without a durable queue.
|
||||
</Warning>
|
||||
|
||||
</Accordion>
|
||||
|
||||
@@ -257,7 +257,7 @@ Use `isLoopbackHost(host)` when a plugin must accept only the local machine. It
|
||||
| `plugin-sdk/sqlite-runtime` | Private-local after July 2026; Focused SQLite agent-schema, path, and transaction helpers for first-party runtime, without database lifecycle controls |
|
||||
| `plugin-sdk/cron-store-runtime` | Private-local after July 2026; Cron store path/load/save helpers |
|
||||
| `plugin-sdk/state-paths` | State/OAuth dir path helpers |
|
||||
| `plugin-sdk/plugin-state-runtime` | Private-local after July 2026; Plugin-scoped keyed-state, BLOB, and cooperative SQLite lease contracts plus connection pragma, verified WAL maintenance, and atomic STRICT-schema migration helpers. Lease callbacks receive an abort signal and typed errors distinguish timeout, cancellation, lost ownership, invalid input, and storage failure |
|
||||
| `plugin-sdk/plugin-state-runtime` | Private-local after July 2026; Plugin-scoped keyed-state and BLOB contracts plus connection pragma, verified WAL maintenance, and atomic STRICT-schema migration helpers. Plugin-state leases were removed; use SQLite transactions and keyed stores instead |
|
||||
| `plugin-sdk/routing` | Route/session-key/account binding helpers such as `resolveAgentRoute`, `buildAgentSessionKey`, and `resolveDefaultAgentBoundAccountId` |
|
||||
| `plugin-sdk/status-helpers` | Shared channel/account status summary helpers, runtime-state defaults, and issue metadata helpers |
|
||||
| `plugin-sdk/target-resolver-runtime` | Private-local after July 2026; Shared target resolver helpers |
|
||||
|
||||
@@ -13,9 +13,8 @@ vi.mock("./src/cli.js", () => ({
|
||||
import plugin from "./cli-metadata.js";
|
||||
|
||||
describe("memory-core CLI metadata", () => {
|
||||
it("passes SQLite state and lease hosts to the standalone CLI", async () => {
|
||||
it("passes the SQLite state host to the standalone CLI", async () => {
|
||||
let registrar: Parameters<OpenClawPluginApi["registerCli"]>[0] | undefined;
|
||||
const hostWithLease = vi.fn();
|
||||
const keyedStore = {};
|
||||
const openKeyedStore = vi.fn(() => keyedStore);
|
||||
const acquireLocalService = vi.fn(async () => undefined);
|
||||
@@ -23,7 +22,7 @@ describe("memory-core CLI metadata", () => {
|
||||
createTestPluginApi({
|
||||
runtime: {
|
||||
llm: { acquireLocalService },
|
||||
state: { openKeyedStore, withLease: hostWithLease },
|
||||
state: { openKeyedStore },
|
||||
} as unknown as OpenClawPluginApi["runtime"],
|
||||
registerCli(nextRegistrar) {
|
||||
registrar = nextRegistrar;
|
||||
@@ -40,7 +39,6 @@ describe("memory-core CLI metadata", () => {
|
||||
expect(registerMemoryCliMock).toHaveBeenCalledWith(program, {
|
||||
acquireLocalService,
|
||||
openKeyedStore: expect.any(Function),
|
||||
withLease: expect.any(Function),
|
||||
});
|
||||
const boundOpenKeyedStore = registerMemoryCliMock.mock.calls[0]?.[1]?.openKeyedStore as
|
||||
| ((options: unknown) => unknown)
|
||||
@@ -51,13 +49,5 @@ describe("memory-core CLI metadata", () => {
|
||||
const storeOptions = { namespace: "cli-status-regression", maxEntries: 1 };
|
||||
expect(boundOpenKeyedStore(storeOptions)).toBe(keyedStore);
|
||||
expect(openKeyedStore).toHaveBeenCalledWith(storeOptions);
|
||||
const withLease = registerMemoryCliMock.mock.calls[0]?.[1]?.withLease as
|
||||
| ((...args: unknown[]) => unknown)
|
||||
| undefined;
|
||||
if (!withLease) {
|
||||
throw new Error("bound lease hook missing");
|
||||
}
|
||||
withLease("options", "callback");
|
||||
expect(hostWithLease).toHaveBeenCalledWith("options", "callback");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,7 +14,6 @@ export default definePluginEntry({
|
||||
acquireLocalService: api.runtime.llm?.acquireLocalService,
|
||||
openKeyedStore: <T>(options: OpenKeyedStoreOptions) =>
|
||||
api.runtime.state.openKeyedStore<T>(options),
|
||||
withLease: api.runtime.state.withLease.bind(api.runtime.state),
|
||||
});
|
||||
},
|
||||
{
|
||||
|
||||
@@ -36,7 +36,6 @@ const hostRuntime = {
|
||||
acquireLocalService: async () => undefined,
|
||||
},
|
||||
state: {
|
||||
withLease: vi.fn(),
|
||||
openKeyedStore: vi.fn(() => ({
|
||||
lookup: vi.fn(),
|
||||
register: vi.fn(),
|
||||
@@ -224,16 +223,14 @@ describe("memory-core plugin runtime registration", () => {
|
||||
expect(createMemoryRuntimeMock).toHaveBeenCalledWith({
|
||||
acquireLocalService: expect.any(Function),
|
||||
openKeyedStore: expect.any(Function),
|
||||
withLease: expect.any(Function),
|
||||
});
|
||||
});
|
||||
|
||||
it("defers nested host runtime access until the injected operation runs", async () => {
|
||||
const acquireLocalService = vi.fn(async () => undefined);
|
||||
const openKeyedStore = vi.fn(() => ({}));
|
||||
const withLease = vi.fn(async (_options, run) => await run({}));
|
||||
const llmGetter = vi.fn(() => ({ acquireLocalService }));
|
||||
const stateGetter = vi.fn(() => ({ openKeyedStore, withLease }));
|
||||
const stateGetter = vi.fn(() => ({ openKeyedStore }));
|
||||
const host = Object.defineProperties(
|
||||
{},
|
||||
{
|
||||
@@ -256,11 +253,7 @@ describe("memory-core plugin runtime registration", () => {
|
||||
expect(stateGetter).not.toHaveBeenCalled();
|
||||
await runtime?.getMemorySearchManager({ cfg: {}, agentId: "main" });
|
||||
const injectedHost = createMemoryRuntimeMock.mock.calls.at(-1)?.[0];
|
||||
if (
|
||||
!injectedHost?.acquireLocalService ||
|
||||
!injectedHost.openKeyedStore ||
|
||||
!injectedHost.withLease
|
||||
) {
|
||||
if (!injectedHost?.acquireLocalService || !injectedHost.openKeyedStore) {
|
||||
throw new Error("expected memory-core host operations");
|
||||
}
|
||||
|
||||
@@ -268,21 +261,11 @@ describe("memory-core plugin runtime registration", () => {
|
||||
await injectedHost.acquireLocalService(target);
|
||||
const storeOptions = { namespace: "lazy-host", maxEntries: 1 };
|
||||
injectedHost.openKeyedStore(storeOptions);
|
||||
const run = vi.fn(async () => "leased");
|
||||
const leaseOptions = {
|
||||
namespace: "lazy-host",
|
||||
key: "manager",
|
||||
database: { scope: "shared" as const },
|
||||
leaseMs: 1_000,
|
||||
waitMs: 1_000,
|
||||
};
|
||||
await injectedHost.withLease(leaseOptions, run as never);
|
||||
|
||||
expect(llmGetter).toHaveBeenCalledOnce();
|
||||
expect(acquireLocalService).toHaveBeenCalledWith(target);
|
||||
expect(stateGetter).toHaveBeenCalledTimes(2);
|
||||
expect(stateGetter).toHaveBeenCalledOnce();
|
||||
expect(openKeyedStore).toHaveBeenCalledWith(storeOptions);
|
||||
expect(withLease).toHaveBeenCalledWith(leaseOptions, run);
|
||||
});
|
||||
|
||||
it("forwards search-hit authorization through the registered memory runtime", async () => {
|
||||
@@ -318,11 +301,10 @@ describe("memory-core plugin runtime registration", () => {
|
||||
expect(createMemoryRuntimeMock).toHaveBeenCalledWith({
|
||||
acquireLocalService: expect.any(Function),
|
||||
openKeyedStore: expect.any(Function),
|
||||
withLease: expect.any(Function),
|
||||
});
|
||||
});
|
||||
|
||||
it("binds the host SQLite state hooks to tools and CLI runtime", async () => {
|
||||
it("binds the host SQLite state hook to tools and CLI runtime", async () => {
|
||||
const runtime = registerMemoryCoreRuntime();
|
||||
const cfg = {} as OpenClawConfig;
|
||||
|
||||
@@ -332,7 +314,6 @@ describe("memory-core plugin runtime registration", () => {
|
||||
const storeOptions = { namespace: "cli-status-regression", maxEntries: 1 };
|
||||
host?.openKeyedStore?.(storeOptions);
|
||||
expect(hostRuntime.state.openKeyedStore).toHaveBeenCalledWith(storeOptions);
|
||||
expect(host?.withLease).toEqual(expect.any(Function));
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -13,10 +13,7 @@ import {
|
||||
type AnyAgentTool,
|
||||
type OpenClawPluginToolContext,
|
||||
} from "openclaw/plugin-sdk/plugin-entry";
|
||||
import type {
|
||||
OpenKeyedStoreOptions,
|
||||
PluginStateLeaseRunner,
|
||||
} from "openclaw/plugin-sdk/plugin-state-runtime";
|
||||
import type { OpenKeyedStoreOptions } from "openclaw/plugin-sdk/plugin-state-runtime";
|
||||
import type { TSchema } from "typebox";
|
||||
import { configureMemoryCoreDreamingState } from "./src/dreaming-state.js";
|
||||
import { registerShortTermPromotionDreaming } from "./src/dreaming.js";
|
||||
@@ -39,7 +36,6 @@ type MemoryToolOptions = {
|
||||
conversationRecall?: OpenClawPluginToolContext["conversationRecall"];
|
||||
activeProjectKeys?: readonly string[];
|
||||
acquireLocalService?: MemoryCoreAcquireLocalService;
|
||||
withLease?: PluginStateLeaseRunner;
|
||||
};
|
||||
|
||||
const loadMemoryToolsModule = createLazyRuntimeModule(() => import("./src/tools.js"));
|
||||
@@ -240,7 +236,6 @@ function resolveMemoryToolOptions(
|
||||
conversationRecall: ctx.conversationRecall,
|
||||
activeProjectKeys: ctx.activeProjectKeys,
|
||||
...(host.acquireLocalService ? { acquireLocalService: host.acquireLocalService } : {}),
|
||||
...(host.withLease ? { withLease: host.withLease } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -282,8 +277,7 @@ export default definePluginEntry({
|
||||
api.runtime.llm.acquireLocalService(...args);
|
||||
const openKeyedStore = <T>(options: OpenKeyedStoreOptions) =>
|
||||
api.runtime.state.openKeyedStore<T>(options);
|
||||
const withLease: PluginStateLeaseRunner = (...args) => api.runtime.state.withLease(...args);
|
||||
const host = { acquireLocalService, openKeyedStore, withLease } satisfies MemoryCoreRuntimeHost;
|
||||
const host = { acquireLocalService, openKeyedStore } satisfies MemoryCoreRuntimeHost;
|
||||
configureMemoryCoreDreamingState(openKeyedStore);
|
||||
const memoryRuntime = createLazyMemoryRuntime(host);
|
||||
registerShortTermPromotionDreaming(api);
|
||||
|
||||
@@ -3,7 +3,6 @@ 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 type { PluginStateLeaseRunner } from "openclaw/plugin-sdk/plugin-state-runtime";
|
||||
import { buildAgentSessionKey } from "openclaw/plugin-sdk/routing";
|
||||
import {
|
||||
defaultRuntime,
|
||||
@@ -159,7 +158,6 @@ async function withMemoryManagerForAgent(params: {
|
||||
agentId: string;
|
||||
purpose?: MemoryManagerPurpose;
|
||||
acquireLocalService?: MemoryCoreAcquireLocalService;
|
||||
withLease?: PluginStateLeaseRunner;
|
||||
run: (manager: MemoryManager) => Promise<void>;
|
||||
}): Promise<void> {
|
||||
const managerParams: Parameters<typeof getMemorySearchManager>[0] = {
|
||||
@@ -172,9 +170,6 @@ async function withMemoryManagerForAgent(params: {
|
||||
if (params.acquireLocalService) {
|
||||
managerParams.acquireLocalService = params.acquireLocalService;
|
||||
}
|
||||
if (params.withLease) {
|
||||
managerParams.withLease = params.withLease;
|
||||
}
|
||||
await withManager<MemoryManager>({
|
||||
getManager: () => getMemorySearchManager(managerParams),
|
||||
onMissing: (error) => defaultRuntime.log(error ?? "Memory search disabled."),
|
||||
@@ -193,7 +188,6 @@ export async function withMemoryCommand(params: {
|
||||
diagnosticsToStderr?: boolean;
|
||||
purpose?: MemoryManagerPurpose;
|
||||
acquireLocalService?: MemoryCoreAcquireLocalService;
|
||||
withLease?: PluginStateLeaseRunner;
|
||||
run: (context: { manager: MemoryManager; cfg: OpenClawConfig; agentId: string }) => Promise<void>;
|
||||
}): Promise<OpenClawConfig> {
|
||||
const { config: cfg, diagnostics } = await loadMemoryCommandConfig(
|
||||
@@ -210,7 +204,6 @@ export async function withMemoryCommand(params: {
|
||||
agentId,
|
||||
purpose: params.purpose,
|
||||
acquireLocalService: params.acquireLocalService,
|
||||
withLease: params.withLease,
|
||||
run: async (manager) => params.run({ manager, cfg, agentId }),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1557,21 +1557,6 @@ describe("memory cli", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("passes the host SQLite lease hook to CLI memory managers", async () => {
|
||||
const close = vi.fn(async () => {});
|
||||
mockManager({ search: vi.fn(async () => []), close });
|
||||
const withLease = vi.fn();
|
||||
|
||||
await runMemoryCli(["search", "hello"], { withLease });
|
||||
|
||||
expect(getMemorySearchManager).toHaveBeenCalledWith({
|
||||
cfg: {},
|
||||
agentId: "main",
|
||||
purpose: "cli",
|
||||
withLease,
|
||||
});
|
||||
});
|
||||
|
||||
it("accepts --query for memory search", async () => {
|
||||
const close = vi.fn(async () => {});
|
||||
const search = vi.fn(async () => []);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// Memory Core plugin module implements memory tool manager mock behavior.
|
||||
import type { MemorySource } from "openclaw/plugin-sdk/memory-core-host-engine-storage";
|
||||
import type { MemorySearchRuntimeDebug } from "openclaw/plugin-sdk/memory-core-host-runtime-files";
|
||||
import type { PluginStateLeaseRunner } from "openclaw/plugin-sdk/plugin-state-runtime";
|
||||
import { vi } from "vitest";
|
||||
import type { getMemorySearchManager } from "./tools.runtime.js";
|
||||
|
||||
@@ -29,7 +28,6 @@ type MemoryManagerParams = {
|
||||
agentId?: string;
|
||||
purpose?: string;
|
||||
acquireLocalService?: unknown;
|
||||
withLease?: PluginStateLeaseRunner;
|
||||
};
|
||||
|
||||
let workspaceDir = "/workspace";
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import type { PluginStateLeaseRunner } from "openclaw/plugin-sdk/plugin-state-runtime";
|
||||
import type { MemoryCoreOpenKeyedStore } from "../dreaming-state.js";
|
||||
import type { MemoryCoreAcquireLocalService } from "./embedding-local-service.js";
|
||||
|
||||
export type MemoryCoreRuntimeHost = {
|
||||
acquireLocalService?: MemoryCoreAcquireLocalService;
|
||||
openKeyedStore?: MemoryCoreOpenKeyedStore;
|
||||
withLease?: PluginStateLeaseRunner;
|
||||
};
|
||||
|
||||
@@ -3,7 +3,6 @@ import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
||||
import { createLazyRuntimeModule } from "openclaw/plugin-sdk/lazy-runtime";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/memory-core-host-engine-foundation";
|
||||
import type { MemorySearchManager } from "openclaw/plugin-sdk/memory-core-host-engine-storage";
|
||||
import type { PluginStateLeaseRunner } from "openclaw/plugin-sdk/plugin-state-runtime";
|
||||
import { normalizeAgentId } from "openclaw/plugin-sdk/routing";
|
||||
import type { MemoryCoreAcquireLocalService } from "./embedding-local-service.js";
|
||||
|
||||
@@ -16,7 +15,6 @@ type MemorySearchManagerParams = {
|
||||
agentId: string;
|
||||
purpose?: MemorySearchManagerPurpose;
|
||||
acquireLocalService?: MemoryCoreAcquireLocalService;
|
||||
withLease?: PluginStateLeaseRunner;
|
||||
};
|
||||
|
||||
type MemorySearchManagerResult = {
|
||||
|
||||
@@ -79,34 +79,6 @@ describe("memoryRuntime", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps SQLite lease coordination scoped to each runtime instance", async () => {
|
||||
const cfg = {} as OpenClawConfig;
|
||||
const firstLease = vi.fn();
|
||||
const secondLease = vi.fn();
|
||||
|
||||
await Promise.all([
|
||||
createMemoryRuntime({ withLease: firstLease }).getMemorySearchManager({
|
||||
cfg,
|
||||
agentId: "first",
|
||||
}),
|
||||
createMemoryRuntime({ withLease: secondLease }).getMemorySearchManager({
|
||||
cfg,
|
||||
agentId: "second",
|
||||
}),
|
||||
]);
|
||||
|
||||
expect(getMemorySearchManagerMock).toHaveBeenCalledWith({
|
||||
cfg,
|
||||
agentId: "first",
|
||||
withLease: firstLease,
|
||||
});
|
||||
expect(getMemorySearchManagerMock).toHaveBeenCalledWith({
|
||||
cfg,
|
||||
agentId: "second",
|
||||
withLease: secondLease,
|
||||
});
|
||||
});
|
||||
|
||||
it("binds the scoped state opener inside each lazy runtime instance", async () => {
|
||||
const cfg = {} as OpenClawConfig;
|
||||
const openKeyedStore = vi.fn();
|
||||
|
||||
@@ -19,7 +19,6 @@ export function createMemoryRuntime(host: MemoryCoreRuntimeHost = {}): MemoryPlu
|
||||
const { manager, debug, error } = await getMemorySearchManager({
|
||||
...params,
|
||||
...(host.acquireLocalService ? { acquireLocalService: host.acquireLocalService } : {}),
|
||||
...(host.withLease ? { withLease: host.withLease } : {}),
|
||||
});
|
||||
return {
|
||||
manager,
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
type AnyAgentTool,
|
||||
type OpenClawConfig,
|
||||
} from "openclaw/plugin-sdk/memory-core-host-runtime-core";
|
||||
import type { PluginStateLeaseRunner } from "openclaw/plugin-sdk/plugin-state-runtime";
|
||||
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import { Type } from "typebox";
|
||||
import type { MemoryCoreAcquireLocalService } from "./memory/embedding-local-service.js";
|
||||
@@ -24,7 +23,6 @@ type MemoryToolOptions = {
|
||||
sandboxed?: boolean;
|
||||
oneShotCliRun?: boolean;
|
||||
acquireLocalService?: MemoryCoreAcquireLocalService;
|
||||
withLease?: PluginStateLeaseRunner;
|
||||
};
|
||||
|
||||
export const loadMemoryToolRuntime = createLazyRuntimeModule(() => import("./tools.runtime.js"));
|
||||
@@ -64,7 +62,6 @@ export async function getMemoryManagerContextWithPurpose(params: {
|
||||
agentId: string;
|
||||
purpose?: "default" | "status" | "cli";
|
||||
acquireLocalService?: MemoryCoreAcquireLocalService;
|
||||
withLease?: PluginStateLeaseRunner;
|
||||
}): Promise<
|
||||
| {
|
||||
manager: NonNullable<MemorySearchManagerResult["manager"]>;
|
||||
@@ -81,7 +78,6 @@ export async function getMemoryManagerContextWithPurpose(params: {
|
||||
agentId: params.agentId,
|
||||
purpose: params.purpose,
|
||||
...(params.acquireLocalService ? { acquireLocalService: params.acquireLocalService } : {}),
|
||||
...(params.withLease ? { withLease: params.withLease } : {}),
|
||||
});
|
||||
return manager
|
||||
? {
|
||||
|
||||
@@ -238,23 +238,6 @@ describe("memory_search unavailable payloads", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("passes the host SQLite lease hook to tool memory managers", async () => {
|
||||
const withLease = vi.fn();
|
||||
const tool = createMemorySearchTool({
|
||||
config: asOpenClawConfig({
|
||||
agents: { list: [{ id: "main", default: true }] },
|
||||
}),
|
||||
withLease,
|
||||
});
|
||||
if (!tool) {
|
||||
throw new Error("tool missing");
|
||||
}
|
||||
|
||||
await tool.execute("sqlite-lease-hook", { query: "hello" });
|
||||
|
||||
expect(getMemorySearchManagerMockParams()).toEqual([expect.objectContaining({ withLease })]);
|
||||
});
|
||||
|
||||
it("returns explicit unavailable metadata for quota failures", async () => {
|
||||
setMemorySearchImpl(async () => {
|
||||
throw new Error("openai embeddings failed: 429 insufficient_quota");
|
||||
|
||||
@@ -26,7 +26,6 @@ import {
|
||||
resolveMemoryDeepDreamingConfig,
|
||||
} from "openclaw/plugin-sdk/memory-core-host-status";
|
||||
import type { OpenClawPluginToolContext } from "openclaw/plugin-sdk/plugin-entry";
|
||||
import type { PluginStateLeaseRunner } from "openclaw/plugin-sdk/plugin-state-runtime";
|
||||
import { asRecord } from "./dreaming-shared.js";
|
||||
import type { MemoryCoreAcquireLocalService } from "./memory/embedding-local-service.js";
|
||||
import {
|
||||
@@ -411,7 +410,6 @@ export function createMemorySearchTool(options: {
|
||||
conversationRecall?: OpenClawPluginToolContext["conversationRecall"];
|
||||
activeProjectKeys?: readonly string[];
|
||||
acquireLocalService?: MemoryCoreAcquireLocalService;
|
||||
withLease?: PluginStateLeaseRunner;
|
||||
}) {
|
||||
return createMemoryTool({
|
||||
options,
|
||||
@@ -505,7 +503,6 @@ export function createMemorySearchTool(options: {
|
||||
agentId,
|
||||
purpose: memoryManagerPurpose,
|
||||
acquireLocalService: options.acquireLocalService,
|
||||
withLease: options.withLease,
|
||||
}),
|
||||
);
|
||||
return { context };
|
||||
@@ -618,7 +615,6 @@ export function createMemorySearchTool(options: {
|
||||
agentId,
|
||||
purpose: memoryManagerPurpose,
|
||||
acquireLocalService: options.acquireLocalService,
|
||||
withLease: options.withLease,
|
||||
}),
|
||||
),
|
||||
);
|
||||
@@ -799,7 +795,6 @@ export function createMemoryGetTool(options: {
|
||||
agentSessionKey?: string;
|
||||
sandboxed?: boolean;
|
||||
acquireLocalService?: MemoryCoreAcquireLocalService;
|
||||
withLease?: PluginStateLeaseRunner;
|
||||
}) {
|
||||
return createMemoryTool({
|
||||
options,
|
||||
|
||||
@@ -1015,7 +1015,7 @@ describe("createBackupArchive", () => {
|
||||
INSERT INTO state_leases (
|
||||
scope, lease_key, owner, expires_at, heartbeat_at,
|
||||
payload_json, created_at, updated_at
|
||||
) VALUES ('plugin:memory-core:qmd', 'embed', 'worker', 9999999999999, 10, NULL, 10, 10)
|
||||
) VALUES ('core:test-fixture', 'write', 'worker', 9999999999999, 10, NULL, 10, 10)
|
||||
`,
|
||||
).run();
|
||||
|
||||
@@ -1199,7 +1199,7 @@ describe("createBackupArchive", () => {
|
||||
INSERT INTO state_leases (
|
||||
scope, lease_key, owner, expires_at, heartbeat_at,
|
||||
payload_json, created_at, updated_at
|
||||
) VALUES ('plugin:memory-core:qmd', 'write', 'worker', 9999999999999, 1, NULL, 1, 1)
|
||||
) VALUES ('core:test-fixture', 'write', 'worker', 9999999999999, 1, NULL, 1, 1)
|
||||
`,
|
||||
)
|
||||
.run();
|
||||
@@ -2407,7 +2407,7 @@ describe("createBackupArchive", () => {
|
||||
PRAGMA user_version = 1;
|
||||
PRAGMA wal_checkpoint(TRUNCATE);
|
||||
INSERT INTO durable_state (id, value) VALUES (1, 'committed-in-wal');
|
||||
INSERT INTO state_leases (scope, lease_key) VALUES ('plugin:memory-core:qmd', 'write');
|
||||
INSERT INTO state_leases (scope, lease_key) VALUES ('core:test-fixture', 'write');
|
||||
`);
|
||||
await fs.symlink(backingDbPath, linkedDbPath);
|
||||
await fs.link(backingDbPath, hardlinkedDbPath);
|
||||
|
||||
@@ -37,11 +37,3 @@ export type {
|
||||
PluginBlobEntryInfo,
|
||||
PluginBlobStore,
|
||||
} from "../plugin-state/plugin-blob-store.js";
|
||||
export {
|
||||
PluginStateLeaseError,
|
||||
type PluginStateLeaseContext,
|
||||
type PluginStateLeaseDatabase,
|
||||
type PluginStateLeaseErrorCode,
|
||||
type PluginStateLeaseOptions,
|
||||
type PluginStateLeaseRunner,
|
||||
} from "../plugin-state/plugin-state-lease.types.js";
|
||||
|
||||
@@ -965,10 +965,6 @@ export function createPluginRuntimeMock(overrides: DeepPartial<PluginRuntime> =
|
||||
openSyncKeyedStore: vi.fn(() => {
|
||||
throw new Error("openSyncKeyedStore mock is not configured");
|
||||
}) as unknown as PluginRuntime["state"]["openSyncKeyedStore"],
|
||||
withLease: vi.fn(
|
||||
async (_options, run) =>
|
||||
await run({ signal: new AbortController().signal, assertOwned: vi.fn() }),
|
||||
),
|
||||
openChannelIngressQueue: vi.fn(() => {
|
||||
throw new Error("openChannelIngressQueue mock is not configured");
|
||||
}) as unknown as PluginRuntime["state"]["openChannelIngressQueue"],
|
||||
|
||||
@@ -1,682 +0,0 @@
|
||||
import fs from "node:fs/promises";
|
||||
import type { DatabaseSync } from "node:sqlite";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { requireNodeSqlite } from "../infra/node-sqlite.js";
|
||||
import { MAX_TIMER_TIMEOUT_MS } from "../shared/number-coercion.js";
|
||||
import { openOpenClawAgentDatabase } from "../state/openclaw-agent-db.js";
|
||||
import { closeOpenClawAgentDatabasesForTest } from "../state/openclaw-agent-db.js";
|
||||
import {
|
||||
closeOpenClawStateDatabaseForTest,
|
||||
openOpenClawStateDatabase,
|
||||
} from "../state/openclaw-state-db.js";
|
||||
import { withOpenClawTestState } from "../test-utils/openclaw-test-state.js";
|
||||
import { withPluginStateLease } from "./plugin-state-lease.js";
|
||||
|
||||
function deferred<T = void>() {
|
||||
let resolve!: (value: T | PromiseLike<T>) => void;
|
||||
const promise = new Promise<T>((next) => {
|
||||
resolve = next;
|
||||
});
|
||||
return { promise, resolve };
|
||||
}
|
||||
|
||||
function abortReason(signal: AbortSignal): Error {
|
||||
return signal.reason instanceof Error
|
||||
? signal.reason
|
||||
: new Error("lease aborted", { cause: signal.reason });
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
closeOpenClawAgentDatabasesForTest();
|
||||
closeOpenClawStateDatabaseForTest();
|
||||
});
|
||||
|
||||
describe("plugin state SQLite leases", () => {
|
||||
it("places shared and agent leases in their canonical databases", async () => {
|
||||
await withOpenClawTestState({ label: "plugin-lease-placement" }, async (state) => {
|
||||
const sharedEntered = deferred();
|
||||
const releaseShared = deferred();
|
||||
const shared = withPluginStateLease(
|
||||
"memory-core",
|
||||
{
|
||||
namespace: "qmd",
|
||||
key: "embed",
|
||||
database: { scope: "shared" },
|
||||
leaseMs: 1_000,
|
||||
waitMs: 0,
|
||||
},
|
||||
async () => {
|
||||
sharedEntered.resolve();
|
||||
await releaseShared.promise;
|
||||
},
|
||||
);
|
||||
await sharedEntered.promise;
|
||||
expect(
|
||||
openOpenClawStateDatabase({ env: state.env })
|
||||
.db.prepare("SELECT scope, lease_key FROM state_leases")
|
||||
.all(),
|
||||
).toEqual([{ scope: "plugin:memory-core:qmd", lease_key: "embed" }]);
|
||||
releaseShared.resolve();
|
||||
await shared;
|
||||
|
||||
const agentEntered = deferred();
|
||||
const releaseAgent = deferred();
|
||||
const agent = withPluginStateLease(
|
||||
"memory-core",
|
||||
{
|
||||
namespace: "qmd",
|
||||
key: "write",
|
||||
database: { scope: "agent", agentId: "main" },
|
||||
leaseMs: 1_000,
|
||||
waitMs: 0,
|
||||
},
|
||||
async () => {
|
||||
agentEntered.resolve();
|
||||
await releaseAgent.promise;
|
||||
},
|
||||
);
|
||||
await agentEntered.promise;
|
||||
expect(
|
||||
openOpenClawAgentDatabase({ agentId: "main", env: state.env })
|
||||
.db.prepare("SELECT scope, lease_key FROM state_leases")
|
||||
.all(),
|
||||
).toEqual([{ scope: "plugin:memory-core:qmd", lease_key: "write" }]);
|
||||
expect(
|
||||
openOpenClawStateDatabase({ env: state.env })
|
||||
.db.prepare("SELECT scope, lease_key FROM state_leases")
|
||||
.all(),
|
||||
).toEqual([]);
|
||||
releaseAgent.resolve();
|
||||
await agent;
|
||||
});
|
||||
});
|
||||
|
||||
it("serializes contenders and times out without entering the callback", async () => {
|
||||
await withOpenClawTestState({ label: "plugin-lease-contenders" }, async () => {
|
||||
const firstEntered = deferred();
|
||||
const releaseFirst = deferred();
|
||||
const first = withPluginStateLease(
|
||||
"memory-core",
|
||||
{
|
||||
namespace: "qmd",
|
||||
key: "embed",
|
||||
database: { scope: "shared" },
|
||||
leaseMs: 2_000,
|
||||
waitMs: 0,
|
||||
},
|
||||
async () => {
|
||||
firstEntered.resolve();
|
||||
await releaseFirst.promise;
|
||||
},
|
||||
);
|
||||
await firstEntered.promise;
|
||||
|
||||
const timedOutCallback = vi.fn(async () => undefined);
|
||||
await expect(
|
||||
withPluginStateLease(
|
||||
"memory-core",
|
||||
{
|
||||
namespace: "qmd",
|
||||
key: "embed",
|
||||
database: { scope: "shared" },
|
||||
leaseMs: 2_000,
|
||||
waitMs: 0,
|
||||
},
|
||||
timedOutCallback,
|
||||
),
|
||||
).rejects.toMatchObject({ code: "PLUGIN_STATE_LEASE_TIMEOUT" });
|
||||
expect(timedOutCallback).not.toHaveBeenCalled();
|
||||
|
||||
const waitAbort = new AbortController();
|
||||
const abortedCallback = vi.fn(async () => undefined);
|
||||
const abortedWait = withPluginStateLease(
|
||||
"memory-core",
|
||||
{
|
||||
namespace: "qmd",
|
||||
key: "embed",
|
||||
database: { scope: "shared" },
|
||||
leaseMs: 2_000,
|
||||
waitMs: 2_000,
|
||||
signal: waitAbort.signal,
|
||||
},
|
||||
abortedCallback,
|
||||
);
|
||||
const aborted = expect(abortedWait).rejects.toMatchObject({
|
||||
code: "PLUGIN_STATE_LEASE_ABORTED",
|
||||
});
|
||||
waitAbort.abort(new Error("stop waiting"));
|
||||
await aborted;
|
||||
expect(abortedCallback).not.toHaveBeenCalled();
|
||||
|
||||
const secondEntered = deferred();
|
||||
const second = withPluginStateLease(
|
||||
"memory-core",
|
||||
{
|
||||
namespace: "qmd",
|
||||
key: "embed",
|
||||
database: { scope: "shared" },
|
||||
leaseMs: 2_000,
|
||||
waitMs: 2_000,
|
||||
},
|
||||
async () => {
|
||||
secondEntered.resolve();
|
||||
},
|
||||
);
|
||||
let entered = false;
|
||||
void secondEntered.promise.then(() => {
|
||||
entered = true;
|
||||
});
|
||||
await Promise.resolve();
|
||||
expect(entered).toBe(false);
|
||||
releaseFirst.resolve();
|
||||
await first;
|
||||
await second;
|
||||
expect(entered).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it("does not enter the callback after acquisition expires", async () => {
|
||||
await withOpenClawTestState({ label: "plugin-lease-expired-entry" }, async (state) => {
|
||||
const database = openOpenClawStateDatabase({ env: state.env }).db;
|
||||
const callback = vi.fn(async () => undefined);
|
||||
const now = vi.spyOn(Date, "now").mockImplementation(() => {
|
||||
const acquired = database
|
||||
.prepare("SELECT 1 FROM state_leases WHERE lease_key = 'embed'")
|
||||
.get();
|
||||
// Model a wall-clock jump immediately after the acquisition row is
|
||||
// committed but before the callback can enter.
|
||||
return acquired ? 11_001 : 10_000;
|
||||
});
|
||||
try {
|
||||
await expect(
|
||||
withPluginStateLease(
|
||||
"memory-core",
|
||||
{
|
||||
namespace: "qmd",
|
||||
key: "embed",
|
||||
database: { scope: "shared" },
|
||||
leaseMs: 1_000,
|
||||
waitMs: 0,
|
||||
},
|
||||
callback,
|
||||
),
|
||||
).rejects.toMatchObject({ code: "PLUGIN_STATE_LEASE_LOST" });
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
now.mockRestore();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("does not serialize different per-agent databases", async () => {
|
||||
await withOpenClawTestState({ label: "plugin-lease-agents" }, async () => {
|
||||
const release = deferred();
|
||||
const entered: string[] = [];
|
||||
const runs = ["main", "research"].map((agentId) =>
|
||||
withPluginStateLease(
|
||||
"memory-core",
|
||||
{
|
||||
namespace: "qmd",
|
||||
key: "write",
|
||||
database: { scope: "agent", agentId },
|
||||
leaseMs: 1_000,
|
||||
waitMs: 0,
|
||||
},
|
||||
async () => {
|
||||
entered.push(agentId);
|
||||
if (entered.length === 2) {
|
||||
release.resolve();
|
||||
}
|
||||
await release.promise;
|
||||
},
|
||||
),
|
||||
);
|
||||
await Promise.all(runs);
|
||||
expect(entered.toSorted()).toEqual(["main", "research"]);
|
||||
});
|
||||
});
|
||||
|
||||
it("aborts the critical section when ownership is replaced", async () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(10_000);
|
||||
await withOpenClawTestState({ label: "plugin-lease-loss" }, async (state) => {
|
||||
const entered = deferred();
|
||||
const run = withPluginStateLease(
|
||||
"memory-core",
|
||||
{
|
||||
namespace: "qmd",
|
||||
key: "embed",
|
||||
database: { scope: "shared" },
|
||||
leaseMs: 1_000,
|
||||
waitMs: 0,
|
||||
},
|
||||
async ({ signal }) => {
|
||||
entered.resolve();
|
||||
await new Promise<void>((_resolve, reject) => {
|
||||
signal.addEventListener("abort", () => reject(abortReason(signal)), { once: true });
|
||||
});
|
||||
},
|
||||
);
|
||||
const lost = expect(run).rejects.toMatchObject({ code: "PLUGIN_STATE_LEASE_LOST" });
|
||||
await entered.promise;
|
||||
openOpenClawStateDatabase({ env: state.env })
|
||||
.db.prepare(
|
||||
`UPDATE state_leases
|
||||
SET owner = 'successor', expires_at = ?, updated_at = ?
|
||||
WHERE scope = 'plugin:memory-core:qmd' AND lease_key = 'embed'`,
|
||||
)
|
||||
.run(20_000, 10_100);
|
||||
await vi.advanceTimersByTimeAsync(334);
|
||||
await lost;
|
||||
expect(
|
||||
openOpenClawStateDatabase({ env: state.env })
|
||||
.db.prepare("SELECT owner FROM state_leases WHERE lease_key = 'embed'")
|
||||
.get(),
|
||||
).toEqual({ owner: "successor" });
|
||||
|
||||
openOpenClawStateDatabase({ env: state.env })
|
||||
.db.prepare("DELETE FROM state_leases WHERE lease_key = 'embed'")
|
||||
.run();
|
||||
const ignoredSignalEntered = deferred();
|
||||
const finishIgnoringSignal = deferred();
|
||||
const ignoresSignal = withPluginStateLease(
|
||||
"memory-core",
|
||||
{
|
||||
namespace: "qmd",
|
||||
key: "embed",
|
||||
database: { scope: "shared" },
|
||||
leaseMs: 1_000,
|
||||
waitMs: 0,
|
||||
},
|
||||
async () => {
|
||||
ignoredSignalEntered.resolve();
|
||||
await finishIgnoringSignal.promise;
|
||||
return "must-not-succeed";
|
||||
},
|
||||
);
|
||||
const ignoredLost = expect(ignoresSignal).rejects.toMatchObject({
|
||||
code: "PLUGIN_STATE_LEASE_LOST",
|
||||
});
|
||||
await ignoredSignalEntered.promise;
|
||||
openOpenClawStateDatabase({ env: state.env })
|
||||
.db.prepare(
|
||||
`UPDATE state_leases
|
||||
SET owner = 'second-successor', expires_at = ?, updated_at = ?
|
||||
WHERE scope = 'plugin:memory-core:qmd' AND lease_key = 'embed'`,
|
||||
)
|
||||
.run(30_000, 10_500);
|
||||
await vi.advanceTimersByTimeAsync(334);
|
||||
finishIgnoringSignal.resolve();
|
||||
await ignoredLost;
|
||||
expect(
|
||||
openOpenClawStateDatabase({ env: state.env })
|
||||
.db.prepare("SELECT owner FROM state_leases WHERE lease_key = 'embed'")
|
||||
.get(),
|
||||
).toEqual({ owner: "second-successor" });
|
||||
});
|
||||
});
|
||||
|
||||
it("renews ownership and preserves callback failures", async () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(30_000);
|
||||
await withOpenClawTestState({ label: "plugin-lease-renewal" }, async (state) => {
|
||||
const entered = deferred();
|
||||
const release = deferred();
|
||||
const run = withPluginStateLease(
|
||||
"memory-core",
|
||||
{
|
||||
namespace: "qmd",
|
||||
key: "embed",
|
||||
database: { scope: "shared" },
|
||||
leaseMs: 1_000,
|
||||
waitMs: 0,
|
||||
},
|
||||
async () => {
|
||||
entered.resolve();
|
||||
await release.promise;
|
||||
},
|
||||
);
|
||||
await entered.promise;
|
||||
const database = openOpenClawStateDatabase({ env: state.env }).db;
|
||||
expect(database.prepare("SELECT expires_at FROM state_leases").get()).toEqual({
|
||||
expires_at: 31_000,
|
||||
});
|
||||
await vi.advanceTimersByTimeAsync(334);
|
||||
expect(
|
||||
(database.prepare("SELECT expires_at FROM state_leases").get() as { expires_at: number })
|
||||
.expires_at,
|
||||
).toBeGreaterThan(31_000);
|
||||
await vi.advanceTimersByTimeAsync(800);
|
||||
expect(
|
||||
(database.prepare("SELECT expires_at FROM state_leases").get() as { expires_at: number })
|
||||
.expires_at,
|
||||
).toBeGreaterThan(Date.now());
|
||||
release.resolve();
|
||||
await run;
|
||||
expect(database.prepare("SELECT owner FROM state_leases").get()).toBeUndefined();
|
||||
|
||||
const callbackError = new Error("qmd failed");
|
||||
await expect(
|
||||
withPluginStateLease(
|
||||
"memory-core",
|
||||
{
|
||||
namespace: "qmd",
|
||||
key: "embed",
|
||||
database: { scope: "shared" },
|
||||
leaseMs: 1_000,
|
||||
waitMs: 0,
|
||||
},
|
||||
async () => {
|
||||
throw callbackError;
|
||||
},
|
||||
),
|
||||
).rejects.toBe(callbackError);
|
||||
expect(database.prepare("SELECT owner FROM state_leases").get()).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
it("retries contended cleanup without replacing the stable timeout outcome", async () => {
|
||||
await withOpenClawTestState({ label: "plugin-lease-release-retry" }, async () => {
|
||||
const opened = openOpenClawStateDatabase();
|
||||
let blocker: DatabaseSync | undefined;
|
||||
let unblockTimer: ReturnType<typeof setTimeout> | undefined;
|
||||
const callback = vi.fn(async () => undefined);
|
||||
const realNow = performance.now.bind(performance);
|
||||
const realStartedAt = realNow();
|
||||
let nowCalls = 0;
|
||||
const nowSpy = vi.spyOn(performance, "now").mockImplementation(() => {
|
||||
nowCalls += 1;
|
||||
if (nowCalls === 1) {
|
||||
return 0;
|
||||
}
|
||||
if (nowCalls === 2) {
|
||||
const sqlite = requireNodeSqlite();
|
||||
blocker = new sqlite.DatabaseSync(opened.path);
|
||||
blocker.exec("PRAGMA busy_timeout = 0; BEGIN IMMEDIATE");
|
||||
unblockTimer = setTimeout(() => {
|
||||
blocker?.exec("ROLLBACK");
|
||||
blocker?.close();
|
||||
}, 50);
|
||||
return 2;
|
||||
}
|
||||
return 2 + (realNow() - realStartedAt);
|
||||
});
|
||||
try {
|
||||
await expect(
|
||||
withPluginStateLease(
|
||||
"memory-core",
|
||||
{
|
||||
namespace: "qmd",
|
||||
key: "embed",
|
||||
database: { scope: "shared" },
|
||||
leaseMs: 60_000,
|
||||
waitMs: 1,
|
||||
},
|
||||
callback,
|
||||
),
|
||||
).rejects.toMatchObject({ code: "PLUGIN_STATE_LEASE_TIMEOUT" });
|
||||
} finally {
|
||||
nowSpy.mockRestore();
|
||||
if (unblockTimer) {
|
||||
clearTimeout(unblockTimer);
|
||||
}
|
||||
if (blocker?.isOpen) {
|
||||
if (blocker.isTransaction) {
|
||||
blocker.exec("ROLLBACK");
|
||||
}
|
||||
blocker.close();
|
||||
}
|
||||
}
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
expect(opened.db.prepare("SELECT owner FROM state_leases").get()).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
it("reclaims expired rows and aborts an active callback on caller cancellation", async () => {
|
||||
await withOpenClawTestState({ label: "plugin-lease-expiry-abort" }, async (state) => {
|
||||
const now = Date.now();
|
||||
openOpenClawStateDatabase({ env: state.env })
|
||||
.db.prepare(
|
||||
`INSERT INTO state_leases
|
||||
(scope, lease_key, owner, expires_at, heartbeat_at, payload_json, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, NULL, ?, ?)`,
|
||||
)
|
||||
.run("plugin:memory-core:qmd", "embed", "expired", now - 1, now - 10, now - 10, now - 10);
|
||||
await expect(
|
||||
withPluginStateLease(
|
||||
"memory-core",
|
||||
{
|
||||
namespace: "qmd",
|
||||
key: "embed",
|
||||
database: { scope: "shared" },
|
||||
leaseMs: 1_000,
|
||||
waitMs: 0,
|
||||
},
|
||||
async () => "reclaimed",
|
||||
),
|
||||
).resolves.toBe("reclaimed");
|
||||
|
||||
const controller = new AbortController();
|
||||
const entered = deferred();
|
||||
const run = withPluginStateLease(
|
||||
"memory-core",
|
||||
{
|
||||
namespace: "qmd",
|
||||
key: "embed",
|
||||
database: { scope: "shared" },
|
||||
leaseMs: 1_000,
|
||||
waitMs: 0,
|
||||
signal: controller.signal,
|
||||
},
|
||||
async ({ signal }) => {
|
||||
entered.resolve();
|
||||
await new Promise<void>((_resolve, reject) => {
|
||||
signal.addEventListener("abort", () => reject(abortReason(signal)), { once: true });
|
||||
});
|
||||
},
|
||||
);
|
||||
const aborted = expect(run).rejects.toMatchObject({ code: "PLUGIN_STATE_LEASE_ABORTED" });
|
||||
await entered.promise;
|
||||
controller.abort(new Error("cancelled"));
|
||||
await aborted;
|
||||
expect(
|
||||
openOpenClawStateDatabase({ env: state.env })
|
||||
.db.prepare("SELECT owner FROM state_leases WHERE lease_key = 'embed'")
|
||||
.get(),
|
||||
).toBeUndefined();
|
||||
|
||||
const checkpointController = new AbortController();
|
||||
const checkpointEntered = deferred();
|
||||
const reachCheckpoint = deferred();
|
||||
const checkpointRun = withPluginStateLease(
|
||||
"memory-core",
|
||||
{
|
||||
namespace: "qmd",
|
||||
key: "embed",
|
||||
database: { scope: "shared" },
|
||||
leaseMs: 1_000,
|
||||
waitMs: 0,
|
||||
signal: checkpointController.signal,
|
||||
},
|
||||
async (lease) => {
|
||||
checkpointEntered.resolve();
|
||||
await reachCheckpoint.promise;
|
||||
lease.assertOwned();
|
||||
},
|
||||
);
|
||||
const checkpointAborted = expect(checkpointRun).rejects.toMatchObject({
|
||||
code: "PLUGIN_STATE_LEASE_ABORTED",
|
||||
});
|
||||
await checkpointEntered.promise;
|
||||
checkpointController.abort(new Error("cancel before marker"));
|
||||
reachCheckpoint.resolve();
|
||||
await checkpointAborted;
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects invalid inputs and pre-aborted acquisition", async () => {
|
||||
await withOpenClawTestState({ label: "plugin-lease-validation" }, async () => {
|
||||
await expect(
|
||||
withPluginStateLease(
|
||||
"memory-core",
|
||||
{
|
||||
namespace: "qmd",
|
||||
key: "embed",
|
||||
database: { scope: "shared" },
|
||||
leaseMs: 999,
|
||||
waitMs: 0,
|
||||
},
|
||||
async () => undefined,
|
||||
),
|
||||
).rejects.toMatchObject({ code: "PLUGIN_STATE_LEASE_INVALID_INPUT" });
|
||||
|
||||
await expect(
|
||||
withPluginStateLease(
|
||||
"memory-core",
|
||||
{
|
||||
namespace: "qmd",
|
||||
key: "embed",
|
||||
database: undefined as never,
|
||||
leaseMs: 1_000,
|
||||
waitMs: 0,
|
||||
},
|
||||
async () => undefined,
|
||||
),
|
||||
).rejects.toMatchObject({ code: "PLUGIN_STATE_LEASE_INVALID_INPUT" });
|
||||
|
||||
await expect(
|
||||
withPluginStateLease(
|
||||
"memory-core",
|
||||
{
|
||||
namespace: "qmd",
|
||||
key: "embed",
|
||||
database: { scope: "agent", agentId: 42 } as never,
|
||||
leaseMs: 1_000,
|
||||
waitMs: 0,
|
||||
},
|
||||
async () => undefined,
|
||||
),
|
||||
).rejects.toMatchObject({ code: "PLUGIN_STATE_LEASE_INVALID_INPUT" });
|
||||
|
||||
const controller = new AbortController();
|
||||
controller.abort(new Error("stop"));
|
||||
await expect(
|
||||
withPluginStateLease(
|
||||
"memory-core",
|
||||
{
|
||||
namespace: "qmd",
|
||||
key: "embed",
|
||||
database: { scope: "shared" },
|
||||
leaseMs: 1_000,
|
||||
waitMs: 0,
|
||||
signal: controller.signal,
|
||||
},
|
||||
async () => undefined,
|
||||
),
|
||||
).rejects.toMatchObject({ code: "PLUGIN_STATE_LEASE_ABORTED" });
|
||||
|
||||
await expect(
|
||||
withPluginStateLease(
|
||||
"memory-core",
|
||||
{
|
||||
namespace: "qmd",
|
||||
key: "max-duration",
|
||||
database: { scope: "shared" },
|
||||
leaseMs: MAX_TIMER_TIMEOUT_MS,
|
||||
waitMs: MAX_TIMER_TIMEOUT_MS,
|
||||
},
|
||||
async (lease) => {
|
||||
lease.assertOwned();
|
||||
return "ok";
|
||||
},
|
||||
),
|
||||
).resolves.toBe("ok");
|
||||
});
|
||||
});
|
||||
|
||||
it("bounds SQLite transaction admission by waitMs", async () => {
|
||||
await withOpenClawTestState({ label: "plugin-lease-admission-timeout" }, async () => {
|
||||
const opened = openOpenClawStateDatabase();
|
||||
const sqlite = requireNodeSqlite();
|
||||
const blocker = new sqlite.DatabaseSync(opened.path);
|
||||
blocker.exec("PRAGMA busy_timeout = 0; BEGIN IMMEDIATE");
|
||||
const callback = vi.fn(async () => undefined);
|
||||
const startedAt = performance.now();
|
||||
try {
|
||||
await expect(
|
||||
withPluginStateLease(
|
||||
"memory-core",
|
||||
{
|
||||
namespace: "qmd",
|
||||
key: "embed",
|
||||
database: { scope: "shared" },
|
||||
leaseMs: 1_000,
|
||||
waitMs: 0,
|
||||
},
|
||||
callback,
|
||||
),
|
||||
).rejects.toMatchObject({ code: "PLUGIN_STATE_LEASE_TIMEOUT" });
|
||||
} finally {
|
||||
blocker.exec("ROLLBACK");
|
||||
blocker.close();
|
||||
}
|
||||
expect(performance.now() - startedAt).toBeLessThan(1_000);
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
expect(opened.db.prepare("PRAGMA busy_timeout").get()).toEqual({ timeout: 5_000 });
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps ownership checks nonblocking during SQLite contention", async () => {
|
||||
await withOpenClawTestState({ label: "plugin-lease-nonblocking-ownership" }, async () => {
|
||||
const opened = openOpenClawStateDatabase();
|
||||
await expect(
|
||||
withPluginStateLease(
|
||||
"memory-core",
|
||||
{
|
||||
namespace: "qmd",
|
||||
key: "embed",
|
||||
database: { scope: "shared" },
|
||||
leaseMs: 1_000,
|
||||
waitMs: 0,
|
||||
},
|
||||
async (lease) => {
|
||||
const sqlite = requireNodeSqlite();
|
||||
const blocker = new sqlite.DatabaseSync(opened.path);
|
||||
blocker.exec("PRAGMA busy_timeout = 0; BEGIN IMMEDIATE");
|
||||
const startedAt = performance.now();
|
||||
try {
|
||||
expect(() => lease.assertOwned()).not.toThrow();
|
||||
} finally {
|
||||
blocker.exec("ROLLBACK");
|
||||
blocker.close();
|
||||
}
|
||||
expect(performance.now() - startedAt).toBeLessThan(1_000);
|
||||
expect(opened.db.prepare("PRAGMA busy_timeout").get()).toEqual({ timeout: 5_000 });
|
||||
},
|
||||
),
|
||||
).resolves.toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
it("maps database open failures to the stable storage error", async () => {
|
||||
await withOpenClawTestState({ label: "plugin-lease-storage-error" }, async (state) => {
|
||||
const blockedStatePath = state.path("not-a-directory");
|
||||
await fs.writeFile(blockedStatePath, "blocked", "utf8");
|
||||
process.env.OPENCLAW_STATE_DIR = blockedStatePath;
|
||||
await expect(
|
||||
withPluginStateLease(
|
||||
"memory-core",
|
||||
{
|
||||
namespace: "qmd",
|
||||
key: "embed",
|
||||
database: { scope: "shared" },
|
||||
leaseMs: 1_000,
|
||||
waitMs: 0,
|
||||
},
|
||||
async () => undefined,
|
||||
),
|
||||
).rejects.toMatchObject({ code: "PLUGIN_STATE_LEASE_STORAGE_FAILED" });
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,177 +0,0 @@
|
||||
// Plugin validation and public errors wrap the host-owned SQLite lease engine.
|
||||
import { MAX_TIMER_TIMEOUT_MS } from "../shared/number-coercion.js";
|
||||
import {
|
||||
OpenClawStateLeaseError,
|
||||
type OpenClawStateLeaseErrorCode,
|
||||
withOpenClawStateLease,
|
||||
} from "../state/openclaw-state-lease.js";
|
||||
import {
|
||||
PluginStateLeaseError,
|
||||
type PluginStateLeaseContext,
|
||||
type PluginStateLeaseErrorCode,
|
||||
type PluginStateLeaseOptions,
|
||||
} from "./plugin-state-lease.types.js";
|
||||
import { validatePluginStoreKey, validatePluginStoreNamespace } from "./plugin-store-validation.js";
|
||||
|
||||
const MIN_LEASE_MS = 1_000;
|
||||
|
||||
function leaseError(
|
||||
code: PluginStateLeaseErrorCode,
|
||||
message: string,
|
||||
cause?: unknown,
|
||||
): PluginStateLeaseError {
|
||||
return new PluginStateLeaseError(message, { code, ...(cause === undefined ? {} : { cause }) });
|
||||
}
|
||||
|
||||
function invalidInput(message: string): PluginStateLeaseError {
|
||||
return leaseError("PLUGIN_STATE_LEASE_INVALID_INPUT", message);
|
||||
}
|
||||
|
||||
function validateDuration(value: number, label: string, minimum: number, maximum: number): number {
|
||||
if (!Number.isInteger(value) || value < minimum || value > maximum) {
|
||||
throw invalidInput(`${label} must be an integer between ${minimum} and ${maximum}`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function validatePluginId(pluginId: string): string {
|
||||
const normalized = pluginId.trim();
|
||||
if (!normalized || normalized.startsWith("core:") || normalized.includes("\0")) {
|
||||
throw invalidInput("plugin lease requires a non-core plugin id");
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function validateOptions(pluginId: string, options: PluginStateLeaseOptions) {
|
||||
if (typeof options !== "object" || options === null || Array.isArray(options)) {
|
||||
throw invalidInput("plugin lease options must be an object");
|
||||
}
|
||||
if (typeof options.namespace !== "string") {
|
||||
throw invalidInput("plugin lease namespace must be a string");
|
||||
}
|
||||
if (typeof options.key !== "string") {
|
||||
throw invalidInput("plugin lease key must be a string");
|
||||
}
|
||||
if (options.signal !== undefined && !(options.signal instanceof AbortSignal)) {
|
||||
throw invalidInput("plugin lease signal must be an AbortSignal");
|
||||
}
|
||||
const errors = {
|
||||
invalid: (message: string) => invalidInput(message),
|
||||
limit: (message: string) => invalidInput(message),
|
||||
};
|
||||
const namespace = validatePluginStoreNamespace({
|
||||
value: options.namespace,
|
||||
label: "plugin lease",
|
||||
errors,
|
||||
});
|
||||
const key = validatePluginStoreKey({
|
||||
value: options.key,
|
||||
label: "plugin lease",
|
||||
errors,
|
||||
});
|
||||
const leaseMs = validateDuration(
|
||||
options.leaseMs,
|
||||
"plugin lease leaseMs",
|
||||
MIN_LEASE_MS,
|
||||
MAX_TIMER_TIMEOUT_MS,
|
||||
);
|
||||
const waitMs = validateDuration(options.waitMs, "plugin lease waitMs", 0, MAX_TIMER_TIMEOUT_MS);
|
||||
const database = options.database;
|
||||
if (typeof database !== "object" || database === null || Array.isArray(database)) {
|
||||
throw invalidInput("plugin lease database must be an object");
|
||||
}
|
||||
if (database.scope !== "shared" && database.scope !== "agent") {
|
||||
throw invalidInput("plugin lease database scope must be shared or agent");
|
||||
}
|
||||
if (database.scope === "agent") {
|
||||
if (typeof database.agentId !== "string" || !database.agentId.trim()) {
|
||||
throw invalidInput("plugin lease agent database requires a string agentId");
|
||||
}
|
||||
}
|
||||
return {
|
||||
scope: `plugin:${validatePluginId(pluginId)}:${namespace}`,
|
||||
key,
|
||||
leaseMs,
|
||||
waitMs,
|
||||
database,
|
||||
signal: options.signal,
|
||||
};
|
||||
}
|
||||
|
||||
function mapErrorCode(code: OpenClawStateLeaseErrorCode): PluginStateLeaseErrorCode {
|
||||
switch (code) {
|
||||
case "OPENCLAW_STATE_LEASE_INVALID_INPUT":
|
||||
return "PLUGIN_STATE_LEASE_INVALID_INPUT";
|
||||
case "OPENCLAW_STATE_LEASE_TIMEOUT":
|
||||
return "PLUGIN_STATE_LEASE_TIMEOUT";
|
||||
case "OPENCLAW_STATE_LEASE_ABORTED":
|
||||
return "PLUGIN_STATE_LEASE_ABORTED";
|
||||
case "OPENCLAW_STATE_LEASE_LOST":
|
||||
return "PLUGIN_STATE_LEASE_LOST";
|
||||
case "OPENCLAW_STATE_LEASE_STORAGE_FAILED":
|
||||
return "PLUGIN_STATE_LEASE_STORAGE_FAILED";
|
||||
default:
|
||||
throw new Error(`unsupported OpenClaw state lease error code: ${String(code)}`);
|
||||
}
|
||||
}
|
||||
|
||||
function mapLeaseError(error: unknown): unknown {
|
||||
if (!(error instanceof OpenClawStateLeaseError)) {
|
||||
return error;
|
||||
}
|
||||
return leaseError(mapErrorCode(error.code), error.message, error.cause);
|
||||
}
|
||||
|
||||
function mapLeaseSignal(signal: AbortSignal): {
|
||||
signal: AbortSignal;
|
||||
dispose(): void;
|
||||
} {
|
||||
const controller = new AbortController();
|
||||
const forwardAbort = () => controller.abort(mapLeaseError(signal.reason));
|
||||
if (signal.aborted) {
|
||||
forwardAbort();
|
||||
} else {
|
||||
signal.addEventListener("abort", forwardAbort, { once: true });
|
||||
}
|
||||
return {
|
||||
signal: controller.signal,
|
||||
dispose: () => signal.removeEventListener("abort", forwardAbort),
|
||||
};
|
||||
}
|
||||
|
||||
/** Run one trusted plugin operation under a host-owned SQLite lease. */
|
||||
export async function withPluginStateLease<T>(
|
||||
pluginId: string,
|
||||
options: PluginStateLeaseOptions,
|
||||
run: (lease: PluginStateLeaseContext) => Promise<T>,
|
||||
): Promise<T> {
|
||||
const validated = validateOptions(pluginId, options);
|
||||
try {
|
||||
return await withOpenClawStateLease(
|
||||
{
|
||||
...validated,
|
||||
leaseLabel: "plugin lease",
|
||||
operationLabel: "plugin-state.lease",
|
||||
},
|
||||
async (lease) => {
|
||||
const mapped = mapLeaseSignal(lease.signal);
|
||||
try {
|
||||
return await run({
|
||||
signal: mapped.signal,
|
||||
assertOwned: () => {
|
||||
try {
|
||||
lease.assertOwned();
|
||||
} catch (error) {
|
||||
throw mapLeaseError(error);
|
||||
}
|
||||
},
|
||||
});
|
||||
} finally {
|
||||
mapped.dispose();
|
||||
}
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
throw mapLeaseError(error);
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
// Public plugin lease contracts. Lease ownership stays host-managed; plugins
|
||||
// receive cancellation plus an exact-owner checkpoint for the critical section.
|
||||
export type PluginStateLeaseDatabase = { scope: "shared" } | { scope: "agent"; agentId: string };
|
||||
|
||||
export type PluginStateLeaseOptions = {
|
||||
namespace: string;
|
||||
key: string;
|
||||
database: PluginStateLeaseDatabase;
|
||||
leaseMs: number;
|
||||
waitMs: number;
|
||||
signal?: AbortSignal;
|
||||
};
|
||||
|
||||
export type PluginStateLeaseContext = {
|
||||
signal: AbortSignal;
|
||||
/** Verify that this exact owner holds a non-expired lease at this instant. */
|
||||
assertOwned(): void;
|
||||
};
|
||||
|
||||
export type PluginStateLeaseRunner = <T>(
|
||||
options: PluginStateLeaseOptions,
|
||||
run: (lease: PluginStateLeaseContext) => Promise<T>,
|
||||
) => Promise<T>;
|
||||
|
||||
export type PluginStateLeaseErrorCode =
|
||||
| "PLUGIN_STATE_LEASE_INVALID_INPUT"
|
||||
| "PLUGIN_STATE_LEASE_TIMEOUT"
|
||||
| "PLUGIN_STATE_LEASE_ABORTED"
|
||||
| "PLUGIN_STATE_LEASE_LOST"
|
||||
| "PLUGIN_STATE_LEASE_STORAGE_FAILED";
|
||||
|
||||
export class PluginStateLeaseError extends Error {
|
||||
readonly code: PluginStateLeaseErrorCode;
|
||||
|
||||
constructor(message: string, options: { code: PluginStateLeaseErrorCode; cause?: unknown }) {
|
||||
super(message, { cause: options.cause });
|
||||
this.name = "PluginStateLeaseError";
|
||||
this.code = options.code;
|
||||
}
|
||||
}
|
||||
@@ -4,10 +4,7 @@ import { resolveStateDir } from "../config/paths.js";
|
||||
import type { PluginRecord } from "../plugins/registry-types.js";
|
||||
import { createPluginRegistry } from "../plugins/registry.js";
|
||||
import type { PluginRuntime } from "../plugins/runtime/types.js";
|
||||
import {
|
||||
closeOpenClawAgentDatabasesForTest,
|
||||
openOpenClawAgentDatabase,
|
||||
} from "../state/openclaw-agent-db.js";
|
||||
import { closeOpenClawAgentDatabasesForTest } from "../state/openclaw-agent-db.js";
|
||||
import { openOpenClawStateDatabase } from "../state/openclaw-state-db.js";
|
||||
import { withOpenClawTestState } from "../test-utils/openclaw-test-state.js";
|
||||
import { resetPluginBlobStoreForTests, type OpenBlobStoreOptions } from "./plugin-blob-store.js";
|
||||
@@ -70,9 +67,6 @@ function createTestPluginRegistry() {
|
||||
openSyncKeyedStore: () => {
|
||||
throw new Error("registry plugin runtime proxy should bind openSyncKeyedStore");
|
||||
},
|
||||
withLease: async () => {
|
||||
throw new Error("registry plugin runtime proxy should bind withLease");
|
||||
},
|
||||
},
|
||||
} as unknown as PluginRuntime,
|
||||
});
|
||||
@@ -135,55 +129,6 @@ describe("plugin runtime state proxy", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("binds SQLite leases to trusted plugin identity and database scope", async () => {
|
||||
await withOpenClawTestState({ label: "plugin-lease-runtime" }, async (state) => {
|
||||
const registry = createTestPluginRegistry();
|
||||
const bundled = createPluginRecord("memory-core", "bundled");
|
||||
registry.registry.plugins.push(bundled);
|
||||
const bundledApi = registry.createApi(bundled, { config: {} });
|
||||
|
||||
await bundledApi.runtime.state.withLease(
|
||||
{
|
||||
namespace: "qmd",
|
||||
key: "embed",
|
||||
database: { scope: "shared" },
|
||||
leaseMs: 1_000,
|
||||
waitMs: 0,
|
||||
},
|
||||
async ({ signal }) => {
|
||||
expect(signal.aborted).toBe(false);
|
||||
expect(
|
||||
openOpenClawStateDatabase({ env: state.env })
|
||||
.db.prepare("SELECT scope, lease_key FROM state_leases")
|
||||
.get(),
|
||||
).toEqual({ scope: "plugin:memory-core:qmd", lease_key: "embed" });
|
||||
},
|
||||
);
|
||||
|
||||
const official = createPluginRecord("memory-official", "global", {
|
||||
trustedOfficialInstall: true,
|
||||
});
|
||||
registry.registry.plugins.push(official);
|
||||
const officialApi = registry.createApi(official, { config: {} });
|
||||
await officialApi.runtime.state.withLease(
|
||||
{
|
||||
namespace: "qmd",
|
||||
key: "write",
|
||||
database: { scope: "agent", agentId: "main" },
|
||||
leaseMs: 1_000,
|
||||
waitMs: 0,
|
||||
},
|
||||
async () => {
|
||||
expect(
|
||||
openOpenClawAgentDatabase({ agentId: "main", env: state.env })
|
||||
.db.prepare("SELECT scope, lease_key FROM state_leases")
|
||||
.get(),
|
||||
).toEqual({ scope: "plugin:memory-official:qmd", lease_key: "write" });
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("binds blob stores to the trusted plugin id", async () => {
|
||||
await withOpenClawTestState({ label: "plugin-blob-runtime" }, async () => {
|
||||
const registry = createTestPluginRegistry();
|
||||
@@ -273,18 +218,6 @@ describe("plugin runtime state proxy", () => {
|
||||
maxBytesPerNamespace: 4096,
|
||||
}),
|
||||
).toThrow("openBlobStore is only available for trusted plugins");
|
||||
expect(() =>
|
||||
api.runtime.state.withLease(
|
||||
{
|
||||
namespace: "runtime",
|
||||
key: "writer",
|
||||
database: { scope: "shared" },
|
||||
leaseMs: 1_000,
|
||||
waitMs: 0,
|
||||
},
|
||||
async () => undefined,
|
||||
),
|
||||
).toThrow("withLease is only available for trusted plugins");
|
||||
});
|
||||
|
||||
it("names the denied capability, plugin, and origin for channel ingress queues", () => {
|
||||
@@ -315,17 +248,5 @@ describe("plugin runtime state proxy", () => {
|
||||
maxBytesPerNamespace: 4096,
|
||||
}),
|
||||
).toThrow("openBlobStore is only available for trusted plugins");
|
||||
expect(() =>
|
||||
api.runtime.state.withLease(
|
||||
{
|
||||
namespace: "runtime",
|
||||
key: "writer",
|
||||
database: { scope: "shared" },
|
||||
leaseMs: 1_000,
|
||||
waitMs: 0,
|
||||
},
|
||||
async () => undefined,
|
||||
),
|
||||
).toThrow("withLease is only available for trusted plugins");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,11 +14,6 @@ import {
|
||||
type OpenBlobStoreOptions,
|
||||
type PluginBlobStore,
|
||||
} from "../plugin-state/plugin-blob-store.js";
|
||||
import { withPluginStateLease } from "../plugin-state/plugin-state-lease.js";
|
||||
import type {
|
||||
PluginStateLeaseContext,
|
||||
PluginStateLeaseOptions,
|
||||
} from "../plugin-state/plugin-state-lease.types.js";
|
||||
import {
|
||||
createPluginStateKeyedStore,
|
||||
createPluginStateSyncKeyedStore,
|
||||
@@ -576,7 +571,6 @@ export function createPluginRuntimeResolver(state: PluginRegistryState) {
|
||||
| "openBlobStore"
|
||||
| "openKeyedStore"
|
||||
| "openSyncKeyedStore"
|
||||
| "withLease"
|
||||
| "openChannelIngressQueue"
|
||||
| "openChannelIngressDrain",
|
||||
) => {
|
||||
@@ -609,13 +603,6 @@ export function createPluginRuntimeResolver(state: PluginRegistryState) {
|
||||
assertPluginStateAllowed("openSyncKeyedStore");
|
||||
return createPluginStateSyncKeyedStore<T>(pluginId, options);
|
||||
},
|
||||
withLease: <T>(
|
||||
options: PluginStateLeaseOptions,
|
||||
run: (lease: PluginStateLeaseContext) => Promise<T>,
|
||||
): Promise<T> => {
|
||||
assertPluginStateAllowed("withLease");
|
||||
return withPluginStateLease(pluginId, options, run);
|
||||
},
|
||||
openChannelIngressQueue: <TPayload, TMetadata = unknown, TCompletedMetadata = unknown>(
|
||||
options?: Omit<Parameters<typeof createChannelIngressQueue>[0], "channelId">,
|
||||
) => {
|
||||
|
||||
@@ -293,9 +293,6 @@ export function createPluginRuntime(_options: CreatePluginRuntimeOptions = {}):
|
||||
openSyncKeyedStore: () => {
|
||||
throw new Error("openSyncKeyedStore is only available through the plugin runtime proxy.");
|
||||
},
|
||||
withLease: async () => {
|
||||
throw new Error("withLease is only available through the plugin runtime proxy.");
|
||||
},
|
||||
openChannelIngressQueue: () => {
|
||||
throw new Error(
|
||||
"openChannelIngressQueue is only available through the plugin runtime proxy.",
|
||||
|
||||
@@ -469,7 +469,6 @@ export type PluginRuntimeCore = {
|
||||
openSyncKeyedStore: <T>(
|
||||
options: import("../../plugin-state/plugin-state-store.types.js").OpenKeyedStoreOptions,
|
||||
) => import("../../plugin-state/plugin-state-store.types.js").PluginStateSyncKeyedStore<T>;
|
||||
withLease: import("../../plugin-state/plugin-state-lease.types.js").PluginStateLeaseRunner;
|
||||
openChannelIngressQueue: <TPayload, TMetadata = unknown, TCompletedMetadata = unknown>(
|
||||
options?: Omit<CreateChannelIngressQueueOptions, "channelId">,
|
||||
) => import("../../channels/message/ingress-queue.js").ChannelIngressQueue<
|
||||
|
||||
@@ -31,7 +31,6 @@ import {
|
||||
} from "./openclaw-state-lease-owner.js";
|
||||
|
||||
export { OpenClawStateLeaseError };
|
||||
export type { OpenClawStateLeaseErrorCode };
|
||||
|
||||
type LeaseDatabase = Pick<OpenClawStateKyselyDatabase, "state_leases">;
|
||||
type AgentLeaseDatabase = Pick<OpenClawAgentKyselyDatabase, "state_leases">;
|
||||
|
||||
Reference in New Issue
Block a user