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:
Peter Steinberger
2026-08-09 11:16:07 -07:00
committed by GitHub
parent a828bfe04e
commit 9809375fda
28 changed files with 46 additions and 1187 deletions
+31 -31
View File
@@ -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
+2 -16
View File
@@ -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>
+1 -1
View File
@@ -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 |
+2 -12
View File
@@ -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");
});
});
-1
View File
@@ -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),
});
},
{
+4 -23
View File
@@ -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));
});
});
+2 -8
View File
@@ -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 }),
});
}
-15
View File
@@ -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
? {
-17
View File
@@ -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");
-5
View File
@@ -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,
+3 -3
View File
@@ -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);
-8
View File
@@ -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"],
-682
View File
@@ -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" });
});
});
});
-177
View File
@@ -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");
});
});
-13
View File
@@ -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">,
) => {
-3
View File
@@ -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.",
-1
View File
@@ -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<
-1
View File
@@ -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">;