diff --git a/CHANGELOG.md b/CHANGELOG.md index 7fe6b9debadc..dc3873a9ca17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -68,6 +68,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- **Telegram live locations:** expose initial, moving, and stopped live-location updates through the channel-neutral `message_received` hook without starting agent turns for edits. - **Updater plugin convergence:** keep pre-plugin doctor passes from installing configured plugins before the updater's plugin sweep, while preserving the final post-plugin migration pass and preventing ambient update-phase state from leaking into fresh doctor processes. - **Control UI browser tab identity:** keep selected tab styling, accessibility, focus, address, and page snapshot aligned across in-place navigation and tab reordering. Fixes #120745. Thanks @shakkernerd. - **Control UI staged attachments:** preserve unsent images, files, pasted images, and large pasted text across same-tab route and narrow split-pane remounts while keeping pane close, mismatched pane/session/Gateway remounts, application shutdown, and hard reload as cleanup boundaries. Fixes #121519. Thanks @shakkernerd. diff --git a/docs/.generated/plugin-sdk-api-baseline/agent-harness-runtime.json b/docs/.generated/plugin-sdk-api-baseline/agent-harness-runtime.json index 65a27da36ca6..6c32621c92fc 100644 --- a/docs/.generated/plugin-sdk-api-baseline/agent-harness-runtime.json +++ b/docs/.generated/plugin-sdk-api-baseline/agent-harness-runtime.json @@ -1 +1 @@ -{"contentHash":"00b95749d23bbf126f7081ee8da4a303b2c984f181be7b68f8bf48b12b67e901","entrypoint":"agent-harness-runtime","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime"} +{"contentHash":"6d42269794e0473fdae832d6c1af49854a822438f4b3b0943fb855846cbd5f64","entrypoint":"agent-harness-runtime","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime"} diff --git a/docs/.generated/plugin-sdk-api-baseline/agent-harness.json b/docs/.generated/plugin-sdk-api-baseline/agent-harness.json index b858637685a4..2e4af18aaedb 100644 --- a/docs/.generated/plugin-sdk-api-baseline/agent-harness.json +++ b/docs/.generated/plugin-sdk-api-baseline/agent-harness.json @@ -1 +1 @@ -{"contentHash":"a806b806c516e82741cf2731d0d0375736eda65dcf130f0a66f3a631b6b1b482","entrypoint":"agent-harness","importSpecifier":"openclaw/plugin-sdk/agent-harness"} +{"contentHash":"d5a6edec36b8675110181c771b79fac5753d242ce8adc58022743eb0184c34f2","entrypoint":"agent-harness","importSpecifier":"openclaw/plugin-sdk/agent-harness"} diff --git a/docs/.generated/plugin-sdk-api-baseline/agent-runtime.json b/docs/.generated/plugin-sdk-api-baseline/agent-runtime.json index 8e98f9077097..5571874cd59e 100644 --- a/docs/.generated/plugin-sdk-api-baseline/agent-runtime.json +++ b/docs/.generated/plugin-sdk-api-baseline/agent-runtime.json @@ -1 +1 @@ -{"contentHash":"c97e7fbdbd52d6afe181fe0685495582ad5802f3f3fb7e531b2e123ddd4f6a32","entrypoint":"agent-runtime","importSpecifier":"openclaw/plugin-sdk/agent-runtime"} +{"contentHash":"02efab07a69f7899421c36ab7cee6dd2bd84ce2d689d10549129dc4dc45a1f15","entrypoint":"agent-runtime","importSpecifier":"openclaw/plugin-sdk/agent-runtime"} diff --git a/docs/.generated/plugin-sdk-api-baseline/channel-contract.json b/docs/.generated/plugin-sdk-api-baseline/channel-contract.json index 5856bca90ceb..35607427e561 100644 --- a/docs/.generated/plugin-sdk-api-baseline/channel-contract.json +++ b/docs/.generated/plugin-sdk-api-baseline/channel-contract.json @@ -1 +1 @@ -{"contentHash":"f93ca33e4a11459e4def3d6c09463fcbe0f93c6cd6bb967edf7ab6ebd3dbe584","entrypoint":"channel-contract","importSpecifier":"openclaw/plugin-sdk/channel-contract"} +{"contentHash":"30b463a4e09c326f52255ad4015c8546c7bca5475d33d784969e2cc4f1feed40","entrypoint":"channel-contract","importSpecifier":"openclaw/plugin-sdk/channel-contract"} diff --git a/docs/.generated/plugin-sdk-api-baseline/channel-core.json b/docs/.generated/plugin-sdk-api-baseline/channel-core.json index 3456659edd9c..1e2b0707b36b 100644 --- a/docs/.generated/plugin-sdk-api-baseline/channel-core.json +++ b/docs/.generated/plugin-sdk-api-baseline/channel-core.json @@ -1 +1 @@ -{"contentHash":"60730746ea4ad9d8aef60a7f4d4aecd7cd28ae5d28b21e44646b3fe1398e6c95","entrypoint":"channel-core","importSpecifier":"openclaw/plugin-sdk/channel-core"} +{"contentHash":"74e044d5e8a93fd319609f0d130eea0c1e636111e7c2597cbc3c98b847515bd3","entrypoint":"channel-core","importSpecifier":"openclaw/plugin-sdk/channel-core"} diff --git a/docs/.generated/plugin-sdk-api-baseline/channel-entry-contract.json b/docs/.generated/plugin-sdk-api-baseline/channel-entry-contract.json index 7b0c8c32a5ca..8995ff213a4b 100644 --- a/docs/.generated/plugin-sdk-api-baseline/channel-entry-contract.json +++ b/docs/.generated/plugin-sdk-api-baseline/channel-entry-contract.json @@ -1 +1 @@ -{"contentHash":"3d0d58f29870cbb7889517e24b874ea5b63dfde6c15433c13414e8484b74d266","entrypoint":"channel-entry-contract","importSpecifier":"openclaw/plugin-sdk/channel-entry-contract"} +{"contentHash":"176592119d65008bfa8d1b465670c9556b0275d432e738353ea5a25d0a61985d","entrypoint":"channel-entry-contract","importSpecifier":"openclaw/plugin-sdk/channel-entry-contract"} diff --git a/docs/.generated/plugin-sdk-api-baseline/channel-inbound.json b/docs/.generated/plugin-sdk-api-baseline/channel-inbound.json index 6b5f9b6370dd..3079e37bcf91 100644 --- a/docs/.generated/plugin-sdk-api-baseline/channel-inbound.json +++ b/docs/.generated/plugin-sdk-api-baseline/channel-inbound.json @@ -1 +1 @@ -{"contentHash":"dbbddd5dc44f96642d3885073c6d2939bca17d1ee59c4e65dfd734ccaa115b4b","entrypoint":"channel-inbound","importSpecifier":"openclaw/plugin-sdk/channel-inbound"} +{"contentHash":"39d7eea281be7a268c1878155930034027f73ae6404622ab09041300bda41991","entrypoint":"channel-inbound","importSpecifier":"openclaw/plugin-sdk/channel-inbound"} diff --git a/docs/.generated/plugin-sdk-api-baseline/channel-message.json b/docs/.generated/plugin-sdk-api-baseline/channel-message.json index c7b39954c9e5..19efe706c718 100644 --- a/docs/.generated/plugin-sdk-api-baseline/channel-message.json +++ b/docs/.generated/plugin-sdk-api-baseline/channel-message.json @@ -1 +1 @@ -{"contentHash":"f185748f1394f1d64653b21889954229bf0049e174526554e52c8e0bfc2c7dc5","entrypoint":"channel-message","importSpecifier":"openclaw/plugin-sdk/channel-message"} +{"contentHash":"d10f59babb0166e4db0a66e801ca674ad23b158737939d9213c3bc6258f2cc5c","entrypoint":"channel-message","importSpecifier":"openclaw/plugin-sdk/channel-message"} diff --git a/docs/.generated/plugin-sdk-api-baseline/channel-outbound.json b/docs/.generated/plugin-sdk-api-baseline/channel-outbound.json index 0a1285782833..59eb4ae86570 100644 --- a/docs/.generated/plugin-sdk-api-baseline/channel-outbound.json +++ b/docs/.generated/plugin-sdk-api-baseline/channel-outbound.json @@ -1 +1 @@ -{"contentHash":"59cfee1296e172c295a550d4adbfff8776f10b73e07902394a68aa143efecabc","entrypoint":"channel-outbound","importSpecifier":"openclaw/plugin-sdk/channel-outbound"} +{"contentHash":"dcbaf714f293fbe933587344f798c0d86bfd65dc74e05e552c08666695da5125","entrypoint":"channel-outbound","importSpecifier":"openclaw/plugin-sdk/channel-outbound"} diff --git a/docs/.generated/plugin-sdk-api-baseline/channel-pairing.json b/docs/.generated/plugin-sdk-api-baseline/channel-pairing.json index 027936ea4406..52230d7fe2cc 100644 --- a/docs/.generated/plugin-sdk-api-baseline/channel-pairing.json +++ b/docs/.generated/plugin-sdk-api-baseline/channel-pairing.json @@ -1 +1 @@ -{"contentHash":"07f8d2b5d1186410f5233394028b5df8a5cdbcb7a40011ca089db78e202d3f8f","entrypoint":"channel-pairing","importSpecifier":"openclaw/plugin-sdk/channel-pairing"} +{"contentHash":"b09818bcd98562155b66aafd6643b7487a4c267f662c1da87434b47a03446b4e","entrypoint":"channel-pairing","importSpecifier":"openclaw/plugin-sdk/channel-pairing"} diff --git a/docs/.generated/plugin-sdk-api-baseline/channel-plugin-common.json b/docs/.generated/plugin-sdk-api-baseline/channel-plugin-common.json index 238879f79200..c0816cae3df3 100644 --- a/docs/.generated/plugin-sdk-api-baseline/channel-plugin-common.json +++ b/docs/.generated/plugin-sdk-api-baseline/channel-plugin-common.json @@ -1 +1 @@ -{"contentHash":"400ffb2b574275188fcbaeb2bc4fd6c5dd6cc1007c76223b1d63d2056b3ddd99","entrypoint":"channel-plugin-common","importSpecifier":"openclaw/plugin-sdk/channel-plugin-common"} +{"contentHash":"5e92fe659c6fa93639b6e197bca678ec533c190208c65eee3ef4cb24f3929848","entrypoint":"channel-plugin-common","importSpecifier":"openclaw/plugin-sdk/channel-plugin-common"} diff --git a/docs/.generated/plugin-sdk-api-baseline/command-auth-native.json b/docs/.generated/plugin-sdk-api-baseline/command-auth-native.json index 296fd604051d..42ea2f19b03e 100644 --- a/docs/.generated/plugin-sdk-api-baseline/command-auth-native.json +++ b/docs/.generated/plugin-sdk-api-baseline/command-auth-native.json @@ -1 +1 @@ -{"contentHash":"a5feee467592d71486b388de14c3a08ccaf776c5e31d9355aa49bbfa3dbcb9d1","entrypoint":"command-auth-native","importSpecifier":"openclaw/plugin-sdk/command-auth-native"} +{"contentHash":"41c8a0c1c9454afec387fb980900d4795ef29f45a05a025aeb1602bb7739b7a8","entrypoint":"command-auth-native","importSpecifier":"openclaw/plugin-sdk/command-auth-native"} diff --git a/docs/.generated/plugin-sdk-api-baseline/command-auth.json b/docs/.generated/plugin-sdk-api-baseline/command-auth.json index 00a0238e6004..b2096f6f80d6 100644 --- a/docs/.generated/plugin-sdk-api-baseline/command-auth.json +++ b/docs/.generated/plugin-sdk-api-baseline/command-auth.json @@ -1 +1 @@ -{"contentHash":"e0e0ab127fcab6833850a208ecf21cc26bbfda73d4240dcddebae4790ea641db","entrypoint":"command-auth","importSpecifier":"openclaw/plugin-sdk/command-auth"} +{"contentHash":"59750b3a09074d7e6743f4f1ec4b9ee3623862521d6be196131ed50977a15539","entrypoint":"command-auth","importSpecifier":"openclaw/plugin-sdk/command-auth"} diff --git a/docs/.generated/plugin-sdk-api-baseline/config-runtime.json b/docs/.generated/plugin-sdk-api-baseline/config-runtime.json index a527178afcb4..cf8cbe7633bd 100644 --- a/docs/.generated/plugin-sdk-api-baseline/config-runtime.json +++ b/docs/.generated/plugin-sdk-api-baseline/config-runtime.json @@ -1 +1 @@ -{"contentHash":"f42f280c5fec25a7dae84fa52387525e3ef21c78ac1ee2c0ae19f8e58db829c1","entrypoint":"config-runtime","importSpecifier":"openclaw/plugin-sdk/config-runtime"} +{"contentHash":"96df262c825f0096a8050df72f6391bfa8c399555d148491dfbdbd6c402fd63b","entrypoint":"config-runtime","importSpecifier":"openclaw/plugin-sdk/config-runtime"} diff --git a/docs/.generated/plugin-sdk-api-baseline/conversation-runtime.json b/docs/.generated/plugin-sdk-api-baseline/conversation-runtime.json index 696797fd246c..b1165608271b 100644 --- a/docs/.generated/plugin-sdk-api-baseline/conversation-runtime.json +++ b/docs/.generated/plugin-sdk-api-baseline/conversation-runtime.json @@ -1 +1 @@ -{"contentHash":"c48ebc2d528a2f3fb2496828c20b871f72c09dc8636ec8b12e94f9e91199f751","entrypoint":"conversation-runtime","importSpecifier":"openclaw/plugin-sdk/conversation-runtime"} +{"contentHash":"b0b4951cd020c358a1d27888c7794a6021c9e714502fa73becafaae100e67003","entrypoint":"conversation-runtime","importSpecifier":"openclaw/plugin-sdk/conversation-runtime"} diff --git a/docs/.generated/plugin-sdk-api-baseline/core.json b/docs/.generated/plugin-sdk-api-baseline/core.json index e75d0359d584..9abaf9a92cb9 100644 --- a/docs/.generated/plugin-sdk-api-baseline/core.json +++ b/docs/.generated/plugin-sdk-api-baseline/core.json @@ -1 +1 @@ -{"contentHash":"589c2e34ab4910b95df172434a94e7a74838280c31f0955319fd696be3f3bc78","entrypoint":"core","importSpecifier":"openclaw/plugin-sdk/core"} +{"contentHash":"db4f872ae8ea2b1c36068c4fa4d0550e49550f34d31cbb4898533cdde9f4d103","entrypoint":"core","importSpecifier":"openclaw/plugin-sdk/core"} diff --git a/docs/.generated/plugin-sdk-api-baseline/discord.json b/docs/.generated/plugin-sdk-api-baseline/discord.json index 42ed4d5a1c06..459a86f0f791 100644 --- a/docs/.generated/plugin-sdk-api-baseline/discord.json +++ b/docs/.generated/plugin-sdk-api-baseline/discord.json @@ -1 +1 @@ -{"contentHash":"55b33df13c3362c01f13580f9903900075a2bb50b05cae56533a3ce9a5170639","entrypoint":"discord","importSpecifier":"openclaw/plugin-sdk/discord"} +{"contentHash":"9a9729aaa9a46400464a7c2b5240541a9f379271b863b25e1c4dd1c3b7508e73","entrypoint":"discord","importSpecifier":"openclaw/plugin-sdk/discord"} diff --git a/docs/.generated/plugin-sdk-api-baseline/hook-runtime.json b/docs/.generated/plugin-sdk-api-baseline/hook-runtime.json index 05a02df290de..c1b7f3460d09 100644 --- a/docs/.generated/plugin-sdk-api-baseline/hook-runtime.json +++ b/docs/.generated/plugin-sdk-api-baseline/hook-runtime.json @@ -1 +1 @@ -{"contentHash":"6066a7ed3f68774a9e108b71a5bedfa002582dfdfa64e53ba8fefecb2310c09f","entrypoint":"hook-runtime","importSpecifier":"openclaw/plugin-sdk/hook-runtime"} +{"contentHash":"372c9a2f49fd5d431197b7f35c37321a4d4296d7a1cddc2b71c3ee757ebe95a7","entrypoint":"hook-runtime","importSpecifier":"openclaw/plugin-sdk/hook-runtime"} diff --git a/docs/.generated/plugin-sdk-api-baseline/inbound-reply-dispatch.json b/docs/.generated/plugin-sdk-api-baseline/inbound-reply-dispatch.json index 83719337658d..2cbbcfca9353 100644 --- a/docs/.generated/plugin-sdk-api-baseline/inbound-reply-dispatch.json +++ b/docs/.generated/plugin-sdk-api-baseline/inbound-reply-dispatch.json @@ -1 +1 @@ -{"contentHash":"61c67e6ebab5fc4d69226afa0d94f794e8d6798658dfaef60184506551b97a47","entrypoint":"inbound-reply-dispatch","importSpecifier":"openclaw/plugin-sdk/inbound-reply-dispatch"} +{"contentHash":"0006fe56489e1a95488e436e5b2d881c353003ba24163aa5620f8b52c87c0d99","entrypoint":"inbound-reply-dispatch","importSpecifier":"openclaw/plugin-sdk/inbound-reply-dispatch"} diff --git a/docs/.generated/plugin-sdk-api-baseline/media-runtime.json b/docs/.generated/plugin-sdk-api-baseline/media-runtime.json index bb1c62881182..55532189f010 100644 --- a/docs/.generated/plugin-sdk-api-baseline/media-runtime.json +++ b/docs/.generated/plugin-sdk-api-baseline/media-runtime.json @@ -1 +1 @@ -{"contentHash":"d5e2f7be4ffffaff8ff33cf86761e8e88ccca74b10a9d5cc31ab7cd99012af74","entrypoint":"media-runtime","importSpecifier":"openclaw/plugin-sdk/media-runtime"} +{"contentHash":"04ca7bf038cd8ad69f3145ebd3c774b8b27b18ed96fa8227e289abcecdbd83b5","entrypoint":"media-runtime","importSpecifier":"openclaw/plugin-sdk/media-runtime"} diff --git a/docs/.generated/plugin-sdk-api-baseline/media-understanding-runtime.json b/docs/.generated/plugin-sdk-api-baseline/media-understanding-runtime.json index b1879dabaf00..587938882db8 100644 --- a/docs/.generated/plugin-sdk-api-baseline/media-understanding-runtime.json +++ b/docs/.generated/plugin-sdk-api-baseline/media-understanding-runtime.json @@ -1 +1 @@ -{"contentHash":"b6ca0750b3c2ada85cec932d08e323e28284566869e2b38d4ae2a849d0947ba5","entrypoint":"media-understanding-runtime","importSpecifier":"openclaw/plugin-sdk/media-understanding-runtime"} +{"contentHash":"7d5fd82c531675b86446df475d1744902b721dbcc345aeabaf7bdfee27a77bc0","entrypoint":"media-understanding-runtime","importSpecifier":"openclaw/plugin-sdk/media-understanding-runtime"} diff --git a/docs/.generated/plugin-sdk-api-baseline/meeting-runtime.json b/docs/.generated/plugin-sdk-api-baseline/meeting-runtime.json index b1b7979b206f..c1ff4fe82a8d 100644 --- a/docs/.generated/plugin-sdk-api-baseline/meeting-runtime.json +++ b/docs/.generated/plugin-sdk-api-baseline/meeting-runtime.json @@ -1 +1 @@ -{"contentHash":"8d9f9721597b619189cbaa30747e10d2fb8adcf8e8404880b85683fb8fcebd47","entrypoint":"meeting-runtime","importSpecifier":"openclaw/plugin-sdk/meeting-runtime"} +{"contentHash":"4d4adfe9f53f025c8bb22ee1a1dff2e224dae6bb25158ac1ac991cf7ebad249b","entrypoint":"meeting-runtime","importSpecifier":"openclaw/plugin-sdk/meeting-runtime"} diff --git a/docs/.generated/plugin-sdk-api-baseline/plugin-entry.json b/docs/.generated/plugin-sdk-api-baseline/plugin-entry.json index 980e7fd34bb4..c90c87fe5b5f 100644 --- a/docs/.generated/plugin-sdk-api-baseline/plugin-entry.json +++ b/docs/.generated/plugin-sdk-api-baseline/plugin-entry.json @@ -1 +1 @@ -{"contentHash":"b25cfc5913de6cc44719995ec258226c7cb9fd1623afc277d75a5bc2e9870ccf","entrypoint":"plugin-entry","importSpecifier":"openclaw/plugin-sdk/plugin-entry"} +{"contentHash":"61e6ba4e3564811059b3d27ecfee1df8b63f151a8a43bab7190336b02f76889d","entrypoint":"plugin-entry","importSpecifier":"openclaw/plugin-sdk/plugin-entry"} diff --git a/docs/.generated/plugin-sdk-api-baseline/plugin-runtime.json b/docs/.generated/plugin-sdk-api-baseline/plugin-runtime.json index 742d19f8bfd6..1489aa426f84 100644 --- a/docs/.generated/plugin-sdk-api-baseline/plugin-runtime.json +++ b/docs/.generated/plugin-sdk-api-baseline/plugin-runtime.json @@ -1 +1 @@ -{"contentHash":"0a01c88224f2d5dfd489f7478940032838e8246c9b00ff94fb6bcaf8d6bf84f5","entrypoint":"plugin-runtime","importSpecifier":"openclaw/plugin-sdk/plugin-runtime"} +{"contentHash":"647420ef973a58b7078e974eba6ab85b6d2df96f28e07c3bce108c0da3f52c68","entrypoint":"plugin-runtime","importSpecifier":"openclaw/plugin-sdk/plugin-runtime"} diff --git a/docs/.generated/plugin-sdk-api-baseline/provider-catalog-runtime.json b/docs/.generated/plugin-sdk-api-baseline/provider-catalog-runtime.json index 41777f20eb3c..8d27f3dac668 100644 --- a/docs/.generated/plugin-sdk-api-baseline/provider-catalog-runtime.json +++ b/docs/.generated/plugin-sdk-api-baseline/provider-catalog-runtime.json @@ -1 +1 @@ -{"contentHash":"54687a6ab40ae684e91ee657a2188685a46df434232982807f5ebcadd941d3b8","entrypoint":"provider-catalog-runtime","importSpecifier":"openclaw/plugin-sdk/provider-catalog-runtime"} +{"contentHash":"61e41f87d2a11f6c85ff712b500fc2cf50b974ee2f701e9c69c1693dc206e15d","entrypoint":"provider-catalog-runtime","importSpecifier":"openclaw/plugin-sdk/provider-catalog-runtime"} diff --git a/docs/.generated/plugin-sdk-api-baseline/reply-dispatch-runtime.json b/docs/.generated/plugin-sdk-api-baseline/reply-dispatch-runtime.json index 855c216d81e1..fde64789d424 100644 --- a/docs/.generated/plugin-sdk-api-baseline/reply-dispatch-runtime.json +++ b/docs/.generated/plugin-sdk-api-baseline/reply-dispatch-runtime.json @@ -1 +1 @@ -{"contentHash":"d852e0d74a12f6eae8695aabc3a3659ec447a5289bf8e9e3a91fce7ad24c9f5a","entrypoint":"reply-dispatch-runtime","importSpecifier":"openclaw/plugin-sdk/reply-dispatch-runtime"} +{"contentHash":"3d4bc42977575af9ec21f48099cb3e74a781839c08e6d3e30baec39f2009ce03","entrypoint":"reply-dispatch-runtime","importSpecifier":"openclaw/plugin-sdk/reply-dispatch-runtime"} diff --git a/docs/.generated/plugin-sdk-api-baseline/reply-runtime.json b/docs/.generated/plugin-sdk-api-baseline/reply-runtime.json index 4415972a7738..1ec462d8c0b0 100644 --- a/docs/.generated/plugin-sdk-api-baseline/reply-runtime.json +++ b/docs/.generated/plugin-sdk-api-baseline/reply-runtime.json @@ -1 +1 @@ -{"contentHash":"3517a6a2ecc546ffac96bec9cfdaab32a08848c150794149581ca4b3932545eb","entrypoint":"reply-runtime","importSpecifier":"openclaw/plugin-sdk/reply-runtime"} +{"contentHash":"d07602091d9f19dc5a53f2df01557a1fb579ea4b298caf596ce9d4e6eb3bef8d","entrypoint":"reply-runtime","importSpecifier":"openclaw/plugin-sdk/reply-runtime"} diff --git a/docs/.generated/plugin-sdk-api-baseline/runtime-store.json b/docs/.generated/plugin-sdk-api-baseline/runtime-store.json index cf1a21675c34..03b6ffc1fe46 100644 --- a/docs/.generated/plugin-sdk-api-baseline/runtime-store.json +++ b/docs/.generated/plugin-sdk-api-baseline/runtime-store.json @@ -1 +1 @@ -{"contentHash":"db430e3abd5c061bbb057dcbc5d542d22ce33ee62a07170f8bac5fcbd4c92e8d","entrypoint":"runtime-store","importSpecifier":"openclaw/plugin-sdk/runtime-store"} +{"contentHash":"6acb8e3118fe9f7a8bf0c3fa16838b0e9c8b69bf440b84c0757d4789a66950bc","entrypoint":"runtime-store","importSpecifier":"openclaw/plugin-sdk/runtime-store"} diff --git a/docs/.generated/plugin-sdk-api-baseline/session-catalog.json b/docs/.generated/plugin-sdk-api-baseline/session-catalog.json index cc24dece7f7e..4cf56be5d1c3 100644 --- a/docs/.generated/plugin-sdk-api-baseline/session-catalog.json +++ b/docs/.generated/plugin-sdk-api-baseline/session-catalog.json @@ -1 +1 @@ -{"contentHash":"791e6d3eccf652c146c79a743edaf6bc7cb5b6fdd313dade78faaf329b5507a1","entrypoint":"session-catalog","importSpecifier":"openclaw/plugin-sdk/session-catalog"} +{"contentHash":"3b52fbfd0855f234f80dce3c062c8d0b48f18d512aa7a26223c9c62b8056848e","entrypoint":"session-catalog","importSpecifier":"openclaw/plugin-sdk/session-catalog"} diff --git a/docs/.generated/plugin-sdk-api-baseline/session-store-runtime.json b/docs/.generated/plugin-sdk-api-baseline/session-store-runtime.json index 85a3c06a3704..2aa3a56bb9c7 100644 --- a/docs/.generated/plugin-sdk-api-baseline/session-store-runtime.json +++ b/docs/.generated/plugin-sdk-api-baseline/session-store-runtime.json @@ -1 +1 @@ -{"contentHash":"552e06b12f79cbcd09e31fd40be2316546dfeb7a1ea6235b06de4f33db3b721b","entrypoint":"session-store-runtime","importSpecifier":"openclaw/plugin-sdk/session-store-runtime"} +{"contentHash":"3da3531231ad1e533b81a215f53d3feb555a3c73420bc6d5e13d170edb8dedcc","entrypoint":"session-store-runtime","importSpecifier":"openclaw/plugin-sdk/session-store-runtime"} diff --git a/docs/.generated/plugin-sdk-api-baseline/tool-plugin.json b/docs/.generated/plugin-sdk-api-baseline/tool-plugin.json index dcf6b2700e4d..e24f107c3e89 100644 --- a/docs/.generated/plugin-sdk-api-baseline/tool-plugin.json +++ b/docs/.generated/plugin-sdk-api-baseline/tool-plugin.json @@ -1 +1 @@ -{"contentHash":"92132ddbd26e3d81621f59004521f3875158b4704ee0b9e39f4798766f229702","entrypoint":"tool-plugin","importSpecifier":"openclaw/plugin-sdk/tool-plugin"} +{"contentHash":"d2d5d792a37398f060d0e069077d794d7e2d91a23fb0a4849ec66b8591f86daa","entrypoint":"tool-plugin","importSpecifier":"openclaw/plugin-sdk/tool-plugin"} diff --git a/docs/.generated/plugin-sdk-api-baseline/webhook-ingress.json b/docs/.generated/plugin-sdk-api-baseline/webhook-ingress.json index decd9ed10322..000f288bb039 100644 --- a/docs/.generated/plugin-sdk-api-baseline/webhook-ingress.json +++ b/docs/.generated/plugin-sdk-api-baseline/webhook-ingress.json @@ -1 +1 @@ -{"contentHash":"50ffb05c64c0f2898b3543900132261e152eda3999462154360efc3db7194359","entrypoint":"webhook-ingress","importSpecifier":"openclaw/plugin-sdk/webhook-ingress"} +{"contentHash":"ae862c22e6d6f96239ea1666faa208c5445aef89b6f3bc4425e3a259c3c6cb2b","entrypoint":"webhook-ingress","importSpecifier":"openclaw/plugin-sdk/webhook-ingress"} diff --git a/docs/plugins/hooks.md b/docs/plugins/hooks.md index 21e00f380330..7fa3a355cf9c 100644 --- a/docs/plugins/hooks.md +++ b/docs/plugins/hooks.md @@ -755,7 +755,8 @@ Use message hooks for channel-level routing and delivery policy: - `message_received`: observe inbound content, sender, `threadId`, `messageId`, `senderId`, optional run/session correlation, ordered `media`, - and metadata. + normalized `location`, stable `providerUpdate` identity when supplied by the + channel, and metadata. - `message_sending`: rewrite `content` or return `{ cancel: true }`. - `reply_payload_sending`: rewrite normalized `ReplyPayload` objects (including `presentation`, `delivery`, media refs, and text) or return diff --git a/extensions/telegram/src/bot-handlers.inbound-pipeline.ts b/extensions/telegram/src/bot-handlers.inbound-pipeline.ts index 37d6975b5cbe..78fa3e1dfdeb 100644 --- a/extensions/telegram/src/bot-handlers.inbound-pipeline.ts +++ b/extensions/telegram/src/bot-handlers.inbound-pipeline.ts @@ -24,11 +24,12 @@ import { } from "./bot/helpers.js"; import { TelegramPairingStoreReadError } from "./bot/helpers.js"; import type { TelegramContext, TelegramGetChat } from "./bot/types.js"; +import { emitTelegramLiveLocationMessageHook } from "./location-message-hook.js"; import type { TelegramMessageDispatchReplayClaim } from "./message-dispatch-dedupe.js"; type TelegramMessageHandlerParams = Pick< RegisterTelegramHandlerParams, - "bot" | "shouldSkipUpdate" + "accountId" | "bot" | "shouldSkipUpdate" > & { opts: Pick; runtime: Pick; @@ -57,7 +58,7 @@ interface TelegramInboundHandlers { } function createTelegramInboundHandlers( - { bot, opts, runtime, shouldSkipUpdate }: TelegramMessageHandlerParams, + { accountId, bot, opts, runtime, shouldSkipUpdate }: TelegramMessageHandlerParams, messageRuntime: TelegramMessageHandlerRuntime, authorizationRuntime: Pick, inboundRuntime: Pick, @@ -128,6 +129,7 @@ function createTelegramInboundHandlers( msg: Message; requireConfiguredGroup: boolean; botUserId: number; + providerUpdate?: { id: number; kind: "edited_message" | "edited_channel_post" }; }) => { if (shouldSkipUpdate(params.ctxForDedupe)) { return; @@ -157,6 +159,15 @@ function createTelegramInboundHandlers( return; } await recordMessageForReplyChain(normalizedMsg, gate.context.threadSpec, params.botUserId); + if (params.providerUpdate) { + emitTelegramLiveLocationMessageHook({ + accountId, + msg: normalizedMsg, + updateId: params.providerUpdate.id, + updateKind: params.providerUpdate.kind, + isForum, + }); + } }; const handleInboundMessageLike = async ( @@ -319,6 +330,10 @@ function createTelegramInboundHandlers( msg, requireConfiguredGroup: false, botUserId: resolveBotUserId(ctx), + providerUpdate: + typeof ctx.update?.update_id === "number" + ? { id: ctx.update.update_id, kind: "edited_message" } + : undefined, }); return { kind: "recorded" }; }; @@ -364,6 +379,10 @@ function createTelegramInboundHandlers( msg: normalizeChannelPostMessage(post), requireConfiguredGroup: true, botUserId: resolveBotUserId(ctx), + providerUpdate: + typeof ctx.update?.update_id === "number" + ? { id: ctx.update.update_id, kind: "edited_channel_post" } + : undefined, }); return { kind: "recorded" }; }; diff --git a/extensions/telegram/src/bot-handlers.message-context.ts b/extensions/telegram/src/bot-handlers.message-context.ts index 6692675aa08f..88873980d04b 100644 --- a/extensions/telegram/src/bot-handlers.message-context.ts +++ b/extensions/telegram/src/bot-handlers.message-context.ts @@ -153,9 +153,14 @@ export function buildSyntheticTextMessage(params: { } export const buildSyntheticContext = ( - ctx: Pick, + ctx: Pick, message: Message, -): TelegramContext => ({ message, me: ctx.me, getFile: ctx.getFile.bind(ctx) }); +): TelegramContext => ({ + message, + update: ctx.update, + me: ctx.me, + getFile: ctx.getFile.bind(ctx), +}); export function formatTelegramAmbientTranscriptBody( messages: readonly Message[], diff --git a/extensions/telegram/src/bot-message-context.session.ts b/extensions/telegram/src/bot-message-context.session.ts index cecd58ddeec8..f75e9de511bf 100644 --- a/extensions/telegram/src/bot-message-context.session.ts +++ b/extensions/telegram/src/bot-message-context.session.ts @@ -585,6 +585,18 @@ export async function buildTelegramInboundContextPayload(params: { : `telegram:${chatId}`; const telegramTo = buildTelegramInboundOriginTarget(chatId, threadSpec); const locationContext = locationData ? toLocationContext(locationData) : undefined; + const telegramUpdate = primaryCtx.update; + const providerUpdateKind = telegramUpdate + ? "edited_message" in telegramUpdate + ? "edited_message" + : "message" in telegramUpdate + ? "message" + : "edited_channel_post" in telegramUpdate + ? "edited_channel_post" + : "channel_post" in telegramUpdate + ? "channel_post" + : undefined + : undefined; const inboundHistory = hasGroupHistoryContext && historyKey && historyLimit > 0 ? groupHistoryPromptEntries.length > 0 @@ -734,6 +746,18 @@ export async function buildTelegramInboundContextPayload(params: { StickerMediaIncluded: allMedia[0]?.stickerMetadata ? currentMediaFacts.length > 0 : undefined, SkipStickerMediaUnderstanding: stickerCacheHit ? true : undefined, ...locationContext, + ProviderUpdateId: + typeof telegramUpdate?.update_id === "number" + ? String(telegramUpdate.update_id) + : undefined, + ProviderUpdateKind: providerUpdateKind, + ProviderMessageTimestamp: primaryCtx.message?.date + ? primaryCtx.message.date * 1000 + : undefined, + ProviderEditTimestamp: primaryCtx.message?.edit_date + ? primaryCtx.message.edit_date * 1000 + : undefined, + LocationLivePeriodSeconds: primaryCtx.message?.location?.live_period, IsForum: isForum, TopicName: isForum && topicName ? topicName : undefined, }, diff --git a/extensions/telegram/src/bot.media.downloads-media-file-path-no-file-download.e2e.test.ts b/extensions/telegram/src/bot.media.downloads-media-file-path-no-file-download.e2e.test.ts index ba58d6b8f76c..63ae8ac6af0d 100644 --- a/extensions/telegram/src/bot.media.downloads-media-file-path-no-file-download.e2e.test.ts +++ b/extensions/telegram/src/bot.media.downloads-media-file-path-no-file-download.e2e.test.ts @@ -295,6 +295,7 @@ describe("telegram inbound media", () => { const cases = [ { + updateId: 7005, message: { chat: { id: 42, type: "private" as const }, message_id: 5, @@ -313,9 +314,13 @@ describe("telegram inbound media", () => { expect(payload.LocationLon).toBe(2.294351); expect(payload.LocationSource).toBe("pin"); expect(payload.LocationIsLive).toBe(false); + expect(payload.ProviderUpdateId).toBe("7005"); + expect(payload.ProviderUpdateKind).toBe("message"); + expect(payload.ProviderMessageTimestamp).toBe(1736380800000); }, }, { + updateId: 7006, message: { chat: { id: 42, type: "private" as const }, message_id: 6, @@ -338,6 +343,7 @@ describe("telegram inbound media", () => { for (const testCase of cases) { replySpy.mockClear(); await handler({ + update: { update_id: testCase.updateId, message: testCase.message }, message: testCase.message, me: { username: "openclaw_bot" }, getFile: async () => ({ file_path: "unused" }), diff --git a/extensions/telegram/src/bot/types.ts b/extensions/telegram/src/bot/types.ts index fc98bcdef7b9..079b547ecaf8 100644 --- a/extensions/telegram/src/bot/types.ts +++ b/extensions/telegram/src/bot/types.ts @@ -1,6 +1,6 @@ // Telegram type declarations define plugin contracts. import type { Context } from "grammy"; -import type { ChatFullInfo, Message, UserFromGetMe } from "grammy/types"; +import type { ChatFullInfo, Message, Update, UserFromGetMe } from "grammy/types"; /** App-specific stream mode for Telegram stream previews. */ export type TelegramStreamMode = "off" | "partial" | "block" | "progress"; @@ -20,7 +20,7 @@ export type TelegramGetChat = (chatId: number | string) => Promise ({ + hasHooks: vi.fn(() => true), + runInboundClaim: vi.fn(async () => undefined), + runMessageReceived: vi.fn(async () => undefined), +})); + +vi.mock("openclaw/plugin-sdk/plugin-runtime", async (importOriginal) => { + const actual = await importOriginal(); + return { ...actual, getGlobalHookRunner: () => hookRunner }; +}); + +const { emitTelegramLiveLocationMessageHook } = await import("./location-message-hook.js"); + +describe("Telegram location message hooks", () => { + beforeEach(() => { + hookRunner.hasHooks.mockClear(); + hookRunner.runInboundClaim.mockClear(); + hookRunner.runMessageReceived.mockReset(); + hookRunner.runMessageReceived.mockResolvedValue(undefined); + }); + + it("ignores non-location edits", () => { + const msg = { + chat: { id: 1234, type: "private" }, + message_id: 456, + date: 1_786_094_460, + edit_date: 1_786_094_520, + from: { id: 789, is_bot: false, first_name: "Mariano" }, + text: "edited text", + } as Message; + + emitTelegramLiveLocationMessageHook({ + accountId: "main", + msg, + updateId: 9002, + updateKind: "edited_message", + isForum: false, + }); + + expect(hookRunner.runMessageReceived).not.toHaveBeenCalled(); + }); + + it("emits edited live locations through the global message-received contract", () => { + const msg = { + chat: { id: 1234, type: "private" }, + message_id: 456, + date: 1_786_094_460, + edit_date: 1_786_094_520, + from: { id: 789, is_bot: false, first_name: "Pat" }, + location: { + latitude: 43.8376, + longitude: 18.4534, + horizontal_accuracy: 12, + live_period: 900, + }, + } as Message; + + emitTelegramLiveLocationMessageHook({ + accountId: "main", + msg, + updateId: 9002, + updateKind: "edited_message", + isForum: false, + }); + + expect(hookRunner.hasHooks).toHaveBeenCalledWith("message_received", expect.any(Object)); + expect(hookRunner.runMessageReceived).toHaveBeenCalledWith( + expect.objectContaining({ + messageId: "456", + senderId: "789", + timestamp: 1_786_094_520_000, + location: expect.objectContaining({ + latitude: 43.8376, + longitude: 18.4534, + accuracy: 12, + source: "live", + isLive: true, + livePeriodSeconds: 900, + }), + providerUpdate: { + id: "9002", + kind: "edited_message", + messageId: "456", + messageTimestamp: 1_786_094_460_000, + editedTimestamp: 1_786_094_520_000, + }, + }), + expect.objectContaining({ + channelId: "telegram", + accountId: "main", + conversationId: "telegram:1234", + messageId: "456", + senderId: "789", + }), + ); + expect(hookRunner.runInboundClaim).not.toHaveBeenCalled(); + }); + + it("emits the terminal edit when live-location sharing stops", () => { + const msg = { + chat: { id: 1234, type: "private" }, + message_id: 456, + date: 1_786_094_460, + edit_date: 1_786_094_580, + from: { id: 789, is_bot: false, first_name: "Pat" }, + location: { latitude: 43.8376, longitude: 18.4534 }, + } as Message; + + emitTelegramLiveLocationMessageHook({ + accountId: "main", + msg, + updateId: 9003, + updateKind: "edited_message", + isForum: false, + }); + + expect(hookRunner.runMessageReceived).toHaveBeenCalledWith( + expect.objectContaining({ + location: expect.objectContaining({ isLive: false }), + providerUpdate: expect.objectContaining({ id: "9003", kind: "edited_message" }), + }), + expect.any(Object), + ); + expect(hookRunner.runInboundClaim).not.toHaveBeenCalled(); + }); + + it("emits edited channel-post live locations with their provider update kind", () => { + const msg = { + chat: { id: -1001234, type: "supergroup", title: "Travel updates" }, + message_id: 456, + date: 1_786_094_460, + edit_date: 1_786_094_580, + from: { id: 789, is_bot: true, first_name: "Travel updates" }, + location: { latitude: 43.8376, longitude: 18.4534, live_period: 900 }, + } as Message; + + emitTelegramLiveLocationMessageHook({ + accountId: "main", + msg, + updateId: 9004, + updateKind: "edited_channel_post", + isForum: false, + }); + + expect(hookRunner.runMessageReceived).toHaveBeenCalledWith( + expect.objectContaining({ + providerUpdate: expect.objectContaining({ + id: "9004", + kind: "edited_channel_post", + }), + }), + expect.any(Object), + ); + }); + + it("does not wait for plugin observers on the Telegram inbound path", () => { + hookRunner.runMessageReceived.mockReturnValueOnce(new Promise(() => {})); + const msg = { + chat: { id: 1234, type: "private" }, + message_id: 456, + date: 1_786_094_460, + edit_date: 1_786_094_520, + from: { id: 789, is_bot: false, first_name: "Pat" }, + location: { latitude: 43.8376, longitude: 18.4534, live_period: 900 }, + } as Message; + + expect( + emitTelegramLiveLocationMessageHook({ + accountId: "main", + msg, + updateId: 9005, + updateKind: "edited_message", + isForum: false, + }), + ).toBeUndefined(); + expect(hookRunner.runMessageReceived).toHaveBeenCalledTimes(1); + }); +}); diff --git a/extensions/telegram/src/location-message-hook.ts b/extensions/telegram/src/location-message-hook.ts new file mode 100644 index 000000000000..800d5222bab8 --- /dev/null +++ b/extensions/telegram/src/location-message-hook.ts @@ -0,0 +1,93 @@ +// Telegram live-location edits bypass agent dispatch but still need the normal observation hook. +import type { Message } from "grammy/types"; +import { formatLocationText } from "openclaw/plugin-sdk/channel-inbound"; +import { + deriveInboundMessageHookContext, + fireAndForgetHook, + toPluginMessageContext, + toPluginMessageReceivedEvent, +} from "openclaw/plugin-sdk/hook-runtime"; +import { getGlobalHookRunner } from "openclaw/plugin-sdk/plugin-runtime"; +import { extractTelegramLocation } from "./bot/body-helpers.js"; +import { + buildTelegramGroupFrom, + buildTelegramInboundOriginTarget, + resolveTelegramMessageThreadSpec, +} from "./bot/helpers.js"; + +function buildTelegramLocationMessageHook(params: { + accountId: string; + msg: Message; + updateId: number; + updateKind: "message" | "edited_message" | "channel_post" | "edited_channel_post"; + isForum: boolean; +}) { + const location = extractTelegramLocation(params.msg); + if (!location) { + return null; + } + const msg = params.msg; + const isGroup = msg.chat.type === "group" || msg.chat.type === "supergroup"; + const threadSpec = resolveTelegramMessageThreadSpec(msg, params.isForum); + const originatingTo = buildTelegramInboundOriginTarget(msg.chat.id, threadSpec); + const from = isGroup + ? buildTelegramGroupFrom(msg.chat.id, threadSpec.id) + : `telegram:${msg.chat.id}`; + const canonical = deriveInboundMessageHookContext({ + From: from, + To: originatingTo, + OriginatingChannel: "telegram", + OriginatingTo: originatingTo, + Provider: "telegram", + Surface: "telegram", + AccountId: params.accountId, + MessageSid: String(msg.message_id), + MessageSidFull: String(msg.message_id), + SenderId: msg.from?.id != null ? String(msg.from.id) : undefined, + SenderName: [msg.from?.first_name, msg.from?.last_name].filter(Boolean).join(" ") || undefined, + SenderUsername: msg.from?.username, + Timestamp: + params.updateKind.startsWith("edited_") && msg.edit_date + ? msg.edit_date * 1000 + : msg.date + ? msg.date * 1000 + : undefined, + Body: formatLocationText(location), + RawBody: formatLocationText(location), + BodyForAgent: formatLocationText(location), + MessageThreadId: threadSpec.id, + GroupSubject: isGroup ? msg.chat.title : undefined, + LocationLat: location.latitude, + LocationLon: location.longitude, + LocationAccuracy: location.accuracy, + LocationName: location.name, + LocationAddress: location.address, + LocationSource: location.source, + LocationIsLive: location.isLive, + LocationLivePeriodSeconds: msg.location?.live_period, + LocationCaption: location.caption, + ProviderUpdateId: String(params.updateId), + ProviderUpdateKind: params.updateKind, + ProviderMessageTimestamp: msg.date ? msg.date * 1000 : undefined, + ProviderEditTimestamp: msg.edit_date ? msg.edit_date * 1000 : undefined, + CommandAuthorized: false, + }); + return { + event: toPluginMessageReceivedEvent(canonical), + context: toPluginMessageContext(canonical), + }; +} + +export function emitTelegramLiveLocationMessageHook( + params: Parameters[0], +): void { + const pair = buildTelegramLocationMessageHook(params); + const runner = getGlobalHookRunner(); + if (!pair || !runner?.hasHooks("message_received", pair.context)) { + return; + } + fireAndForgetHook( + runner.runMessageReceived(pair.event, pair.context), + "message_received plugin hook failed", + ); +} diff --git a/scripts/plugin-sdk-surface-report.mts b/scripts/plugin-sdk-surface-report.mts index 6c903efe2bf9..d1ee39b9fd83 100644 --- a/scripts/plugin-sdk-surface-report.mts +++ b/scripts/plugin-sdk-surface-report.mts @@ -270,7 +270,8 @@ export function readPluginSdkSurfaceBudgets(env: NodeJS.ProcessEnv = process.env // +1: add the account-aware native approval request selector. // +3: add canonical coercion exports while retaining the shipped asString compatibility name. // +2: add high-use coercion primitives while retaining shipped object-record exports. - 4868, + // +2: channel-neutral location and provider-update hook contracts. + 4870, env, ), publicFunctionExports: readPluginSdkSurfaceBudgetEnv( diff --git a/src/auto-reply/templating.ts b/src/auto-reply/templating.ts index e5d0895a79b3..92a3e9d33a18 100644 --- a/src/auto-reply/templating.ts +++ b/src/auto-reply/templating.ts @@ -325,7 +325,16 @@ export type MsgContext = Partial & { LocationAddress?: string; LocationSource?: string; LocationIsLive?: boolean; + LocationLivePeriodSeconds?: number; LocationCaption?: string; + /** Stable identity of the provider update that carried this message. */ + ProviderUpdateId?: string; + /** Provider update kind, for example `message` or `edited_message`. */ + ProviderUpdateKind?: string; + /** Provider-native timestamp for the original message. */ + ProviderMessageTimestamp?: number; + /** Provider-native timestamp for an edited message update. */ + ProviderEditTimestamp?: number; /** Provider label. */ Provider?: string; /** Provider surface label. Prefer this over `Provider` when available. */ diff --git a/src/hooks/message-hook-mappers.test.ts b/src/hooks/message-hook-mappers.test.ts index c288ec75fdb3..c056b2a4735a 100644 --- a/src/hooks/message-hook-mappers.test.ts +++ b/src/hooks/message-hook-mappers.test.ts @@ -820,4 +820,42 @@ describe("message hook mappers", () => { groupId: "demo-chat:chat:456", }); }); + + it("projects normalized location and stable provider update identity", () => { + const canonical = deriveInboundMessageHookContext( + makeInboundCtx({ + LocationLat: 43.8376, + LocationLon: 18.4534, + LocationAccuracy: 12, + LocationSource: "live", + LocationIsLive: true, + LocationLivePeriodSeconds: 900, + ProviderUpdateId: "9002", + ProviderUpdateKind: "edited_message", + ProviderMessageTimestamp: 1_786_094_460_000, + ProviderEditTimestamp: 1_786_094_520_000, + }), + ); + + const { event } = toPluginInboundClaimPair(canonical); + expect(event.location).toEqual({ + latitude: 43.8376, + longitude: 18.4534, + accuracy: 12, + source: "live", + isLive: true, + livePeriodSeconds: 900, + }); + expect(event.providerUpdate).toEqual({ + id: "9002", + kind: "edited_message", + messageId: "msg-1", + messageTimestamp: 1_786_094_460_000, + editedTimestamp: 1_786_094_520_000, + }); + expect(toPluginMessageReceivedEvent(canonical)).toMatchObject({ + location: event.location, + providerUpdate: event.providerUpdate, + }); + }); }); diff --git a/src/hooks/message-hook-mappers.ts b/src/hooks/message-hook-mappers.ts index 673ecbe04584..d3b388983e38 100644 --- a/src/hooks/message-hook-mappers.ts +++ b/src/hooks/message-hook-mappers.ts @@ -81,6 +81,8 @@ type CanonicalInboundMessageHookContext = { isGroup: boolean; groupId?: string; topicName?: string; + location?: PluginHookInboundClaimEvent["location"]; + providerUpdate?: PluginHookInboundClaimEvent["providerUpdate"]; trace?: DiagnosticTraceContext; callDepth?: number; }; @@ -168,6 +170,17 @@ export function deriveInboundMessageHookContext( const mediaUrls = compact(media.map((fact) => fact.url ?? fact.path)); const mediaTypes = compact(media.map((fact) => fact.contentType ?? fact.kind)); const firstMedia = media[0]; + const hasLocation = + typeof ctx.LocationLat === "number" && + Number.isFinite(ctx.LocationLat) && + typeof ctx.LocationLon === "number" && + Number.isFinite(ctx.LocationLon); + const locationSource = + ctx.LocationSource === "pin" || ctx.LocationSource === "place" || ctx.LocationSource === "live" + ? ctx.LocationSource + : undefined; + const providerUpdateId = normalizeOptionalString(ctx.ProviderUpdateId); + const providerUpdateKind = normalizeOptionalString(ctx.ProviderUpdateKind); return { from: ctx.From ?? "", to: ctx.To, @@ -217,6 +230,43 @@ export function deriveInboundMessageHookContext( isGroup, groupId: isGroup ? conversationId : undefined, topicName: ctx.TopicName, + ...(hasLocation + ? { + location: { + latitude: ctx.LocationLat as number, + longitude: ctx.LocationLon as number, + ...(typeof ctx.LocationAccuracy === "number" ? { accuracy: ctx.LocationAccuracy } : {}), + ...(ctx.LocationName ? { name: ctx.LocationName } : {}), + ...(ctx.LocationAddress ? { address: ctx.LocationAddress } : {}), + ...(locationSource ? { source: locationSource } : {}), + ...(typeof ctx.LocationIsLive === "boolean" ? { isLive: ctx.LocationIsLive } : {}), + ...(typeof ctx.LocationLivePeriodSeconds === "number" && + Number.isFinite(ctx.LocationLivePeriodSeconds) + ? { livePeriodSeconds: ctx.LocationLivePeriodSeconds } + : {}), + ...(ctx.LocationCaption ? { caption: ctx.LocationCaption } : {}), + }, + } + : {}), + ...(providerUpdateId && providerUpdateKind + ? { + providerUpdate: { + id: providerUpdateId, + kind: providerUpdateKind, + ...(normalizeOptionalString(ctx.MessageSidFull ?? ctx.MessageSid) + ? { messageId: normalizeOptionalString(ctx.MessageSidFull ?? ctx.MessageSid) } + : {}), + ...(typeof ctx.ProviderMessageTimestamp === "number" && + Number.isFinite(ctx.ProviderMessageTimestamp) + ? { messageTimestamp: ctx.ProviderMessageTimestamp } + : {}), + ...(typeof ctx.ProviderEditTimestamp === "number" && + Number.isFinite(ctx.ProviderEditTimestamp) + ? { editedTimestamp: ctx.ProviderEditTimestamp } + : {}), + }, + } + : {}), }; } @@ -436,6 +486,8 @@ function buildPluginInboundClaimEvent( isGroup: canonical.isGroup, commandAuthorized: extras?.commandAuthorized, wasMentioned: extras?.wasMentioned, + ...(canonical.location ? { location: { ...canonical.location } } : {}), + ...(canonical.providerUpdate ? { providerUpdate: { ...canonical.providerUpdate } } : {}), ...projectHookMediaState(canonical), metadata: { from: canonical.from, @@ -504,6 +556,8 @@ export function toPluginMessageReceivedEvent( ...(canonical.replyToIsQuote !== undefined ? { replyToIsQuote: canonical.replyToIsQuote } : {}), sessionKey: canonical.sessionKey, runId: canonical.runId, + ...(canonical.location ? { location: { ...canonical.location } } : {}), + ...(canonical.providerUpdate ? { providerUpdate: { ...canonical.providerUpdate } } : {}), ...projectHookMediaState(canonical), metadata: { to: canonical.to, diff --git a/src/plugin-sdk/plugin-entry.ts b/src/plugin-sdk/plugin-entry.ts index f85750cc9528..87a2972bd6df 100644 --- a/src/plugin-sdk/plugin-entry.ts +++ b/src/plugin-sdk/plugin-entry.ts @@ -163,8 +163,10 @@ export type { PluginHookInboundClaimEvent, PluginHookInboundClaimResult, PluginHookInboundMessageMetadata, + PluginHookLocation, PluginHookMediaFact, PluginHookMessageReceivedEvent, + PluginHookProviderUpdate, PluginHookSkillArtifact, PluginHookSkillBundleFile, PluginHookSkillBundleSnapshot, diff --git a/src/plugins/hook-message.types.ts b/src/plugins/hook-message.types.ts index caabb74d856e..bb015dd6f76a 100644 --- a/src/plugins/hook-message.types.ts +++ b/src/plugins/hook-message.types.ts @@ -5,6 +5,28 @@ import type { PluginConversationBinding } from "./conversation-binding.types.js" /** Ordered media fact exposed by inbound message hooks. */ export type PluginHookMediaFact = MessageHookMediaFact; +/** Channel-neutral geographic fix carried by an inbound provider update. */ +export type PluginHookLocation = { + latitude: number; + longitude: number; + accuracy?: number; + name?: string; + address?: string; + source?: "pin" | "place" | "live"; + isLive?: boolean; + livePeriodSeconds?: number; + caption?: string; +}; + +/** Stable provider update identity for transport-level correlation and deduplication. */ +export type PluginHookProviderUpdate = { + id: string; + kind: string; + messageId?: string; + messageTimestamp?: number; + editedTimestamp?: number; +}; + /** Provider metadata plus deprecated media aliases retained during the SDK migration window. */ export type PluginHookInboundMessageMetadata = Record & { /** @deprecated Use the first `event.media` fact with a defined `path`. */ @@ -129,6 +151,8 @@ export type PluginHookInboundClaimEvent = { commandAuthorized?: boolean; senderIsOwner?: boolean; wasMentioned?: boolean; + location?: PluginHookLocation; + providerUpdate?: PluginHookProviderUpdate; /** Staged, locally usable attachments in stable source order. */ media?: PluginHookMediaFact[]; /** Original attachment facts when local staging has not completed yet. */ @@ -156,6 +180,8 @@ export type PluginHookMessageReceivedEvent = { traceId?: string; spanId?: string; parentSpanId?: string; + location?: PluginHookLocation; + providerUpdate?: PluginHookProviderUpdate; /** Staged, locally usable attachments in stable source order. */ media?: PluginHookMediaFact[]; /** Original attachment facts when local staging has not completed yet. */ diff --git a/src/plugins/hook-types.ts b/src/plugins/hook-types.ts index c71633a7372f..cda36ad9c8b0 100644 --- a/src/plugins/hook-types.ts +++ b/src/plugins/hook-types.ts @@ -65,12 +65,14 @@ export type { PluginHookInboundClaimContext, PluginHookInboundClaimEvent, PluginHookInboundMessageMetadata, + PluginHookLocation, PluginHookMediaFact, PluginHookMessageContext, PluginHookMessageReceivedEvent, PluginHookMessageSendingEvent, PluginHookMessageSendingResult, PluginHookMessageSentEvent, + PluginHookProviderUpdate, } from "./hook-message.types.js"; export { PluginApprovalResolutions,