diff --git a/docs/.generated/plugin-sdk-api-baseline.sha256 b/docs/.generated/plugin-sdk-api-baseline.sha256 index 1f712f07c93b..8265c96e5c20 100644 --- a/docs/.generated/plugin-sdk-api-baseline.sha256 +++ b/docs/.generated/plugin-sdk-api-baseline.sha256 @@ -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 diff --git a/docs/plugins/sdk-runtime.md b/docs/plugins/sdk-runtime.md index 409d7762b6f9..f5b11c61882d 100644 --- a/docs/plugins/sdk-runtime.md +++ b/docs/plugins/sdk-runtime.md @@ -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(...)` 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. - `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. diff --git a/docs/plugins/sdk-subpaths.md b/docs/plugins/sdk-subpaths.md index fc84589ebf9d..c7b7b97928a4 100644 --- a/docs/plugins/sdk-subpaths.md +++ b/docs/plugins/sdk-subpaths.md @@ -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 | diff --git a/extensions/memory-core/cli-metadata.test.ts b/extensions/memory-core/cli-metadata.test.ts index 122d7df73ae1..f22792581c10 100644 --- a/extensions/memory-core/cli-metadata.test.ts +++ b/extensions/memory-core/cli-metadata.test.ts @@ -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[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"); }); }); diff --git a/extensions/memory-core/cli-metadata.ts b/extensions/memory-core/cli-metadata.ts index 82a1b1e4dc4e..4087038112b6 100644 --- a/extensions/memory-core/cli-metadata.ts +++ b/extensions/memory-core/cli-metadata.ts @@ -14,7 +14,6 @@ export default definePluginEntry({ acquireLocalService: api.runtime.llm?.acquireLocalService, openKeyedStore: (options: OpenKeyedStoreOptions) => api.runtime.state.openKeyedStore(options), - withLease: api.runtime.state.withLease.bind(api.runtime.state), }); }, { diff --git a/extensions/memory-core/index.test.ts b/extensions/memory-core/index.test.ts index 2983b76cc55a..267ee85a504b 100644 --- a/extensions/memory-core/index.test.ts +++ b/extensions/memory-core/index.test.ts @@ -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)); }); }); diff --git a/extensions/memory-core/index.ts b/extensions/memory-core/index.ts index 600d038988c5..d555ee26085b 100644 --- a/extensions/memory-core/index.ts +++ b/extensions/memory-core/index.ts @@ -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 = (options: OpenKeyedStoreOptions) => api.runtime.state.openKeyedStore(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); diff --git a/extensions/memory-core/src/cli-runtime-common.ts b/extensions/memory-core/src/cli-runtime-common.ts index e6552c2d6237..93bd3210a929 100644 --- a/extensions/memory-core/src/cli-runtime-common.ts +++ b/extensions/memory-core/src/cli-runtime-common.ts @@ -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; }): Promise { const managerParams: Parameters[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({ 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; }): Promise { 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 }), }); } diff --git a/extensions/memory-core/src/cli.test.ts b/extensions/memory-core/src/cli.test.ts index e3c5c99a3225..6983e4afbf82 100644 --- a/extensions/memory-core/src/cli.test.ts +++ b/extensions/memory-core/src/cli.test.ts @@ -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 () => []); diff --git a/extensions/memory-core/src/memory-tool-manager.test-mocks.ts b/extensions/memory-core/src/memory-tool-manager.test-mocks.ts index 9d1aa9d80773..733b5f205e61 100644 --- a/extensions/memory-core/src/memory-tool-manager.test-mocks.ts +++ b/extensions/memory-core/src/memory-tool-manager.test-mocks.ts @@ -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"; diff --git a/extensions/memory-core/src/memory/runtime-host.ts b/extensions/memory-core/src/memory/runtime-host.ts index e812822c3aca..cd91e78a1707 100644 --- a/extensions/memory-core/src/memory/runtime-host.ts +++ b/extensions/memory-core/src/memory/runtime-host.ts @@ -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; }; diff --git a/extensions/memory-core/src/memory/search-manager.ts b/extensions/memory-core/src/memory/search-manager.ts index d5f63dd49f93..207ee3318c41 100644 --- a/extensions/memory-core/src/memory/search-manager.ts +++ b/extensions/memory-core/src/memory/search-manager.ts @@ -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 = { diff --git a/extensions/memory-core/src/runtime-provider.test.ts b/extensions/memory-core/src/runtime-provider.test.ts index c0a6279e7662..4b7088ecc77e 100644 --- a/extensions/memory-core/src/runtime-provider.test.ts +++ b/extensions/memory-core/src/runtime-provider.test.ts @@ -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(); diff --git a/extensions/memory-core/src/runtime-provider.ts b/extensions/memory-core/src/runtime-provider.ts index 5085866497ba..522a2da2bce8 100644 --- a/extensions/memory-core/src/runtime-provider.ts +++ b/extensions/memory-core/src/runtime-provider.ts @@ -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, diff --git a/extensions/memory-core/src/tools.shared.ts b/extensions/memory-core/src/tools.shared.ts index 6b766e214578..48f2f715444f 100644 --- a/extensions/memory-core/src/tools.shared.ts +++ b/extensions/memory-core/src/tools.shared.ts @@ -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; @@ -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 ? { diff --git a/extensions/memory-core/src/tools.test.ts b/extensions/memory-core/src/tools.test.ts index 1bf21a29c301..7c11d13e7db6 100644 --- a/extensions/memory-core/src/tools.test.ts +++ b/extensions/memory-core/src/tools.test.ts @@ -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"); diff --git a/extensions/memory-core/src/tools.ts b/extensions/memory-core/src/tools.ts index d9a977188914..92411e162108 100644 --- a/extensions/memory-core/src/tools.ts +++ b/extensions/memory-core/src/tools.ts @@ -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, diff --git a/src/infra/backup-create.test.ts b/src/infra/backup-create.test.ts index 465cf44d461d..8dc50509328f 100644 --- a/src/infra/backup-create.test.ts +++ b/src/infra/backup-create.test.ts @@ -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); diff --git a/src/plugin-sdk/plugin-state-runtime.ts b/src/plugin-sdk/plugin-state-runtime.ts index 552985c08b9c..0b43def2f7bc 100644 --- a/src/plugin-sdk/plugin-state-runtime.ts +++ b/src/plugin-sdk/plugin-state-runtime.ts @@ -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"; diff --git a/src/plugin-sdk/test-helpers/plugin-runtime-mock.ts b/src/plugin-sdk/test-helpers/plugin-runtime-mock.ts index 8ff0f56bc9d1..057a16d5d401 100644 --- a/src/plugin-sdk/test-helpers/plugin-runtime-mock.ts +++ b/src/plugin-sdk/test-helpers/plugin-runtime-mock.ts @@ -965,10 +965,6 @@ export function createPluginRuntimeMock(overrides: DeepPartial = 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"], diff --git a/src/plugin-state/plugin-state-lease.test.ts b/src/plugin-state/plugin-state-lease.test.ts deleted file mode 100644 index 93b9e76abf85..000000000000 --- a/src/plugin-state/plugin-state-lease.test.ts +++ /dev/null @@ -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() { - let resolve!: (value: T | PromiseLike) => void; - const promise = new Promise((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((_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 | 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((_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" }); - }); - }); -}); diff --git a/src/plugin-state/plugin-state-lease.ts b/src/plugin-state/plugin-state-lease.ts deleted file mode 100644 index e1cab436172f..000000000000 --- a/src/plugin-state/plugin-state-lease.ts +++ /dev/null @@ -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( - pluginId: string, - options: PluginStateLeaseOptions, - run: (lease: PluginStateLeaseContext) => Promise, -): Promise { - 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); - } -} diff --git a/src/plugin-state/plugin-state-lease.types.ts b/src/plugin-state/plugin-state-lease.types.ts deleted file mode 100644 index 147687e17930..000000000000 --- a/src/plugin-state/plugin-state-lease.types.ts +++ /dev/null @@ -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 = ( - options: PluginStateLeaseOptions, - run: (lease: PluginStateLeaseContext) => Promise, -) => Promise; - -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; - } -} diff --git a/src/plugin-state/plugin-state-store.runtime.test.ts b/src/plugin-state/plugin-state-store.runtime.test.ts index 59ccee7bcbce..8cae4f33e850 100644 --- a/src/plugin-state/plugin-state-store.runtime.test.ts +++ b/src/plugin-state/plugin-state-store.runtime.test.ts @@ -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"); }); }); diff --git a/src/plugins/registry-runtime.ts b/src/plugins/registry-runtime.ts index d7c3fa536e70..2476a2c0132e 100644 --- a/src/plugins/registry-runtime.ts +++ b/src/plugins/registry-runtime.ts @@ -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(pluginId, options); }, - withLease: ( - options: PluginStateLeaseOptions, - run: (lease: PluginStateLeaseContext) => Promise, - ): Promise => { - assertPluginStateAllowed("withLease"); - return withPluginStateLease(pluginId, options, run); - }, openChannelIngressQueue: ( options?: Omit[0], "channelId">, ) => { diff --git a/src/plugins/runtime/index.ts b/src/plugins/runtime/index.ts index 5534c7d96fa2..cbe3306de46b 100644 --- a/src/plugins/runtime/index.ts +++ b/src/plugins/runtime/index.ts @@ -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.", diff --git a/src/plugins/runtime/types-core.ts b/src/plugins/runtime/types-core.ts index ba4af21916a2..d3d9c47f3775 100644 --- a/src/plugins/runtime/types-core.ts +++ b/src/plugins/runtime/types-core.ts @@ -469,7 +469,6 @@ export type PluginRuntimeCore = { openSyncKeyedStore: ( options: import("../../plugin-state/plugin-state-store.types.js").OpenKeyedStoreOptions, ) => import("../../plugin-state/plugin-state-store.types.js").PluginStateSyncKeyedStore; - withLease: import("../../plugin-state/plugin-state-lease.types.js").PluginStateLeaseRunner; openChannelIngressQueue: ( options?: Omit, ) => import("../../channels/message/ingress-queue.js").ChannelIngressQueue< diff --git a/src/state/openclaw-state-lease.ts b/src/state/openclaw-state-lease.ts index 7cae6bb42a8e..2729100fca74 100644 --- a/src/state/openclaw-state-lease.ts +++ b/src/state/openclaw-state-lease.ts @@ -31,7 +31,6 @@ import { } from "./openclaw-state-lease-owner.js"; export { OpenClawStateLeaseError }; -export type { OpenClawStateLeaseErrorCode }; type LeaseDatabase = Pick; type AgentLeaseDatabase = Pick;