From 6aa27d6ecd8fd7b8892ba331e43a958a2a99109c Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 15 Aug 2026 22:43:47 -0700 Subject: [PATCH] refactor: retire August compat windows (embedding API, pi aliases, target parser, spawning hook, setup exports, WhatsApp inbound aliases) (#124416) * refactor(plugin-sdk): retire embedded Pi aliases * refactor(channels): retire explicit target compatibility * refactor(plugins): retire subagent spawning hook * refactor(plugin-sdk): retire shipped channel setup exports * refactor(whatsapp): retire inbound callback aliases Proof: focused build and WhatsApp E2E green; broad WhatsApp suite 188/189 files green. extensions/whatsapp/src/monitor-inbox.policy.test.ts flakes only in the parallel batch and passes isolated (10/10). * refactor(plugin-sdk): retire memory embedding registrar Migrate every bundled provider and manifest to registerEmbeddingProvider and contracts.embeddingProviders. Preserve memory-specific batching, local-service acquisition, index identity, and auto-selection through the canonical generic registry adapter, then remove the parallel registrar, registry, diagnostics, contracts, tests, and docs. * chore(plugin-sdk): tighten retired surface budgets Pin the post-retirement public SDK surface to 144 entrypoints, 4,312 exports, 2,564 callable exports, and 1,133 deprecated exports; agent-harness-runtime now permits exactly nine deprecated exports. --- docs/plugins/adding-capabilities.md | 5 +- docs/plugins/compatibility.md | 57 +- docs/plugins/hooks.md | 5 - docs/plugins/manifest.md | 4 +- docs/plugins/plugin-inventory.md | 2 +- docs/plugins/reference/amazon-bedrock.md | 2 +- docs/plugins/reference/github-copilot.md | 2 +- docs/plugins/reference/google.md | 2 +- docs/plugins/reference/lmstudio.md | 2 +- docs/plugins/reference/mistral.md | 2 +- docs/plugins/reference/ollama.md | 2 +- docs/plugins/reference/openai.md | 2 +- docs/plugins/reference/voyage.md | 6 +- docs/plugins/sdk-migration.md | 32 +- docs/plugins/sdk-overview.md | 20 +- docs/plugins/sdk-provider-plugins.md | 4 +- docs/plugins/sdk-runtime.md | 2 - docs/plugins/sdk-subpaths.md | 3 +- .../amazon-bedrock/openclaw.plugin.json | 2 +- .../amazon-bedrock/register.sync.runtime.ts | 5 +- extensions/deepinfra/index.test.ts | 1 - extensions/discord/api.ts | 1 - extensions/discord/src/api-barrel.test.ts | 1 - extensions/discord/src/subagent-hooks.test.ts | 346 +------- extensions/discord/src/subagent-hooks.ts | 124 --- extensions/feishu/api.ts | 1 - extensions/feishu/src/subagent-hooks.test.ts | 431 ++-------- extensions/feishu/src/subagent-hooks.ts | 119 --- extensions/github-copilot/index.test.ts | 17 +- extensions/github-copilot/index.ts | 5 +- .../github-copilot/openclaw.plugin.json | 2 +- extensions/google/index.ts | 5 +- extensions/google/openclaw.plugin.json | 2 +- extensions/llama-cpp/index.test.ts | 3 +- extensions/lmstudio/index.ts | 5 +- extensions/lmstudio/openclaw.plugin.json | 2 +- extensions/matrix/index.test.ts | 1 - .../matrix/src/matrix/subagent-hooks.test.ts | 743 +++--------------- .../matrix/src/matrix/subagent-hooks.ts | 175 +---- .../memory-core/src/memory/embeddings.test.ts | 30 +- .../generic-embedding-provider.bridge.test.ts | 11 - ...ric-embedding-provider.integration.test.ts | 24 - .../src/memory/manager-index.test-support.ts | 2 +- ...anager-provider-lifecycle-fallback.test.ts | 2 +- .../src/memory/manager.watcher-config.test.ts | 2 +- extensions/mistral/index.ts | 5 +- extensions/mistral/openclaw.plugin.json | 2 +- extensions/ollama/index.ts | 9 +- extensions/ollama/lazy-import.test.ts | 6 +- extensions/ollama/openclaw.plugin.json | 2 +- extensions/openai/index.ts | 5 +- extensions/openai/openclaw.plugin.json | 2 +- .../scenarios/scenario-runtime-thread.ts | 6 - extensions/voyage/index.ts | 5 +- extensions/voyage/openclaw.plugin.json | 2 +- extensions/whatsapp/api.ts | 2 - extensions/whatsapp/runtime-api.ts | 2 - ...uto-reply.broadcast-groups.test-harness.ts | 4 +- .../whatsapp/src/auto-reply.test-harness.ts | 20 +- ...o-reply.connection-and-logging.e2e.test.ts | 71 +- extensions/whatsapp/src/auto-reply/monitor.ts | 22 +- .../on-message.audio-preflight.test.ts | 129 +-- .../src/auto-reply/monitor/on-message.ts | 46 +- extensions/whatsapp/src/inbound.ts | 2 - .../whatsapp/src/inbound/admission-types.ts | 18 - extensions/whatsapp/src/inbound/admission.ts | 99 --- .../src/inbound/message-aliases.test.ts | 440 ----------- .../whatsapp/src/inbound/message-aliases.ts | 468 ----------- .../whatsapp/src/inbound/message-debounce.ts | 11 +- .../whatsapp/src/inbound/message-delivery.ts | 113 ++- extensions/whatsapp/src/inbound/monitor.ts | 13 +- .../src/inbound/test-message.test-helper.ts | 48 +- extensions/whatsapp/src/inbound/types.ts | 134 +--- .../monitor-inbox.delivery-and-dedupe.test.ts | 10 +- .../monitor-inbox.socket-lifecycle.test.ts | 8 +- ...x.streams-inbound-messages.test-support.ts | 8 - package.json | 4 - .../memory-host-sdk/src/engine-embeddings.ts | 1 - .../src/host/openclaw-runtime-memory.ts | 1 - .../src/host/openclaw-runtime.ts | 1 - .../plugins/kitchen-sink-live-openai.yaml | 1 - .../lib/kitchen-sink-plugin/assertions.mjs | 1 - scripts/generate-plugin-inventory-doc.mts | 2 +- .../official-external-provider-catalog.json | 4 +- ...plugin-sdk-deprecated-public-subpaths.json | 1 - scripts/lib/plugin-sdk-entrypoints.json | 1 - scripts/plugin-sdk-surface-report.mts | 22 +- src/agents/memory-search.test.ts | 28 +- ...subagents.sessions-spawn.lifecycle.test.ts | 3 - .../spawn/subagent-spawn.test-helpers.ts | 2 +- src/auto-reply/reply/group-id.test.ts | 56 +- src/auto-reply/reply/group-id.ts | 39 +- src/channels/conversation-resolution.test.ts | 25 - src/channels/conversation-resolution.ts | 13 - .../test-helpers/surface-contract-suite.ts | 1 - .../setup-wizard-legacy-compat.test.ts | 74 -- .../plugins/setup-wizard-legacy-compat.ts | 181 ----- .../plugins/target-parsing-loaded.test.ts | 68 -- src/channels/plugins/target-parsing-loaded.ts | 33 - src/channels/plugins/types.core.ts | 10 - .../missing-configured-plugin-install.ids.ts | 12 +- .../shared/startup-plugin-convergence-plan.ts | 2 +- src/commands/status-json-payload.test.ts | 8 +- src/commands/status-json-runtime.test.ts | 8 +- src/commands/status.scan-result.test.ts | 6 +- .../isolated-agent/delivery-target.test.ts | 51 -- src/cron/isolated-agent/delivery-target.ts | 19 +- src/gateway/embeddings-http.test.ts | 12 +- src/gateway/server-startup-plugins.test.ts | 5 +- src/gateway/server-startup-plugins.ts | 2 +- .../outbound/outbound-session.test-helpers.ts | 11 - src/infra/outbound/outbound-session.test.ts | 12 - src/infra/outbound/outbound-session.ts | 15 - src/infra/outbound/targets-session.ts | 25 +- src/infra/outbound/targets.test.ts | 57 -- src/plugin-sdk/agent-harness-runtime.ts | 18 +- .../bundled-channel-config-schema.ts | 19 +- src/plugin-sdk/channel-route.test.ts | 23 - src/plugin-sdk/channel-route.ts | 57 -- .../memory-core-host-engine-embeddings.ts | 9 +- src/plugin-sdk/messaging-targets.ts | 15 - src/plugin-sdk/plugin-test-api.ts | 1 - src/plugin-sdk/plugin-test-runtime.ts | 6 - src/plugin-sdk/setup-runtime.ts | 4 - src/plugin-sdk/shipped-channel-compat.test.ts | 30 - .../test-helpers/plugin-runtime-mock.ts | 1 - .../plugin-state-store.runtime.test.ts | 1 - src/plugins/api-builder.ts | 5 - .../capability-provider-runtime.test.ts | 23 +- src/plugins/capability-provider-runtime.ts | 34 +- src/plugins/captured-registration.test.ts | 1 - src/plugins/captured-registration.ts | 7 - src/plugins/channel-plugin-ids.test.ts | 4 +- src/plugins/compat/registry-records.ts | 80 +- src/plugins/compat/registry.test.ts | 115 ++- src/plugins/compat/registry.ts | 12 - .../config-footprint-guardrails.test.ts | 7 +- .../core-extension-facade-boundary.test.ts | 2 +- ...memory-embedding-provider.contract.test.ts | 162 ---- ...in-sdk-package-contract-guardrails.test.ts | 71 -- .../plugin-sdk-runtime-api-guardrails.test.ts | 2 +- .../contracts/plugin-sdk-subpaths.test.ts | 2 +- src/plugins/embedding-provider-config.ts | 2 +- .../embedding-provider-runtime-shared.ts | 2 +- src/plugins/embedding-providers.test.ts | 4 +- .../gateway-startup-plugin-activation.ts | 6 - .../gateway-startup-plugin-providers.ts | 10 +- src/plugins/hook-types.ts | 81 -- src/plugins/hooks.ts | 44 +- .../installed-plugin-index-scope-lookup.ts | 1 - src/plugins/loader-records.test.ts | 2 - src/plugins/loader-records.ts | 1 - .../loader.hooks-and-runtime.test-utils.ts | 31 - src/plugins/loader.registration.test-utils.ts | 18 - src/plugins/loader.runtime-registry.test.ts | 12 +- .../manifest-capability-normalizers.ts | 1 - src/plugins/manifest-registry.ts | 2 - src/plugins/manifest-types.ts | 1 - .../memory-embedding-provider-runtime.test.ts | 95 ++- .../memory-embedding-provider-runtime.ts | 58 +- .../memory-embedding-providers.test.ts | 180 ----- src/plugins/memory-embedding-providers.ts | 139 ++-- .../official-external-plugin-catalog.test.ts | 9 +- .../official-external-plugin-catalog.ts | 1 - src/plugins/plugin-api.types.ts | 8 - src/plugins/registry-api.ts | 3 - src/plugins/registry-contribution-types.ts | 5 - src/plugins/registry-empty.ts | 1 - src/plugins/registry-registrars-memory.ts | 40 - .../registry-registrars-tools-hooks.ts | 30 - src/plugins/registry-runtime.ts | 5 - src/plugins/registry-types.ts | 5 - src/plugins/registry.runtime-config.test.ts | 1 - src/plugins/registry.test-fixtures.ts | 2 - src/plugins/runtime/index.test.ts | 2 - src/plugins/runtime/runtime-agent.ts | 9 +- .../runtime/runtime-registry-loader.test.ts | 6 +- src/plugins/runtime/types-core.ts | 2 - src/plugins/status-snapshot.ts | 1 - src/plugins/status.test-fixtures.ts | 10 - src/plugins/status.test-helpers.ts | 1 - src/plugins/status.test.ts | 70 -- src/plugins/status.ts | 24 +- src/plugins/wired-hooks-subagent.test.ts | 104 +-- src/trajectory/metadata.test.ts | 1 - src/trajectory/metadata.ts | 1 - 186 files changed, 866 insertions(+), 5621 deletions(-) delete mode 100644 extensions/whatsapp/src/inbound/admission-types.ts delete mode 100644 extensions/whatsapp/src/inbound/message-aliases.test.ts delete mode 100644 extensions/whatsapp/src/inbound/message-aliases.ts delete mode 100644 src/channels/plugins/setup-wizard-legacy-compat.test.ts delete mode 100644 src/channels/plugins/setup-wizard-legacy-compat.ts delete mode 100644 src/channels/plugins/target-parsing-loaded.test.ts delete mode 100644 src/channels/plugins/target-parsing-loaded.ts delete mode 100644 src/plugin-sdk/messaging-targets.ts delete mode 100644 src/plugin-sdk/shipped-channel-compat.test.ts delete mode 100644 src/plugins/contracts/memory-embedding-provider.contract.test.ts delete mode 100644 src/plugins/memory-embedding-providers.test.ts diff --git a/docs/plugins/adding-capabilities.md b/docs/plugins/adding-capabilities.md index be719e27ae82..8fe6d3f326df 100644 --- a/docs/plugins/adding-capabilities.md +++ b/docs/plugins/adding-capabilities.md @@ -109,8 +109,9 @@ can consume embeddings without depending on the memory engine. Memory search also consumes generic `embeddingProviders`. The older memory-specific registration API and `memoryEmbeddingProviders` -contract are deprecated. Use `registerEmbeddingProvider` and -`embeddingProviders` for all new embedding providers. +contract were removed after their August 2026 migration window. Use +`registerEmbeddingProvider` and `embeddingProviders` for every embedding +provider. ## Review checklist diff --git a/docs/plugins/compatibility.md b/docs/plugins/compatibility.md index 8b8ce7953e93..7d918c02fc45 100644 --- a/docs/plugins/compatibility.md +++ b/docs/plugins/compatibility.md @@ -79,11 +79,7 @@ separately tracked so supported upgrade paths can still repair old config. The remaining dated compatibility areas are: - the September SDK subpath window listed in the migration guide -- the `api.on("subagent_spawning", ...)` hook alias -- memory-specific embedding registration and the beta.5 session-store bridge -- WhatsApp inbound callback aliases described below -- explicit channel target parsing and `openclaw/plugin-sdk/messaging-targets` -- embedded Pi agent aliases +- the beta.5 session-store bridge - the shipped agent-harness SDK aliases, whose removal is pending a new externally documented migration decision - the October 2026 SDK annotation families listed below @@ -132,50 +128,17 @@ runtime context. The security runtime similarly exports `buildChannelMetadata`; the deprecated `buildUntrustedChannelMetadata` alias remains available on the same schedule. -### WhatsApp inbound callback flat aliases +### WhatsApp inbound callback retirement -WhatsApp runtime callbacks deliver `WebInboundMessage`: the canonical -nested `event`, `payload`, `quote`, `group`, and `platform` contexts plus -deprecated flat aliases for the shipped callback fields. New callback code -should read the nested contexts. Code that constructs clean nested callback -messages can use `WebInboundCallbackMessage`; compatibility listeners that -still inject old flat test or plugin messages should use -`LegacyFlatWebInboundMessage` or `WebInboundMessageInput`. +The August 2026 WhatsApp callback compatibility window is closed. Runtime +callbacks now accept only `WebInboundCallbackMessage`: nested `event`, +`payload`, `quote`, `group`, and `platform` contexts plus the required public +`admission` envelope. Flat callback fields and top-level admission aliases are +no longer accepted. -The flat aliases remain available until **2026-08-30**; that window applies -only to flat alias access, not to the nested shape, which is the canonical -runtime contract. Each flat alias's TypeScript `@deprecated` annotation -names its exact nested replacement. Common examples: - -- `id`, `timestamp`, and `isBatched` move under `event`. -- `body`, `mediaPath`, `mediaType`, `mediaFileName`, `mediaUrl`, `location`, - and `channelStructuredContext` move under `payload`. -- `to`, `chatId`, sender/self fields, `sendComposing`, `reply(...)`, and - `sendMedia(...)` move under `platform`. -- `replyTo*` fields move under `quote`; group subject/participant/mention - fields move under `group`. - -`payload.channelStructuredContext` is extracted from inbound provider -payloads. Plugins should inspect `label`, `source`, and `type` before -treating its `payload` as authoritative. - -### WhatsApp inbound admission fields - -Accepted WhatsApp callback messages carry `admission`, a public-safe -envelope for the access-control decision that admitted the message. New -callback code should read admission facts from `msg.admission` instead of -the older top-level admission fields. - -The top-level fields remain available until **2026-08-30**. Each field's -TypeScript `@deprecated` annotation names its replacement: - -- `from` and `conversationId` move to `admission.conversation.id`. -- `accountId` moves to `admission.accountId`. -- `accessControlPassed` is a derived compatibility view of - `admission.ingress.decision === "allow"`; on messages that already carry - `admission`, writing the legacy boolean does not rewrite the ingress - graph. -- `chatType` moves to `admission.conversation.kind`. +`payload.channelStructuredContext` is extracted from inbound provider payloads. +Plugins should inspect `label`, `source`, and `type` before treating its +`payload` as authoritative. ## Plugin inspector package diff --git a/docs/plugins/hooks.md b/docs/plugins/hooks.md index eb72d9bb1061..2887d9730fe3 100644 --- a/docs/plugins/hooks.md +++ b/docs/plugins/hooks.md @@ -203,7 +203,6 @@ For `sessions.create` calls with `parentSessionKey` and `emitCommandHooks: true` - `subagent_spawned` / `subagent_ended` - observe subagent launch and completion. - `subagent_delivery_target` - compatibility hook for completion delivery when no core session binding can project a route. -- `subagent_spawning` - deprecated compatibility hook. Core now prepares `thread: true` subagent bindings through channel session-binding adapters before `subagent_spawned` fires. - `subagent_spawned` includes `resolvedModel` and `resolvedProvider` when OpenClaw has resolved the child session's native model before launch. - `subagent_ended` carries `targetSessionKey` (identity - matches `subagent_spawned.childSessionKey`), `targetKind` (`"subagent"` or `"acp"`), `reason`, optional `outcome` (`"ok"`, `"error"`, `"timeout"`, `"killed"`, `"reset"`, or `"deleted"`), optional `error`, `runId`, `endedAt`, `accountId`, and `sendFarewell`. It does **not** include `agentId` or `childSessionKey`; use `targetSessionKey` to correlate with the matching `subagent_spawned` event. @@ -1055,10 +1054,6 @@ before the next major release: handlers. Read `BodyForAgent` and the structured user-context blocks instead of parsing flat envelope text. See [Plaintext channel envelopes → BodyForAgent](/plugins/sdk-migration#removal-timeline). -- **`subagent_spawning`** remains for compatibility with older plugins, but - new plugins should not return thread routing from it. Core prepares - `thread: true` subagent bindings through channel session-binding adapters - before `subagent_spawned` fires. - **`onResolution` in `before_tool_call`** now uses the typed `PluginApprovalResolution` union (`allow-once` / `allow-always` / `deny` / `timeout` / `cancelled`) instead of a free-form `string`. diff --git a/docs/plugins/manifest.md b/docs/plugins/manifest.md index cff2bf0368a0..e7badcebf962 100644 --- a/docs/plugins/manifest.md +++ b/docs/plugins/manifest.md @@ -666,7 +666,6 @@ Use `contracts` only for static capability ownership metadata that OpenClaw can "speechProviders": ["openai"], "realtimeTranscriptionProviders": ["openai"], "realtimeVoiceProviders": ["openai"], - "memoryEmbeddingProviders": ["local"], "mediaUnderstandingProviders": ["openai"], "imageGenerationProviders": ["openai"], "videoGenerationProviders": ["qwen"], @@ -696,7 +695,6 @@ Each list is optional: | `speechProviders` | `string[]` | Speech provider ids this plugin owns. | | `realtimeTranscriptionProviders` | `string[]` | Realtime-transcription provider ids this plugin owns. | | `realtimeVoiceProviders` | `string[]` | Realtime-voice provider ids this plugin owns. | -| `memoryEmbeddingProviders` | `string[]` | Deprecated memory-specific embedding provider ids this plugin owns. | | `mediaUnderstandingProviders` | `string[]` | Media-understanding provider ids this plugin owns. | | `transcriptSourceProviders` | `string[]` | Transcript source provider ids this plugin owns. | | `documentExtractors` | `string[]` | Document (for example PDF) extractor provider ids this plugin owns. | @@ -722,7 +720,7 @@ Provider plugins that implement `resolveExternalAuthProfiles` should declare `co Provider plugins that implement both `resolveUsageAuth` and `fetchUsageSnapshot` should declare each auto-discovered provider id in `contracts.usageProviders`. Usage discovery reads this contract before loading runtime code, then verifies both hooks after loading only the declared owners. -General embedding providers should declare `contracts.embeddingProviders` for each adapter registered with `api.registerEmbeddingProvider(...)`. Use the general contract for reusable vector generation, including providers consumed by memory search. `contracts.memoryEmbeddingProviders` is deprecated memory-specific compatibility and remains only while existing providers migrate to the generic embedding provider seam. +Embedding providers must declare `contracts.embeddingProviders` for each adapter registered with `api.registerEmbeddingProvider(...)`. The same generic contract serves reusable vector generation and memory search. The retired `contracts.memoryEmbeddingProviders` key is no longer accepted. Worker providers must declare each `api.registerWorkerProvider(...)` id in `contracts.workerProviders`. Core persists durable intent before calling `provision`; providers validate their settings before external allocation, and repeated calls with the same operation id must adopt the same lease. Providers whose bounded provisioning exceeds core's five-minute default may implement `resolveProvisionTimeoutMs(profile)` and include acquisition, provider-owned setup, and cleanup in the returned positive millisecond budget. Core also persists that validated settings snapshot and passes it with `leaseId` to `inspect({ leaseId, profile })` and `destroy({ leaseId, profile })`, including after the named profile is changed or removed. Destruction is idempotent, inspection returns the closed `active` / `destroyed` / `unknown` status union, and SSH private-key material is referenced only through `SecretRef`. Provisioned SSH endpoints must also include a public `hostKey` from trusted provisioning output as exactly `algorithm base64`, without a hostname or comment, so core can pin the host before connecting. They may include up to 10 ordered, unique `fallbackPorts`, excluding the primary `port`; core persists those candidates and rotates among them only for idempotent probes, content-addressed transfers, receipt/lock-guarded artifact installation, convergent managed-worktree mirroring, and tunnel reconnects. Ambiguous unguarded stateful commands fail closed and are not replayed across candidates. A lease may set `sharedHost: true` when the SSH account also owns unrelated processes; core then avoids host-wide process freezing during workspace reconciliation. Omitted or `false` means a dedicated worker host. Active inspection repeats this fact so core can reconcile provider-owned isolation for leases persisted before the field existed; tunnel startup waits for that first authoritative inspection. Optional desktop metadata may advertise up to eight unique closed apps: `browser` with an absolute `executablePath` and a CDP port from 1 through 65535, or `terminal` with an absolute `executablePath`. Core rejects unknown app ids and fields and persists the validated metadata with the existing desktop record. Providers that mint dynamic identity refs may implement authoritative `resolveSshIdentity({ leaseId, profile, keyRef })`; providers without it use core's generic secret resolver. An authoritative `unknown` orphans an active local record; after a persisted destroy request it confirms teardown. diff --git a/docs/plugins/plugin-inventory.md b/docs/plugins/plugin-inventory.md index 1502f18ccd5d..bdb340011a7e 100644 --- a/docs/plugins/plugin-inventory.md +++ b/docs/plugins/plugin-inventory.md @@ -338,7 +338,7 @@ Each entry lists the package, distribution route, and description. - **[volcengine](/plugins/reference/volcengine)** (`@openclaw/volcengine-provider`) - npm; ClawHub: `clawhub:@openclaw/volcengine-provider`. Adds Volcengine, Volcengine Plan model provider support to OpenClaw. -- **[voyage](/plugins/reference/voyage)** (`@openclaw/voyage-provider`) - npm; ClawHub: `clawhub:@openclaw/voyage-provider`. Adds memory embedding provider support. +- **[voyage](/plugins/reference/voyage)** (`@openclaw/voyage-provider`) - npm; ClawHub: `clawhub:@openclaw/voyage-provider`. Adds embedding provider support, including memory search. - **[vydra](/plugins/reference/vydra)** (`@openclaw/vydra-provider`) - npm; ClawHub: `clawhub:@openclaw/vydra-provider`. Adds Vydra model provider support to OpenClaw. diff --git a/docs/plugins/reference/amazon-bedrock.md b/docs/plugins/reference/amazon-bedrock.md index 8f750f015bdb..154408eceb5a 100644 --- a/docs/plugins/reference/amazon-bedrock.md +++ b/docs/plugins/reference/amazon-bedrock.md @@ -16,7 +16,7 @@ OpenClaw Amazon Bedrock provider plugin with model discovery, embeddings, and gu ## Surface -providers: `amazon-bedrock`; contracts: `memoryEmbeddingProviders` +providers: `amazon-bedrock`; contracts: `embeddingProviders` ## Related docs diff --git a/docs/plugins/reference/github-copilot.md b/docs/plugins/reference/github-copilot.md index 48bcc9ffc0d5..97d9fbac8035 100644 --- a/docs/plugins/reference/github-copilot.md +++ b/docs/plugins/reference/github-copilot.md @@ -16,7 +16,7 @@ Adds GitHub Copilot model provider support to OpenClaw. ## Surface -providers: `github-copilot`; contracts: `memoryEmbeddingProviders`, `usageProviders` +providers: `github-copilot`; contracts: `embeddingProviders`, `usageProviders` ## Related docs diff --git a/docs/plugins/reference/google.md b/docs/plugins/reference/google.md index e2f09c35afd8..81a9afbaac65 100644 --- a/docs/plugins/reference/google.md +++ b/docs/plugins/reference/google.md @@ -16,7 +16,7 @@ Adds Google, Google Gemini CLI, Google Vertex model provider support to OpenClaw ## Surface -providers: `google`, `google-gemini-cli`, `google-vertex`; contracts: `imageGenerationProviders`, `mediaUnderstandingProviders`, `memoryEmbeddingProviders`, `musicGenerationProviders`, `realtimeVoiceProviders`, `speechProviders`, `videoGenerationProviders`, `webSearchProviders` +providers: `google`, `google-gemini-cli`, `google-vertex`; contracts: `embeddingProviders`, `imageGenerationProviders`, `mediaUnderstandingProviders`, `musicGenerationProviders`, `realtimeVoiceProviders`, `speechProviders`, `videoGenerationProviders`, `webSearchProviders` ## Related docs diff --git a/docs/plugins/reference/lmstudio.md b/docs/plugins/reference/lmstudio.md index eb531ae99bf1..0531a50a4949 100644 --- a/docs/plugins/reference/lmstudio.md +++ b/docs/plugins/reference/lmstudio.md @@ -16,7 +16,7 @@ Adds LM Studio model provider support to OpenClaw. ## Surface -providers: `lmstudio`; contracts: `memoryEmbeddingProviders` +providers: `lmstudio`; contracts: `embeddingProviders` ## Related docs diff --git a/docs/plugins/reference/mistral.md b/docs/plugins/reference/mistral.md index 7226e986dd00..e51b2b342069 100644 --- a/docs/plugins/reference/mistral.md +++ b/docs/plugins/reference/mistral.md @@ -16,7 +16,7 @@ Adds Mistral model provider support to OpenClaw. ## Surface -providers: `mistral`; contracts: `mediaUnderstandingProviders`, `memoryEmbeddingProviders`, `realtimeTranscriptionProviders` +providers: `mistral`; contracts: `embeddingProviders`, `mediaUnderstandingProviders`, `realtimeTranscriptionProviders` ## Related docs diff --git a/docs/plugins/reference/ollama.md b/docs/plugins/reference/ollama.md index 1b85dcf62ff7..c72bdd90c63d 100644 --- a/docs/plugins/reference/ollama.md +++ b/docs/plugins/reference/ollama.md @@ -16,7 +16,7 @@ Adds Ollama, Ollama Cloud model provider support to OpenClaw. ## Surface -providers: `ollama`, `ollama-cloud`; contracts: `memoryEmbeddingProviders`, `tools`, `webSearchProviders` +providers: `ollama`, `ollama-cloud`; contracts: `embeddingProviders`, `tools`, `webSearchProviders` ## Related docs diff --git a/docs/plugins/reference/openai.md b/docs/plugins/reference/openai.md index 895cba4a844b..40dae8acda1c 100644 --- a/docs/plugins/reference/openai.md +++ b/docs/plugins/reference/openai.md @@ -16,7 +16,7 @@ Adds OpenAI model provider support to OpenClaw. ## Surface -providers: `openai`; contracts: `imageGenerationProviders`, `mediaUnderstandingProviders`, `memoryEmbeddingProviders`, `realtimeTranscriptionProviders`, `realtimeVoiceProviders`, `speechProviders`, `usageProviders`, `videoGenerationProviders` +providers: `openai`; contracts: `embeddingProviders`, `imageGenerationProviders`, `mediaUnderstandingProviders`, `realtimeTranscriptionProviders`, `realtimeVoiceProviders`, `speechProviders`, `usageProviders`, `videoGenerationProviders` ## Related docs diff --git a/docs/plugins/reference/voyage.md b/docs/plugins/reference/voyage.md index c4220ee48600..c35ea8a5452c 100644 --- a/docs/plugins/reference/voyage.md +++ b/docs/plugins/reference/voyage.md @@ -1,5 +1,5 @@ --- -summary: "Adds memory embedding provider support." +summary: "Adds embedding provider support, including memory search." read_when: - You are installing, configuring, or auditing the voyage plugin title: "Voyage plugin" @@ -7,7 +7,7 @@ title: "Voyage plugin" # Voyage plugin -Adds memory embedding provider support. +Adds embedding provider support, including memory search. ## Distribution @@ -16,4 +16,4 @@ Adds memory embedding provider support. ## Surface -contracts: `memoryEmbeddingProviders` +contracts: `embeddingProviders` diff --git a/docs/plugins/sdk-migration.md b/docs/plugins/sdk-migration.md index 50d82e4a208f..164b4f06a6aa 100644 --- a/docs/plugins/sdk-migration.md +++ b/docs/plugins/sdk-migration.md @@ -142,17 +142,15 @@ review date; removal still requires the reader condition in the final column. ### Published channel setup compatibility Slack, Discord, Signal, and Microsoft Teams packages published through -`2026.7.1` import channel-specific config schemas from +`2026.7.1` imported channel-specific config schemas from `openclaw/plugin-sdk/bundled-channel-config-schema`. The published Slack and -Discord packages also import `createLegacyCompatChannelDmPolicy` and +Discord packages also imported `createLegacyCompatChannelDmPolicy` and `promptLegacyChannelAllowFromForAccount` from `openclaw/plugin-sdk/setup-runtime`. -Those exports remain available as deprecated runtime compatibility adapters. -New and republished plugins should own their config schemas and setup policy -locally, using generic primitives from `channel-config-schema` and -`setup-runtime`. The compatibility exports can be removed only after the -minimum supported published package versions no longer import them. +Those compatibility exports were removed in August 2026. Channel plugins must +own their config schemas and setup policy locally, using generic primitives +from `channel-config-schema` and `setup-runtime`. ### Channel setup input field compatibility @@ -512,11 +510,7 @@ For local media read policy, import `getAgentScopedMediaLocalRoots(...)` or consistently across native approvals, reply suppression, inbound dedupe, cron delivery, and session routing. - Do not add new uses of `ChannelMessagingAdapter.parseExplicitTarget` or - `resolveChannelRouteTargetWithParser(...)` from - `plugin-sdk/channel-route` - those are deprecated and remain only for older - plugins. New channel plugins should use - `messaging.targetResolver.resolveTarget(...)` for target-id normalization + Channel plugins use `messaging.targetResolver.resolveTarget(...)` for target-id normalization and directory-miss fallback, `messaging.inferTargetChatType(...)` when core needs an early peer kind, and `messaging.resolveOutboundSessionRoute(...)` for provider-native @@ -702,11 +696,8 @@ timeline for current status. }); ``` - `subagent_spawning`, `PluginHookSubagentSpawningEvent`, - `PluginHookSubagentSpawningResult`, and - `SubagentLifecycleHookRunner.runSubagentSpawning(...)` remain only as - deprecated compatibility surfaces while external plugins migrate, removed - after 2026-08-30. + The `subagent_spawning` hook and its event/result types were removed in + August 2026 after thread binding moved to the core session-binding path. @@ -796,10 +787,9 @@ timeline for current status. `contracts.embeddingProviders`. The generic embedding provider contract is reusable outside memory and is - the supported path for new providers. The memory-specific registration API - remains wired as deprecated compatibility while existing providers - migrate. Plugin inspection reports non-bundled usage as compatibility - debt. + the supported path for every provider. The memory-specific registration API + and manifest contract were removed after the **2026-08-21** migration + deadline. diff --git a/docs/plugins/sdk-overview.md b/docs/plugins/sdk-overview.md index 8619315b89d0..f8b1486043c6 100644 --- a/docs/plugins/sdk-overview.md +++ b/docs/plugins/sdk-overview.md @@ -140,10 +140,8 @@ Providers with renewable leases can also implement `renew(leaseId)`. Embedding providers registered with `api.registerEmbeddingProvider(...)` must also be listed in `contracts.embeddingProviders` in the plugin manifest. This is the generic embedding surface for reusable vector generation. Memory search -can consume this generic provider surface. The older -`api.registerMemoryEmbeddingProvider(...)` and -`contracts.memoryEmbeddingProviders` seam is deprecated compatibility while -existing memory-specific providers migrate. +consumes this generic provider surface. The older memory-specific registrar and +manifest contract were removed after their August 2026 migration window. Memory-specific providers that still expose a runtime `batchEmbed(...)` stay on the existing per-file batching contract unless their runtime explicitly sets @@ -669,11 +667,7 @@ rebuild earlier history. Without the full contract, OpenClaw uses the legacy context path for the whole logical turn and its retries, leaves the configured engine unchanged, and tries that engine again on the next logical turn. -### Deprecated memory embedding adapters - -| Method | What it registers | -| ---------------------------------------------- | ---------------------------------------------- | -| `api.registerMemoryEmbeddingProvider(adapter)` | Memory embedding adapter for the active plugin | +### Memory embedding adapters - `registerMemoryCapability` is the exclusive memory-plugin API. - `registerMemoryCapability` may also expose `publicArtifacts.listArtifacts(...)` @@ -692,12 +686,8 @@ engine unchanged, and tries that engine again on the next logical turn. - `MemoryFlushPlan.model` can pin the flush turn to an exact `provider/model` reference, such as `ollama/qwen3:8b`, without inheriting the active fallback chain. -- `registerMemoryEmbeddingProvider` is deprecated. New embedding providers - should use `api.registerEmbeddingProvider(...)` and - `contracts.embeddingProviders`. -- Existing memory-specific providers continue to work during the migration - window, but plugin inspection reports this as compatibility debt for - non-bundled plugins. +- Embedding providers use `api.registerEmbeddingProvider(...)` and + `contracts.embeddingProviders`; there is no separate memory-only registry. ### Events and lifecycle diff --git a/docs/plugins/sdk-provider-plugins.md b/docs/plugins/sdk-provider-plugins.md index 7af27c77406b..9b24294591cd 100644 --- a/docs/plugins/sdk-provider-plugins.md +++ b/docs/plugins/sdk-provider-plugins.md @@ -949,8 +949,8 @@ catalog, API-key auth, and dynamic model resolution. Declare the same id in `contracts.embeddingProviders`. This is the general embedding contract for reusable vector generation, including - memory search. `registerMemoryEmbeddingProvider(...)` is deprecated - compatibility for existing memory-specific adapters. + memory search. The retired memory-specific registrar and manifest + contract are no longer accepted. Image and video capabilities use a **mode-aware** shape. Image diff --git a/docs/plugins/sdk-runtime.md b/docs/plugins/sdk-runtime.md index b8f27767e213..fdbcdd2e106c 100644 --- a/docs/plugins/sdk-runtime.md +++ b/docs/plugins/sdk-runtime.md @@ -208,8 +208,6 @@ snapshots; OpenClaw owns all persistence and lifecycle coordination. `runEmbeddedAgent(...)` is the neutral helper for starting a normal OpenClaw agent turn from plugin code. It uses the same provider/model resolution and agent-harness selection as channel-triggered replies. - `runEmbeddedPiAgent(...)` remains as a deprecated compatibility alias for existing plugins. New code should use `runEmbeddedAgent(...)`. - `resolveCliBackendDispatchEligibility({ provider, model, agentId, authProfileId, config, agentDir, workspaceDir })` shares the embedded runner's CLI-backend dispatch decision (route, the backend's declared `subscriptionAuthDispatch` capability, stored credential mode — honoring an explicitly pinned `authProfileId`) with callers that opt embedded runs into `cliBackendDispatch: "subscription-auth"`. It returns `{ provider }` when the run would execute through the CLI backend and `undefined` when it stays on the direct passthrough, so callers can budget timeouts for the run that will actually execute. `resolveThinkingPolicy(...)` returns the provider/model's supported thinking levels and optional default. Provider plugins own the model-specific profile through their thinking hooks, so tool plugins should call this runtime helper instead of importing or duplicating provider lists. diff --git a/docs/plugins/sdk-subpaths.md b/docs/plugins/sdk-subpaths.md index 733986dc83ab..39bacbf8307e 100644 --- a/docs/plugins/sdk-subpaths.md +++ b/docs/plugins/sdk-subpaths.md @@ -99,7 +99,6 @@ new code; see the per-row notes below. | `plugin-sdk/inbound-envelope` | Shared inbound route + envelope builder helpers | | `plugin-sdk/inbound-event-delivery` | Process-local correlation between active inbound events and successful channel sends | | `plugin-sdk/inbound-reply-dispatch` | Deprecated compatibility shim for `dispatchInboundReplyWithBase`; its compatibility-ledger gate is the next Plugin SDK major, not a calendar date. Use `plugin-sdk/channel-inbound` for inbound runners and `plugin-sdk/channel-outbound` for message delivery helpers. | - | `plugin-sdk/messaging-targets` | Deprecated target parsing alias; use `plugin-sdk/channel-targets` | | `plugin-sdk/outbound-media` | Private-local after July 2026; Shared outbound media loading and hosted-media state helpers | | `plugin-sdk/poll-runtime` | Private-local after July 2026; Narrow poll normalization helpers | | `plugin-sdk/thread-bindings-runtime` | Private-local after July 2026; Thread-binding lifecycle and adapter helpers | @@ -371,7 +370,7 @@ Use `isLoopbackHost(host)` when a plugin must accept only the local machine. It | `plugin-sdk/memory-core-host-engine-curated` | Private-local focused curated-memory annotation parsing for doctor and promotion paths | | `plugin-sdk/memory-core-host-engine-foundation` | Memory host foundation engine exports | | `plugin-sdk/memory-core-host-engine-fs` | Private-local focused filesystem and user-path helpers for doctor migrations | - | `plugin-sdk/memory-core-host-engine-embeddings` | Private-local after July 2026; Memory host embedding contracts, registry access, and generic batch/remote helpers. `registerMemoryEmbeddingProvider` on this surface is deprecated; use the generic embedding provider API for new providers. | + | `plugin-sdk/memory-core-host-engine-embeddings` | Private-local after July 2026; Memory host embedding contracts, generic adapter bridging, and batch/remote helpers. Providers register through the generic embedding provider API. | | `plugin-sdk/memory-core-host-engine-sessions` | Private-local after July 2026; Memory session transcript and query helpers | | `plugin-sdk/memory-core-host-engine-schema` | Private-local focused memory index schema and sqlite-vec helpers for doctor migrations | | `plugin-sdk/memory-core-host-engine-storage` | Private-local after July 2026; Memory host storage engine exports | diff --git a/extensions/amazon-bedrock/openclaw.plugin.json b/extensions/amazon-bedrock/openclaw.plugin.json index 17d10f9a08fe..25e30157aa0a 100644 --- a/extensions/amazon-bedrock/openclaw.plugin.json +++ b/extensions/amazon-bedrock/openclaw.plugin.json @@ -13,7 +13,7 @@ } }, "contracts": { - "memoryEmbeddingProviders": ["bedrock"] + "embeddingProviders": ["bedrock"] }, "configSchema": { "type": "object", diff --git a/extensions/amazon-bedrock/register.sync.runtime.ts b/extensions/amazon-bedrock/register.sync.runtime.ts index 75b4b2ece4b1..33772407649d 100644 --- a/extensions/amazon-bedrock/register.sync.runtime.ts +++ b/extensions/amazon-bedrock/register.sync.runtime.ts @@ -5,6 +5,7 @@ import type { BedrockClient } from "@aws-sdk/client-bedrock"; import type { StreamFn } from "openclaw/plugin-sdk/agent-core"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; +import { adaptMemoryEmbeddingProviderAdapter } from "openclaw/plugin-sdk/memory-core-host-engine-embeddings"; import { resolvePluginConfigObject } from "openclaw/plugin-sdk/plugin-config-runtime"; import type { OpenClawPluginApi, @@ -398,7 +399,9 @@ export function registerAmazonBedrockPlugin(api: OpenClawPluginApi): void { ); } - api.registerMemoryEmbeddingProvider(bedrockMemoryEmbeddingProviderAdapter); + api.registerEmbeddingProvider( + adaptMemoryEmbeddingProviderAdapter(bedrockMemoryEmbeddingProviderAdapter), + ); const baseWrapStreamFn = ({ modelId, diff --git a/extensions/deepinfra/index.test.ts b/extensions/deepinfra/index.test.ts index df53b2601123..162d3b1defae 100644 --- a/extensions/deepinfra/index.test.ts +++ b/extensions/deepinfra/index.test.ts @@ -230,7 +230,6 @@ describe("deepinfra capability registration", () => { "deepinfra", ]); expect(captured.embeddingProviders.map((provider) => provider.id)).toEqual(["deepinfra"]); - expect(captured.memoryEmbeddingProviders).toEqual([]); expect(captured.speechProviders.map((provider) => provider.id)).toEqual(["deepinfra"]); expect(captured.videoGenerationProviders.map((provider) => provider.id)).toEqual(["deepinfra"]); }); diff --git a/extensions/discord/api.ts b/extensions/discord/api.ts index fd4122abfa0f..cc7ca910d1fa 100644 --- a/extensions/discord/api.ts +++ b/extensions/discord/api.ts @@ -4,7 +4,6 @@ export { discordSetupPlugin } from "./src/channel.setup.js"; export { handleDiscordSubagentDeliveryTarget, handleDiscordSubagentEnded, - handleDiscordSubagentSpawning, } from "./src/subagent-hooks.js"; export { inspectDiscordAccount, type InspectedDiscordAccount } from "./src/account-inspect.js"; export { type DiscordCredentialStatus } from "./src/token.js"; diff --git a/extensions/discord/src/api-barrel.test.ts b/extensions/discord/src/api-barrel.test.ts index 11ad6fe3c97d..9f73aad17b05 100644 --- a/extensions/discord/src/api-barrel.test.ts +++ b/extensions/discord/src/api-barrel.test.ts @@ -66,7 +66,6 @@ describe("discord API barrel", () => { "buildDiscordComponentMessageFlags", "createDiscordFormModal", "handleDiscordMessageAction", - "handleDiscordSubagentSpawning", "listEnabledDiscordAccounts", "parseDiscordComponentCustomIdForCarbon", "parseDiscordModalCustomIdForCarbon", diff --git a/extensions/discord/src/subagent-hooks.test.ts b/extensions/discord/src/subagent-hooks.test.ts index 1bea26f42d7b..3077811798d2 100644 --- a/extensions/discord/src/subagent-hooks.test.ts +++ b/extensions/discord/src/subagent-hooks.test.ts @@ -5,59 +5,14 @@ import { } from "openclaw/plugin-sdk/channel-test-helpers"; import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core"; import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; -import { handleDiscordSubagentSpawning } from "./subagent-hooks.js"; type ThreadBindingRecord = { accountId: string; threadId: string; }; -type MockResolvedDiscordAccount = { - accountId: string; - config: { - threadBindings?: { - enabled?: boolean; - spawnSessions?: boolean; - }; - }; -}; - -type MockResolveDiscordAccountParams = { - cfg?: { - channels?: { - discord?: { - defaultAccount?: string; - accounts?: Record< - string, - { threadBindings?: MockResolvedDiscordAccount["config"]["threadBindings"] } - >; - }; - }; - }; - accountId?: string; -}; - const hookMocks = vi.hoisted(() => { - const resolveDiscordAccountImpl = ( - params?: MockResolveDiscordAccountParams, - ): MockResolvedDiscordAccount => { - const accountId = - params?.accountId?.trim() || params?.cfg?.channels?.discord?.defaultAccount || "default"; - return { - accountId, - config: { - threadBindings: params?.cfg?.channels?.discord?.accounts?.[accountId]?.threadBindings ?? { - spawnSessions: true, - }, - }, - }; - }; return { - resolveDiscordAccountImpl, - resolveDiscordAccount: vi.fn(resolveDiscordAccountImpl), - autoBindSpawnedDiscordSubagent: vi.fn( - async (): Promise<{ threadId: string } | null> => ({ threadId: "thread-1" }), - ), listThreadBindingsBySessionKey: vi.fn((_params?: unknown): ThreadBindingRecord[] => []), unbindThreadBindingsBySessionKey: vi.fn(() => []), progressModuleFactory: vi.fn(), @@ -67,11 +22,7 @@ const hookMocks = vi.hoisted(() => { let registerDiscordSubagentHooks: typeof import("../subagent-hooks-api.js").registerDiscordSubagentHooks; -vi.mock("./accounts.js", () => ({ - resolveDiscordAccount: hookMocks.resolveDiscordAccount, -})); vi.mock("./monitor/thread-bindings.js", () => ({ - autoBindSpawnedDiscordSubagent: hookMocks.autoBindSpawnedDiscordSubagent, listThreadBindingsBySessionKey: hookMocks.listThreadBindingsBySessionKey, unbindThreadBindingsBySessionKey: hookMocks.unbindThreadBindingsBySessionKey, })); @@ -80,23 +31,10 @@ vi.mock("./subagent-progress.js", () => { return { recoverDiscordSubagentProgress: hookMocks.recoverDiscordSubagentProgress }; }); -function registerHandlersForTest( - config: Record = { - channels: { - discord: { - threadBindings: { - spawnSessions: true, - }, - }, - }, - }, -) { +function registerHandlersForTest() { return registerHookHandlersForTest({ - config, - register: (api) => { - registerDiscordSubagentHooks(api); - api.on("subagent_spawning", (event) => handleDiscordSubagentSpawning(api, event)); - }, + config: {}, + register: registerDiscordSubagentHooks, }); } @@ -121,99 +59,12 @@ async function resolveSubagentDeliveryTargetForTest(requesterOrigin: { ); } -function createSpawnEvent(overrides?: { - childSessionKey?: string; - agentId?: string; - label?: string; - mode?: string; - requester?: { - channel?: string; - accountId?: string | undefined; - to?: string; - threadId?: string; - }; - threadRequested?: boolean; -}): { - childSessionKey: string; - agentId: string; - label: string; - mode: string; - requester: { - channel: string; - accountId?: string; - to: string; - threadId?: string; - }; - threadRequested: boolean; -} { - const base = { - childSessionKey: "agent:main:subagent:child", - agentId: "main", - label: "banana", - mode: "session", - requester: { - channel: "discord", - accountId: "work", - to: "channel:123", - threadId: "456", - }, - threadRequested: true, - }; - return { - ...base, - ...overrides, - requester: { - ...base.requester, - ...overrides?.requester, - }, - }; -} - -function createSpawnEventWithoutThread() { - return createSpawnEvent({ - label: "", - requester: { threadId: undefined }, - }); -} - -async function runSubagentSpawning( - config?: Record, - event = createSpawnEventWithoutThread(), -) { - const handlers = registerHandlersForTest(config); - const handler = getRequiredHookHandler(handlers, "subagent_spawning"); - return await handler(event, {}); -} - -function expectSubagentHookError(result: unknown): { status: "error"; error: string } { - expect((result as { status?: unknown } | undefined)?.status).toBe("error"); - const error = (result as { error?: unknown } | undefined)?.error; - expect(typeof error).toBe("string"); - return result as { status: "error"; error: string }; -} - -async function expectSubagentSpawningError(params?: { - config?: Record; - errorContains?: string; - event?: ReturnType; -}) { - const result = await runSubagentSpawning(params?.config, params?.event); - expect(hookMocks.autoBindSpawnedDiscordSubagent).not.toHaveBeenCalled(); - const errorResult = expectSubagentHookError(result); - if (params?.errorContains) { - expect(errorResult.error).toContain(params.errorContains); - } -} - describe("discord subagent hook handlers", () => { beforeAll(async () => { ({ registerDiscordSubagentHooks } = await import("../subagent-hooks-api.js")); }); beforeEach(() => { - hookMocks.resolveDiscordAccount.mockClear(); - hookMocks.resolveDiscordAccount.mockImplementation(hookMocks.resolveDiscordAccountImpl); - hookMocks.autoBindSpawnedDiscordSubagent.mockClear(); hookMocks.listThreadBindingsBySessionKey.mockClear(); hookMocks.unbindThreadBindingsBySessionKey.mockClear(); hookMocks.progressModuleFactory.mockClear(); @@ -249,197 +100,6 @@ describe("discord subagent hook handlers", () => { expect(hookMocks.recoverDiscordSubagentProgress).toHaveBeenCalledTimes(1); }); - it("binds thread routing on subagent_spawning", async () => { - const config = { - channels: { - discord: { - threadBindings: { - spawnSessions: true, - }, - }, - }, - }; - const handlers = registerHandlersForTest(config); - const handler = getRequiredHookHandler(handlers, "subagent_spawning"); - - const result = await handler(createSpawnEvent(), {}); - - expect(hookMocks.autoBindSpawnedDiscordSubagent).toHaveBeenCalledTimes(1); - expect(hookMocks.autoBindSpawnedDiscordSubagent).toHaveBeenCalledWith({ - cfg: config, - accountId: "work", - channel: "discord", - to: "channel:123", - threadId: "456", - childSessionKey: "agent:main:subagent:child", - agentId: "main", - label: "banana", - boundBy: "system", - }); - expect(result).toMatchObject({ - status: "ok", - threadBindingReady: true, - deliveryOrigin: { - channel: "discord", - accountId: "work", - to: "channel:thread-1", - threadId: "thread-1", - }, - }); - }); - - it("returns error when thread-bound subagent spawn is disabled", async () => { - await expectSubagentSpawningError({ - config: { - channels: { - discord: { - threadBindings: { - spawnSessions: false, - }, - }, - }, - }, - errorContains: "spawnSessions=true", - }); - }); - - it("honors defaultAccount policy when requester omits accountId", async () => { - const config = { - channels: { - discord: { - defaultAccount: "work", - threadBindings: { - spawnSessions: true, - }, - accounts: { - work: { - threadBindings: { - spawnSessions: false, - }, - }, - }, - }, - }, - }; - await expectSubagentSpawningError({ - config, - event: createSpawnEvent({ - requester: { - accountId: undefined, - channel: "discord", - to: "channel:123", - threadId: undefined, - }, - }), - errorContains: "spawnSessions=true", - }); - expect(hookMocks.resolveDiscordAccount).toHaveBeenCalledWith({ - cfg: config, - accountId: undefined, - }); - }); - - it("returns error when global thread bindings are disabled", async () => { - await expectSubagentSpawningError({ - config: { - session: { - threadBindings: { - enabled: false, - }, - }, - channels: { - discord: { - threadBindings: { - spawnSessions: true, - }, - }, - }, - }, - errorContains: "threadBindings.enabled=true", - }); - }); - - it("allows account-level threadBindings.enabled to override global disable", async () => { - const result = await runSubagentSpawning({ - session: { - threadBindings: { - enabled: false, - }, - }, - channels: { - discord: { - accounts: { - work: { - threadBindings: { - enabled: true, - spawnSessions: true, - }, - }, - }, - }, - }, - }); - - expect(hookMocks.autoBindSpawnedDiscordSubagent).toHaveBeenCalledTimes(1); - expect(result).toMatchObject({ - status: "ok", - threadBindingReady: true, - deliveryOrigin: { - channel: "discord", - accountId: "work", - to: "channel:thread-1", - threadId: "thread-1", - }, - }); - }); - - it("defaults thread-bound subagent spawn to enabled when unset", async () => { - const result = await runSubagentSpawning({ - channels: { - discord: { - threadBindings: {}, - }, - }, - }); - - expect(hookMocks.autoBindSpawnedDiscordSubagent).toHaveBeenCalledTimes(1); - expect(result).toMatchObject({ - status: "ok", - threadBindingReady: true, - deliveryOrigin: { - channel: "discord", - accountId: "work", - to: "channel:thread-1", - threadId: "thread-1", - }, - }); - }); - - it("no-ops when thread binding is requested on non-discord channel", async () => { - const result = await runSubagentSpawning( - undefined, - createSpawnEvent({ - requester: { - channel: "signal", - accountId: "", - to: "+123", - threadId: undefined, - }, - }), - ); - - expect(hookMocks.autoBindSpawnedDiscordSubagent).not.toHaveBeenCalled(); - expect(result).toBeUndefined(); - }); - - it("returns error when thread bind fails", async () => { - hookMocks.autoBindSpawnedDiscordSubagent.mockResolvedValueOnce(null); - const result = await runSubagentSpawning(); - - const errorResult = expectSubagentHookError(result); - expect(errorResult.error).toMatch(/unable to create or bind/i); - }); - it("unbinds thread routing on subagent_ended", async () => { const handlers = registerHandlersForTest(); const handler = getRequiredHookHandler(handlers, "subagent_ended"); diff --git a/extensions/discord/src/subagent-hooks.ts b/extensions/discord/src/subagent-hooks.ts index 9a27688aa01e..97c7c9ab3b66 100644 --- a/extensions/discord/src/subagent-hooks.ts +++ b/extensions/discord/src/subagent-hooks.ts @@ -1,45 +1,14 @@ // Discord plugin module implements subagent hooks behavior. -import type { OpenClawPluginApi } from "openclaw/plugin-sdk/channel-plugin-common"; -import { - formatThreadBindingDisabledError, - formatThreadBindingSpawnDisabledError, - resolveThreadBindingSpawnPolicy, -} from "openclaw/plugin-sdk/conversation-runtime"; import { normalizeOptionalLowercaseString, normalizeOptionalStringifiedId, } from "openclaw/plugin-sdk/string-coerce-runtime"; -import { resolveDiscordAccount } from "./accounts.js"; import { - autoBindSpawnedDiscordSubagent, listThreadBindingsBySessionKey, type ThreadBindingTargetKind, unbindThreadBindingsBySessionKey, } from "./monitor/thread-bindings.js"; -function summarizeError(err: unknown): string { - if (err instanceof Error) { - return err.message; - } - if (typeof err === "string") { - return err; - } - return "error"; -} - -type DiscordSubagentSpawningEvent = { - threadRequested?: boolean; - requester?: { - channel?: string; - accountId?: string; - to?: string; - threadId?: string | number; - }; - childSessionKey: string; - agentId: string; - label?: string; -}; - type DiscordSubagentEndedEvent = { targetSessionKey: string; accountId?: string; @@ -58,20 +27,6 @@ type DiscordSubagentDeliveryTargetEvent = { }; }; -type DiscordSubagentSpawningResult = - | { - status: "ok"; - threadBindingReady?: boolean; - deliveryOrigin?: { - channel: "discord"; - accountId?: string; - to: string; - threadId?: string | number; - }; - } - | { status: "error"; error: string } - | undefined; - type DiscordSubagentDeliveryTargetResult = | { origin: { @@ -91,85 +46,6 @@ function normalizeThreadBindingTargetKind(raw?: string): ThreadBindingTargetKind return undefined; } -export async function handleDiscordSubagentSpawning( - api: OpenClawPluginApi, - event: DiscordSubagentSpawningEvent, -): Promise { - if (!event.threadRequested) { - return undefined; - } - const channel = normalizeOptionalLowercaseString(event.requester?.channel); - if (channel !== "discord") { - return undefined; - } - const account = resolveDiscordAccount({ - cfg: api.config, - accountId: event.requester?.accountId, - }); - const threadBindingPolicy = resolveThreadBindingSpawnPolicy({ - cfg: api.config, - channel: "discord", - accountId: account.accountId, - kind: "subagent", - }); - if (!threadBindingPolicy.enabled) { - return { - status: "error" as const, - error: formatThreadBindingDisabledError({ - channel: threadBindingPolicy.channel, - accountId: threadBindingPolicy.accountId, - kind: "subagent", - }), - }; - } - if (!threadBindingPolicy.spawnEnabled) { - return { - status: "error" as const, - error: formatThreadBindingSpawnDisabledError({ - channel: threadBindingPolicy.channel, - accountId: threadBindingPolicy.accountId, - kind: "subagent", - }), - }; - } - try { - const agentId = event.agentId?.trim() || "subagent"; - const binding = await autoBindSpawnedDiscordSubagent({ - cfg: api.config, - accountId: account.accountId, - channel: event.requester?.channel, - to: event.requester?.to, - threadId: event.requester?.threadId, - childSessionKey: event.childSessionKey, - agentId, - label: event.label, - boundBy: "system", - }); - if (!binding) { - return { - status: "error" as const, - error: - "Unable to create or bind a Discord thread for this subagent session. Session mode is unavailable for this target.", - }; - } - return { - status: "ok" as const, - threadBindingReady: true, - deliveryOrigin: { - channel: "discord", - accountId: account.accountId, - to: `channel:${binding.threadId}`, - threadId: binding.threadId, - }, - }; - } catch (err) { - return { - status: "error" as const, - error: `Discord thread bind failed: ${summarizeError(err)}`, - }; - } -} - export function handleDiscordSubagentEnded(event: DiscordSubagentEndedEvent) { unbindThreadBindingsBySessionKey({ targetSessionKey: event.targetSessionKey, diff --git a/extensions/feishu/api.ts b/extensions/feishu/api.ts index a9a35fe7173f..80aa9f7dc94f 100644 --- a/extensions/feishu/api.ts +++ b/extensions/feishu/api.ts @@ -9,7 +9,6 @@ export { registerFeishuBitableTools } from "./src/bitable.js"; export { handleFeishuSubagentDeliveryTarget, handleFeishuSubagentEnded, - handleFeishuSubagentSpawning, } from "./src/subagent-hooks.js"; export { buildFeishuConversationId, diff --git a/extensions/feishu/src/subagent-hooks.test.ts b/extensions/feishu/src/subagent-hooks.test.ts index 92dfe194756c..76c36c79e178 100644 --- a/extensions/feishu/src/subagent-hooks.test.ts +++ b/extensions/feishu/src/subagent-hooks.test.ts @@ -1,4 +1,4 @@ -// Feishu tests cover subagent hooks plugin behavior. +// Feishu tests cover current subagent delivery and cleanup hooks. import { getRequiredHookHandler, registerHookHandlersForTest, @@ -6,7 +6,6 @@ import { import { afterEach, describe, expect, it } from "vitest"; import type { ClawdbotConfig, OpenClawPluginApi } from "../runtime-api.js"; import { registerFeishuSubagentHooks } from "../subagent-hooks-api.js"; -import { handleFeishuSubagentSpawning } from "./subagent-hooks.js"; import { createFeishuThreadBindingManager as createFeishuThreadBindingManagerImpl } from "./thread-bindings.js"; const baseConfig: ClawdbotConfig = { @@ -17,29 +16,18 @@ const baseConfig: ClawdbotConfig = { type FeishuThreadBindingManager = ReturnType; let trackedManager: FeishuThreadBindingManager | null = null; -function createFeishuThreadBindingManager( - params: Parameters[0], -): FeishuThreadBindingManager { - trackedManager = createFeishuThreadBindingManagerImpl(params); +function createFeishuThreadBindingManager(): FeishuThreadBindingManager { + trackedManager = createFeishuThreadBindingManagerImpl({ cfg: baseConfig, accountId: "work" }); return trackedManager; } -function registerHandlersForTest(config: Record = baseConfig) { +function registerHandlersForTest() { return registerHookHandlersForTest({ - config, - register: (api) => { - registerFeishuSubagentHooks(api); - api.on("subagent_spawning", (event, ctx) => handleFeishuSubagentSpawning(event, ctx)); - }, + config: baseConfig, + register: registerFeishuSubagentHooks, }); } -async function expectHookError(value: unknown, expectedErrorFragment: string): Promise { - const result = (await value) as { status?: unknown; error?: unknown }; - expect(result.status).toBe("error"); - expect(result.error).toContain(expectedErrorFragment); -} - function dmOrigin(sender = "ou_sender_1") { return { channel: "feishu" as const, @@ -48,27 +36,12 @@ function dmOrigin(sender = "ou_sender_1") { }; } -type FeishuOrigin = ReturnType & { threadId?: string }; - -function topicOrigin() { - return { - channel: "feishu" as const, - accountId: "work", - to: "chat:oc_group_chat", - threadId: "om_topic_root", - }; -} - -function spawnEvent(params: { childSessionKey: string; requester?: FeishuOrigin; label?: string }) { - return { - childSessionKey: params.childSessionKey, - agentId: "codex", - label: params.label, - mode: "session" as const, - requester: params.requester ?? dmOrigin(), - threadRequested: true, - }; -} +type FeishuOrigin = { + channel: string; + accountId?: string; + to?: string; + threadId?: string; +}; function deliveryEvent(params: { childSessionKey: string; @@ -86,10 +59,9 @@ function deliveryEvent(params: { function managedHookFixture() { const handlers = registerHandlersForTest(); return { - spawnHandler: getRequiredHookHandler(handlers, "subagent_spawning"), deliveryHandler: getRequiredHookHandler(handlers, "subagent_delivery_target"), endedHandler: getRequiredHookHandler(handlers, "subagent_ended"), - manager: createFeishuThreadBindingManager({ cfg: baseConfig, accountId: "work" }), + manager: createFeishuThreadBindingManager(), }; } @@ -99,49 +71,13 @@ describe("feishu subagent hook handlers", () => { trackedManager = null; }); - it("binds a Feishu DM conversation on subagent_spawning", async () => { - const { spawnHandler, deliveryHandler } = managedHookFixture(); - - const result = await spawnHandler( - spawnEvent({ - childSessionKey: "agent:main:subagent:child", - label: "banana", - }), - {}, - ); - - expect(result).toEqual({ - status: "ok", - threadBindingReady: true, - deliveryOrigin: { - channel: "feishu", - accountId: "work", - to: "user:ou_sender_1", - }, - }); - - await expect( - deliveryHandler(deliveryEvent({ childSessionKey: "agent:main:subagent:child" }), {}), - ).resolves.toEqual({ - origin: { - channel: "feishu", - accountId: "work", - to: "user:ou_sender_1", - }, - }); - }); - - it("preserves the original Feishu DM delivery target", async () => { + it("preserves the bound Feishu DM delivery target", async () => { const { deliveryHandler, manager } = managedHookFixture(); - manager.bindConversation({ conversationId: "ou_sender_1", targetKind: "subagent", targetSessionKey: "agent:main:subagent:chat-dm-child", - metadata: { - deliveryTo: "chat:oc_dm_chat_1", - boundBy: "system", - }, + metadata: { deliveryTo: "chat:oc_dm_chat_1", boundBy: "system" }, }); await expect( @@ -157,102 +93,34 @@ describe("feishu subagent hook handlers", () => { {}, ), ).resolves.toEqual({ - origin: { - channel: "feishu", - accountId: "work", - to: "chat:oc_dm_chat_1", - }, + origin: { channel: "feishu", accountId: "work", to: "chat:oc_dm_chat_1" }, }); }); - it("binds a Feishu topic conversation and preserves parent context", async () => { - const { spawnHandler, deliveryHandler } = managedHookFixture(); - - const result = await spawnHandler( - spawnEvent({ - childSessionKey: "agent:main:subagent:topic-child", - label: "topic-child", - requester: topicOrigin(), - }), - {}, - ); - - expect(result).toEqual({ - status: "ok", - threadBindingReady: true, - deliveryOrigin: { - channel: "feishu", - accountId: "work", - to: "chat:oc_group_chat", - threadId: "om_topic_root", - }, - }); - await expect( - deliveryHandler( - deliveryEvent({ - childSessionKey: "agent:main:subagent:topic-child", - requesterOrigin: topicOrigin(), - }), - {}, - ), - ).resolves.toEqual({ - origin: { - channel: "feishu", - accountId: "work", - to: "chat:oc_group_chat", - threadId: "om_topic_root", - }, - }); - }); - - it("uses the requester session binding to preserve sender-scoped topic conversations", async () => { - const { spawnHandler, deliveryHandler, manager } = managedHookFixture(); - + it("preserves the bound Feishu topic parent context", async () => { + const { deliveryHandler, manager } = managedHookFixture(); manager.bindConversation({ - conversationId: "oc_group_chat:topic:om_topic_root:sender:ou_sender_1", + conversationId: "oc_group_chat:topic:om_topic_root", parentConversationId: "oc_group_chat", targetKind: "subagent", - targetSessionKey: "agent:main:parent", + targetSessionKey: "agent:main:subagent:topic-child", metadata: { - agentId: "codex", - label: "parent", + deliveryTo: "chat:oc_group_chat", + deliveryThreadId: "om_topic_root", boundBy: "system", }, }); - const reboundResult = await spawnHandler( - spawnEvent({ - childSessionKey: "agent:main:subagent:sender-child", - label: "sender-child", - requester: topicOrigin(), - }), - { - requesterSessionKey: "agent:main:parent", - }, - ); - - expect(reboundResult).toEqual({ - status: "ok", - threadBindingReady: true, - deliveryOrigin: { - channel: "feishu", - accountId: "work", - to: "chat:oc_group_chat", - threadId: "om_topic_root", - }, - }); - const childBindings = manager.listBySessionKey("agent:main:subagent:sender-child"); - expect(childBindings).toHaveLength(1); - expect(childBindings[0]?.conversationId).toBe( - "oc_group_chat:topic:om_topic_root:sender:ou_sender_1", - ); - expect(childBindings[0]?.parentConversationId).toBe("oc_group_chat"); await expect( deliveryHandler( deliveryEvent({ - childSessionKey: "agent:main:subagent:sender-child", - requesterSessionKey: "agent:main:parent", - requesterOrigin: topicOrigin(), + childSessionKey: "agent:main:subagent:topic-child", + requesterOrigin: { + channel: "feishu", + accountId: "work", + to: "chat:oc_group_chat", + threadId: "om_topic_root", + }, }), {}, ), @@ -266,24 +134,16 @@ describe("feishu subagent hook handlers", () => { }); }); - it("prefers requester-matching bindings when multiple child bindings exist", async () => { - const { spawnHandler, deliveryHandler } = managedHookFixture(); - - await spawnHandler( - spawnEvent({ - childSessionKey: "agent:main:subagent:shared", - label: "shared", - }), - {}, - ); - await spawnHandler( - spawnEvent({ - childSessionKey: "agent:main:subagent:shared", - label: "shared", - requester: dmOrigin("ou_sender_2"), - }), - {}, - ); + it("selects the requester-matching binding when a child has multiple routes", async () => { + const { deliveryHandler, manager } = managedHookFixture(); + for (const sender of ["ou_sender_1", "ou_sender_2"]) { + manager.bindConversation({ + conversationId: sender, + targetKind: "subagent", + targetSessionKey: "agent:main:subagent:shared", + metadata: { deliveryTo: `user:${sender}`, boundBy: "system" }, + }); + } await expect( deliveryHandler( @@ -294,197 +154,18 @@ describe("feishu subagent hook handlers", () => { {}, ), ).resolves.toEqual({ - origin: { - channel: "feishu", - accountId: "work", - to: "user:ou_sender_2", - }, + origin: { channel: "feishu", accountId: "work", to: "user:ou_sender_2" }, }); }); - it("fails closed when requester-session bindings remain ambiguous for the same topic", async () => { - const { spawnHandler, deliveryHandler, manager } = managedHookFixture(); - + it("removes bound routes on subagent_ended", async () => { + const { deliveryHandler, endedHandler, manager } = managedHookFixture(); manager.bindConversation({ - conversationId: "oc_group_chat:topic:om_topic_root:sender:ou_sender_1", - parentConversationId: "oc_group_chat", + conversationId: "ou_sender_1", targetKind: "subagent", - targetSessionKey: "agent:main:parent", - metadata: { boundBy: "system" }, + targetSessionKey: "agent:main:subagent:child", + metadata: { deliveryTo: "user:ou_sender_1", boundBy: "system" }, }); - manager.bindConversation({ - conversationId: "oc_group_chat:topic:om_topic_root:sender:ou_sender_2", - parentConversationId: "oc_group_chat", - targetKind: "subagent", - targetSessionKey: "agent:main:parent", - metadata: { boundBy: "system" }, - }); - - await expectHookError( - spawnHandler( - spawnEvent({ - childSessionKey: "agent:main:subagent:ambiguous-child", - label: "ambiguous-child", - requester: topicOrigin(), - }), - { - requesterSessionKey: "agent:main:parent", - }, - ), - "direct messages or topic conversations", - ); - - await expect( - deliveryHandler( - deliveryEvent({ - childSessionKey: "agent:main:subagent:ambiguous-child", - requesterSessionKey: "agent:main:parent", - requesterOrigin: topicOrigin(), - }), - {}, - ), - ).resolves.toBeUndefined(); - }); - - it("fails closed when both topic-level and sender-scoped requester bindings exist", async () => { - const { spawnHandler, deliveryHandler, manager } = managedHookFixture(); - - manager.bindConversation({ - conversationId: "oc_group_chat:topic:om_topic_root", - parentConversationId: "oc_group_chat", - targetKind: "subagent", - targetSessionKey: "agent:main:parent", - metadata: { boundBy: "system" }, - }); - manager.bindConversation({ - conversationId: "oc_group_chat:topic:om_topic_root:sender:ou_sender_1", - parentConversationId: "oc_group_chat", - targetKind: "subagent", - targetSessionKey: "agent:main:parent", - metadata: { boundBy: "system" }, - }); - - await expectHookError( - spawnHandler( - spawnEvent({ - childSessionKey: "agent:main:subagent:mixed-topic-child", - label: "mixed-topic-child", - requester: topicOrigin(), - }), - { - requesterSessionKey: "agent:main:parent", - }, - ), - "direct messages or topic conversations", - ); - - await expect( - deliveryHandler( - deliveryEvent({ - childSessionKey: "agent:main:subagent:mixed-topic-child", - requesterSessionKey: "agent:main:parent", - requesterOrigin: topicOrigin(), - }), - {}, - ), - ).resolves.toBeUndefined(); - }); - - it("no-ops for non-Feishu channels and non-threaded spawns", async () => { - const handlers = registerHandlersForTest(); - const spawnHandler = getRequiredHookHandler(handlers, "subagent_spawning"); - const deliveryHandler = getRequiredHookHandler(handlers, "subagent_delivery_target"); - const endedHandler = getRequiredHookHandler(handlers, "subagent_ended"); - - await expect( - spawnHandler( - { - childSessionKey: "agent:main:subagent:child", - agentId: "codex", - mode: "run", - requester: { - channel: "discord", - accountId: "work", - to: "channel:123", - }, - threadRequested: true, - }, - {}, - ), - ).resolves.toBeUndefined(); - - await expect( - spawnHandler( - { - childSessionKey: "agent:main:subagent:child", - agentId: "codex", - mode: "run", - requester: { - channel: "feishu", - accountId: "work", - to: "user:ou_sender_1", - }, - threadRequested: false, - }, - {}, - ), - ).resolves.toBeUndefined(); - - await expect( - deliveryHandler( - { - childSessionKey: "agent:main:subagent:child", - requesterSessionKey: "agent:main:main", - requesterOrigin: { - channel: "discord", - accountId: "work", - to: "channel:123", - }, - expectsCompletionMessage: true, - }, - {}, - ), - ).resolves.toBeUndefined(); - - await expect( - endedHandler( - { - targetSessionKey: "agent:main:subagent:child", - targetKind: "subagent", - reason: "done", - accountId: "work", - }, - {}, - ), - ).resolves.toBeUndefined(); - }); - - it("returns an error for unsupported non-topic Feishu group conversations", async () => { - const { spawnHandler } = managedHookFixture(); - - await expectHookError( - spawnHandler( - { - childSessionKey: "agent:main:subagent:child", - agentId: "codex", - mode: "session", - requester: { - channel: "feishu", - accountId: "work", - to: "chat:oc_group_chat", - }, - threadRequested: true, - }, - {}, - ), - "direct messages or topic conversations", - ); - }); - - it("unbinds Feishu bindings on subagent_ended", async () => { - const { spawnHandler, deliveryHandler, endedHandler } = managedHookFixture(); - - await spawnHandler(spawnEvent({ childSessionKey: "agent:main:subagent:child" }), {}); await endedHandler( { @@ -501,23 +182,25 @@ describe("feishu subagent hook handlers", () => { ).resolves.toBeUndefined(); }); - it("fails closed when the Feishu monitor-owned binding manager is unavailable", async () => { + it("leaves unrelated channels and missing managers unchanged", async () => { const handlers = registerHandlersForTest(); - const spawnHandler = getRequiredHookHandler(handlers, "subagent_spawning"); const deliveryHandler = getRequiredHookHandler(handlers, "subagent_delivery_target"); - await expectHookError( - spawnHandler( - spawnEvent({ - childSessionKey: "agent:main:subagent:no-manager", + await expect( + deliveryHandler( + deliveryEvent({ + childSessionKey: "agent:main:subagent:child", + requesterOrigin: { + channel: "discord", + accountId: "work", + to: "channel:123", + }, }), {}, ), - "monitor is not active", - ); - + ).resolves.toBeUndefined(); await expect( - deliveryHandler(deliveryEvent({ childSessionKey: "agent:main:subagent:no-manager" }), {}), + deliveryHandler(deliveryEvent({ childSessionKey: "agent:main:subagent:child" }), {}), ).resolves.toBeUndefined(); }); }); diff --git a/extensions/feishu/src/subagent-hooks.ts b/extensions/feishu/src/subagent-hooks.ts index 4286671669ab..51f119ac9a67 100644 --- a/extensions/feishu/src/subagent-hooks.ts +++ b/extensions/feishu/src/subagent-hooks.ts @@ -7,16 +7,6 @@ import { buildFeishuConversationId, parseFeishuConversationId } from "./conversa import { normalizeFeishuTarget } from "./targets.js"; import { getFeishuThreadBindingManager } from "./thread-bindings.js"; -function summarizeError(err: unknown): string { - if (err instanceof Error) { - return err.message; - } - if (typeof err === "string") { - return err; - } - return "error"; -} - function stripProviderPrefix(raw: string): string { return raw.replace(/^(feishu|lark):/i, "").trim(); } @@ -248,23 +238,6 @@ function resolveMatchingChildBinding(params: { return childBindings.length === 1 ? childBindings[0] : null; } -type FeishuSubagentContext = { - requesterSessionKey?: string; -}; - -type FeishuSubagentSpawningEvent = { - threadRequested?: boolean; - requester?: { - channel?: string; - accountId?: string; - to?: string; - threadId?: string | number; - }; - childSessionKey: string; - agentId?: string; - label?: string; -}; - type FeishuSubagentDeliveryTargetEvent = { expectsCompletionMessage?: boolean; requesterOrigin?: { @@ -282,20 +255,6 @@ type FeishuSubagentEndedEvent = { targetSessionKey: string; }; -type FeishuSubagentSpawningResult = - | { - status: "ok"; - threadBindingReady?: boolean; - deliveryOrigin?: { - channel: "feishu"; - accountId?: string; - to?: string; - threadId?: string | number; - }; - } - | { status: "error"; error: string } - | undefined; - type FeishuSubagentDeliveryTargetResult = | { origin: { @@ -307,84 +266,6 @@ type FeishuSubagentDeliveryTargetResult = } | undefined; -export async function handleFeishuSubagentSpawning( - event: FeishuSubagentSpawningEvent, - ctx: FeishuSubagentContext, -): Promise { - if (!event.threadRequested) { - return undefined; - } - const requesterChannel = normalizeOptionalLowercaseString(event.requester?.channel); - if (requesterChannel !== "feishu") { - return undefined; - } - - const manager = getFeishuThreadBindingManager(event.requester?.accountId); - if (!manager) { - return { - status: "error" as const, - error: - "Feishu current-conversation binding is unavailable because the Feishu account monitor is not active.", - }; - } - - const conversation = resolveFeishuRequesterConversation({ - accountId: event.requester?.accountId, - to: event.requester?.to, - threadId: event.requester?.threadId, - requesterSessionKey: ctx.requesterSessionKey, - }); - if (!conversation) { - return { - status: "error" as const, - error: - "Feishu current-conversation binding is only available in direct messages or topic conversations.", - }; - } - - try { - const binding = manager.bindConversation({ - conversationId: conversation.conversationId, - parentConversationId: conversation.parentConversationId, - targetKind: "subagent", - targetSessionKey: event.childSessionKey, - metadata: { - agentId: event.agentId, - label: event.label, - boundBy: "system", - deliveryTo: event.requester?.to, - deliveryThreadId: - event.requester?.threadId != null && event.requester.threadId !== "" - ? String(event.requester.threadId) - : undefined, - }, - }); - if (!binding) { - return { - status: "error" as const, - error: - "Unable to bind this Feishu conversation to the spawned subagent session. Session mode is unavailable for this target.", - }; - } - return { - status: "ok" as const, - threadBindingReady: true, - deliveryOrigin: resolveFeishuDeliveryOrigin({ - conversationId: binding.conversationId, - parentConversationId: binding.parentConversationId, - accountId: binding.accountId, - deliveryTo: binding.deliveryTo, - deliveryThreadId: binding.deliveryThreadId, - }), - }; - } catch (err) { - return { - status: "error" as const, - error: `Feishu conversation bind failed: ${summarizeError(err)}`, - }; - } -} - export function handleFeishuSubagentDeliveryTarget( event: FeishuSubagentDeliveryTargetEvent, ): FeishuSubagentDeliveryTargetResult { diff --git a/extensions/github-copilot/index.test.ts b/extensions/github-copilot/index.test.ts index 1cd1e5cfa1e6..e743230e0172 100644 --- a/extensions/github-copilot/index.test.ts +++ b/extensions/github-copilot/index.test.ts @@ -56,9 +56,7 @@ vi.mock("./register.runtime.js", () => ({ import plugin from "./index.js"; const tempDirs: string[] = []; -type RegisteredMemoryEmbeddingProvider = Parameters< - OpenClawPluginApi["registerMemoryEmbeddingProvider"] ->[0]; +type RegisteredEmbeddingProvider = Parameters[0]; type RegisteredProvider = Parameters[0]; type GithubCopilotTestProvider = RegisteredProvider & { auth: Array<{ @@ -370,8 +368,7 @@ describe("github-copilot plugin", () => { }); it("registers embedding provider", () => { - const registerMemoryEmbeddingProviderMock = - vi.fn(); + const registerEmbeddingProviderMock = vi.fn(); plugin.register( createTestPluginApi({ @@ -382,14 +379,14 @@ describe("github-copilot plugin", () => { pluginConfig: {}, runtime: {} as never, registerProvider: vi.fn(), - registerMemoryEmbeddingProvider: registerMemoryEmbeddingProviderMock, + registerEmbeddingProvider: registerEmbeddingProviderMock, }), ); - expect(registerMemoryEmbeddingProviderMock).toHaveBeenCalledTimes(1); - const adapter = requireFirstMockArg( - registerMemoryEmbeddingProviderMock, - "memory embedding provider registration", + expect(registerEmbeddingProviderMock).toHaveBeenCalledTimes(1); + const adapter = requireFirstMockArg( + registerEmbeddingProviderMock, + "embedding provider registration", ); expect(adapter.id).toBe("github-copilot"); }); diff --git a/extensions/github-copilot/index.ts b/extensions/github-copilot/index.ts index 830fd01b10c5..6ba43fcfb68e 100644 --- a/extensions/github-copilot/index.ts +++ b/extensions/github-copilot/index.ts @@ -1,5 +1,6 @@ // Github Copilot plugin entrypoint registers its OpenClaw integration. import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; +import { adaptMemoryEmbeddingProviderAdapter } from "openclaw/plugin-sdk/memory-core-host-engine-embeddings"; import { resolvePluginConfigObject } from "openclaw/plugin-sdk/plugin-config-runtime"; import { definePluginEntry, @@ -640,7 +641,9 @@ export default definePluginEntry({ return await runGitHubCopilotDeviceAuth(ctx, domain); } - api.registerMemoryEmbeddingProvider(githubCopilotMemoryEmbeddingProviderAdapter); + api.registerEmbeddingProvider( + adaptMemoryEmbeddingProviderAdapter(githubCopilotMemoryEmbeddingProviderAdapter), + ); api.registerProvider({ id: PROVIDER_ID, diff --git a/extensions/github-copilot/openclaw.plugin.json b/extensions/github-copilot/openclaw.plugin.json index 285930c9cf76..25b0591a59e2 100644 --- a/extensions/github-copilot/openclaw.plugin.json +++ b/extensions/github-copilot/openclaw.plugin.json @@ -221,7 +221,7 @@ } }, "contracts": { - "memoryEmbeddingProviders": ["github-copilot"], + "embeddingProviders": ["github-copilot"], "usageProviders": ["github-copilot"] }, "setup": { diff --git a/extensions/google/index.ts b/extensions/google/index.ts index ba63e88a6234..8531afd2cee3 100644 --- a/extensions/google/index.ts +++ b/extensions/google/index.ts @@ -1,6 +1,7 @@ // Google plugin entrypoint registers its OpenClaw integration. import type { ImageGenerationProvider } from "openclaw/plugin-sdk/image-generation"; import type { MediaUnderstandingProvider } from "openclaw/plugin-sdk/media-understanding"; +import { adaptMemoryEmbeddingProviderAdapter } from "openclaw/plugin-sdk/memory-core-host-engine-embeddings"; import type { MusicGenerationProvider } from "openclaw/plugin-sdk/music-generation"; import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry"; import type { @@ -470,7 +471,9 @@ export default definePluginEntry({ api.registerCliBackend(buildGoogleGeminiCliBackend()); registerGoogleGeminiCliProvider(api); registerGoogleProvider(api); - api.registerMemoryEmbeddingProvider(geminiMemoryEmbeddingProviderAdapter); + api.registerEmbeddingProvider( + adaptMemoryEmbeddingProviderAdapter(geminiMemoryEmbeddingProviderAdapter), + ); api.registerImageGenerationProvider(createLazyGoogleImageGenerationProvider()); api.registerMediaUnderstandingProvider(createLazyGoogleMediaUnderstandingProvider()); api.registerMusicGenerationProvider(createLazyGoogleMusicGenerationProvider()); diff --git a/extensions/google/openclaw.plugin.json b/extensions/google/openclaw.plugin.json index 9aad07784f35..b3927751ec96 100644 --- a/extensions/google/openclaw.plugin.json +++ b/extensions/google/openclaw.plugin.json @@ -750,7 +750,7 @@ }, "contracts": { "mediaUnderstandingProviders": ["google"], - "memoryEmbeddingProviders": ["gemini"], + "embeddingProviders": ["gemini"], "imageGenerationProviders": ["google"], "musicGenerationProviders": ["google"], "realtimeVoiceProviders": ["google"], diff --git a/extensions/llama-cpp/index.test.ts b/extensions/llama-cpp/index.test.ts index 582d4ddce907..e6fd5de95c8c 100644 --- a/extensions/llama-cpp/index.test.ts +++ b/extensions/llama-cpp/index.test.ts @@ -9,7 +9,6 @@ import { } from "openclaw/plugin-sdk/plugin-test-contracts"; import { clearEmbeddingProviders, - clearMemoryEmbeddingProviders, createEmptyPluginRegistry, getActivePluginRegistry, getRegisteredEmbeddingProvider, @@ -74,7 +73,7 @@ beforeEach(() => { afterEach(() => { clearEmbeddingProviders(); - clearMemoryEmbeddingProviders(); + clearEmbeddingProviders(); setActivePluginRegistry(previousPluginRegistry ?? createEmptyPluginRegistry()); vi.clearAllMocks(); }); diff --git a/extensions/lmstudio/index.ts b/extensions/lmstudio/index.ts index 675b8c77780b..0906fdc2fe88 100644 --- a/extensions/lmstudio/index.ts +++ b/extensions/lmstudio/index.ts @@ -1,3 +1,4 @@ +import { adaptMemoryEmbeddingProviderAdapter } from "openclaw/plugin-sdk/memory-core-host-engine-embeddings"; // Lmstudio plugin entrypoint registers its OpenClaw integration. import { definePluginEntry, @@ -152,7 +153,9 @@ export default definePluginEntry({ name: "LM Studio Provider", description: "Bundled LM Studio provider plugin", register(api: OpenClawPluginApi) { - api.registerMemoryEmbeddingProvider(lmstudioMemoryEmbeddingProviderAdapter); + api.registerEmbeddingProvider( + adaptMemoryEmbeddingProviderAdapter(lmstudioMemoryEmbeddingProviderAdapter), + ); api.registerProvider({ id: PROVIDER_ID, label: "LM Studio", diff --git a/extensions/lmstudio/openclaw.plugin.json b/extensions/lmstudio/openclaw.plugin.json index af97f44d83f7..3f5ec2ae04f3 100644 --- a/extensions/lmstudio/openclaw.plugin.json +++ b/extensions/lmstudio/openclaw.plugin.json @@ -60,7 +60,7 @@ } ], "contracts": { - "memoryEmbeddingProviders": ["lmstudio"] + "embeddingProviders": ["lmstudio"] }, "configSchema": { "type": "object", diff --git a/extensions/matrix/index.test.ts b/extensions/matrix/index.test.ts index eb74060e7862..dca54276a8e1 100644 --- a/extensions/matrix/index.test.ts +++ b/extensions/matrix/index.test.ts @@ -12,7 +12,6 @@ const runtimeMocks = vi.hoisted(() => ({ ensureMatrixCryptoRuntime: vi.fn(async () => {}), handleMatrixSubagentDeliveryTarget: vi.fn(() => "delivery-target"), handleMatrixSubagentEnded: vi.fn(async () => {}), - handleMatrixSubagentSpawning: vi.fn(async () => "spawned"), handleVerificationBootstrap: vi.fn(async () => {}), handleVerificationStatus: vi.fn(async () => {}), handleVerifyRecoveryKey: vi.fn(async () => {}), diff --git a/extensions/matrix/src/matrix/subagent-hooks.test.ts b/extensions/matrix/src/matrix/subagent-hooks.test.ts index 08a0eb6e8ea6..69f55be2ae3b 100644 --- a/extensions/matrix/src/matrix/subagent-hooks.test.ts +++ b/extensions/matrix/src/matrix/subagent-hooks.test.ts @@ -1,35 +1,20 @@ +// Matrix tests cover current subagent delivery and cleanup hooks. import type { OpenClawPluginApi as MatrixEntryPluginApi } from "openclaw/plugin-sdk/channel-entry-contract"; import { getRequiredHookHandler, registerHookHandlersForTest, } from "openclaw/plugin-sdk/channel-test-helpers"; -// Matrix tests cover subagent hooks plugin behavior. -import { createRequireRecord } from "openclaw/plugin-sdk/test-fixtures"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { registerMatrixSubagentHooks } from "../../subagent-hooks-api.js"; -// Hoisted stubs referenced in vi.mock factories below -const bindMock = vi.hoisted(() => vi.fn()); const unbindMock = vi.hoisted(() => vi.fn()); -const getCapabilitiesMock = vi.hoisted(() => vi.fn()); const getManagerMock = vi.hoisted(() => vi.fn()); const listAllBindingsMock = vi.hoisted(() => vi.fn((): any[] => [])); const listBindingsForAccountMock = vi.hoisted(() => vi.fn((): any[] => [])); const removeBindingRecordMock = vi.hoisted(() => vi.fn(() => false)); -const resolveMatrixBaseConfigMock = vi.hoisted(() => vi.fn((): any => ({}))); -const findMatrixAccountConfigMock = vi.hoisted(() => vi.fn((): any => undefined)); vi.mock("openclaw/plugin-sdk/conversation-binding-runtime", () => ({ - getSessionBindingService: () => ({ - bind: bindMock, - getCapabilities: getCapabilitiesMock, - unbind: unbindMock, - }), -})); - -vi.mock("./account-config.js", () => ({ - resolveMatrixBaseConfig: resolveMatrixBaseConfigMock, - findMatrixAccountConfig: findMatrixAccountConfigMock, + getSessionBindingService: () => ({ unbind: unbindMock }), })); vi.mock("./thread-bindings-shared.js", () => ({ @@ -45,53 +30,10 @@ vi.mock("./thread-bindings-shared.js", () => ({ `${params.accountId}:${params.parentConversationId?.trim() || "-"}:${params.conversationId}`, })); -import { - handleMatrixSubagentDeliveryTarget, - handleMatrixSubagentEnded, - handleMatrixSubagentSpawning, -} from "./subagent-hooks.js"; +import { handleMatrixSubagentDeliveryTarget, handleMatrixSubagentEnded } from "./subagent-hooks.js"; -const fakeApi = { config: {} } as never; -const DEFAULT_CHILD_SESSION_KEY = "agent:ops:subagent:child"; -const DEFAULT_ROOM_ID = "!room:example"; - -function registerHandlersForTest(config: Record = {}) { - return registerHookHandlersForTest({ - config, - register: (api) => { - registerMatrixSubagentHooks(api); - api.on("subagent_spawning", (event) => handleMatrixSubagentSpawning(api, event)); - }, - }); -} - -function makeSpawnEvent( - overrides: Partial<{ - threadRequested: boolean; - channel: string; - accountId: string; - to: string; - childSessionKey: string; - agentId: string; - label: string; - }> = {}, -) { - return { - threadRequested: overrides.threadRequested ?? true, - requester: { - channel: overrides.channel ?? "matrix", - accountId: overrides.accountId ?? "default", - to: overrides.to ?? "room:!room123:example.org", - }, - childSessionKey: overrides.childSessionKey ?? "agent:default:subagent:child", - agentId: overrides.agentId ?? "worker", - label: overrides.label, - }; -} - -function apiWithMatrixConfig(matrix: Record) { - return { config: { channels: { matrix } } } as never; -} +const CHILD_SESSION_KEY = "agent:ops:subagent:child"; +const ROOM_ID = "!room:example"; function makeBinding( overrides: Partial<{ @@ -103,11 +45,11 @@ function makeBinding( }> = {}, ) { return { - targetSessionKey: DEFAULT_CHILD_SESSION_KEY, + targetSessionKey: CHILD_SESSION_KEY, targetKind: "subagent", accountId: "ops", conversationId: "$thread", - parentConversationId: DEFAULT_ROOM_ID, + parentConversationId: ROOM_ID, boundAt: 0, lastActivityAt: 0, ...overrides, @@ -119,443 +61,47 @@ function makeDeliveryEvent( childSessionKey: string; channel: string; accountId: string | undefined; - to: string; threadId: string; expectsCompletionMessage: boolean; }> = {}, ) { - const requesterOrigin: { - channel: string; - accountId?: string; - to?: string; - threadId?: string; - } = { channel: overrides.channel ?? "matrix" }; + const requesterOrigin: { channel: string; accountId?: string; threadId?: string } = { + channel: overrides.channel ?? "matrix", + }; if (!("accountId" in overrides) || overrides.accountId !== undefined) { requesterOrigin.accountId = overrides.accountId ?? "ops"; } - if (overrides.to !== undefined) { - requesterOrigin.to = overrides.to; - } if (overrides.threadId !== undefined) { requesterOrigin.threadId = overrides.threadId; } return { - childSessionKey: overrides.childSessionKey ?? DEFAULT_CHILD_SESSION_KEY, + childSessionKey: overrides.childSessionKey ?? CHILD_SESSION_KEY, requesterOrigin, expectsCompletionMessage: overrides.expectsCompletionMessage ?? true, }; } -function makeDeliveryResult( - overrides: Partial<{ accountId: string; to: string; threadId: string }> = {}, -) { - return { - origin: { - channel: "matrix", - accountId: "ops", - to: `room:${DEFAULT_ROOM_ID}`, - ...overrides, - }, - }; -} - -const requireRecord = createRequireRecord("object", "label-not-object"); - -function expectRecordFields(record: Record, fields: Record) { - for (const [key, value] of Object.entries(fields)) { - expect(record[key]).toEqual(value); - } -} - -function expectResultFields(result: unknown, fields: Record) { - expectRecordFields(requireRecord(result, "hook result"), fields); -} - -function expectErrorResult(result: unknown, messagePart: string) { - const record = requireRecord(result, "hook result"); - expect(record.status).toBe("error"); - expect(String(record.error).toLowerCase()).toContain(messagePart.toLowerCase()); -} - -function requireBindCallWithTarget(targetSessionKey: string) { - const calls = bindMock.mock.calls; - const call = calls.find(([params]) => { - const record = params as { targetSessionKey?: string }; - return record.targetSessionKey === targetSessionKey; - }); - if (!call) { - throw new Error(`missing bind call for ${targetSessionKey}`); - } - return requireRecord(call[0], "bind params"); -} - -function resetSpawningMocks() { - bindMock.mockReset(); - getCapabilitiesMock.mockReset(); - getManagerMock.mockReset(); - resolveMatrixBaseConfigMock.mockReset(); - findMatrixAccountConfigMock.mockReset(); - resolveMatrixBaseConfigMock.mockReturnValue({ - threadBindings: { enabled: true, spawnSessions: true }, - }); - findMatrixAccountConfigMock.mockReturnValue(undefined); - getCapabilitiesMock.mockReturnValue({ - adapterAvailable: true, - bindSupported: true, - placements: ["current", "child"], - unbindSupported: true, - }); - getManagerMock.mockReturnValue({ persist: vi.fn() }); - bindMock.mockResolvedValue({ - conversation: { - accountId: "default", - conversationId: "$thread-root", - parentConversationId: "!room123:example.org", - }, - }); -} - -describe("handleMatrixSubagentSpawning", () => { - beforeEach(() => { - resetSpawningMocks(); - }); - - it("returns undefined when threadRequested is false", async () => { - const result = await handleMatrixSubagentSpawning( - fakeApi, - makeSpawnEvent({ threadRequested: false }), - ); - expect(result).toBeUndefined(); - expect(bindMock).not.toHaveBeenCalled(); - }); - - it("returns undefined when channel is not matrix", async () => { - const result = await handleMatrixSubagentSpawning( - fakeApi, - makeSpawnEvent({ channel: "slack" }), - ); - expect(result).toBeUndefined(); - expect(bindMock).not.toHaveBeenCalled(); - }); - - it("proceeds past channel check when channel is 'matrix' with mixed casing", async () => { - // channel.trim().toLowerCase() must equal "matrix" — mixed case is accepted - const result = await handleMatrixSubagentSpawning( - fakeApi, - makeSpawnEvent({ channel: " Matrix " }), - ); - expectResultFields(result, { status: "ok", threadBindingReady: true }); - }); - - it("returns error when thread bindings are disabled", async () => { - const result = await handleMatrixSubagentSpawning( - apiWithMatrixConfig({ threadBindings: { enabled: false, spawnSessions: true } }), - makeSpawnEvent(), - ); - expectErrorResult(result, "thread bindings are disabled"); - }); - - it("returns error when spawnSessions is false", async () => { - const result = await handleMatrixSubagentSpawning( - apiWithMatrixConfig({ threadBindings: { enabled: true, spawnSessions: false } }), - makeSpawnEvent(), - ); - expectErrorResult(result, "spawnSessions"); - }); - - it("allows thread-bound subagent spawn by default", async () => { - const result = await handleMatrixSubagentSpawning(fakeApi, makeSpawnEvent()); - expectResultFields(result, { status: "ok", threadBindingReady: true }); - }); - - it("returns error when requester.to has no room target", async () => { - const result = await handleMatrixSubagentSpawning( - fakeApi, - makeSpawnEvent({ to: "@user:example.org" }), - ); - expectErrorResult(result, "no room target"); - }); - - it("returns error when requester.to is empty", async () => { - const result = await handleMatrixSubagentSpawning(fakeApi, makeSpawnEvent({ to: "" })); - expectErrorResult(result, "no room target"); - }); - - it("returns error when no binding adapter is available for the account", async () => { - getCapabilitiesMock.mockReturnValue({ - adapterAvailable: false, - bindSupported: false, - placements: [], - unbindSupported: false, - }); - const result = await handleMatrixSubagentSpawning(fakeApi, makeSpawnEvent()); - expectErrorResult(result, "No Matrix session binding adapter"); - expect(bindMock).not.toHaveBeenCalled(); - }); - - it("calls bind with the resolved room id and returns ok", async () => { - bindMock.mockResolvedValue({ - conversation: { - accountId: "ops", - conversationId: "$thread-ops", - parentConversationId: "!roomAbc:technerik.com", - }, - }); - const result = await handleMatrixSubagentSpawning( - fakeApi, - makeSpawnEvent({ - accountId: "ops", - to: "room:!roomAbc:technerik.com", - childSessionKey: "agent:ops:subagent:worker", - agentId: "builder", - label: "Build Agent", - }), - ); - - const bindParams = requireBindCallWithTarget("agent:ops:subagent:worker"); - expectRecordFields(bindParams, { - targetKind: "subagent", - placement: "child", - }); - expectRecordFields(requireRecord(bindParams.conversation, "bind conversation"), { - channel: "matrix", - accountId: "ops", - conversationId: "!roomAbc:technerik.com", - }); - expectRecordFields(requireRecord(bindParams.metadata, "bind metadata"), { - agentId: "builder", - label: "Build Agent", - }); - expectResultFields(result, { - status: "ok", - threadBindingReady: true, - }); - expectRecordFields( - requireRecord(requireRecord(result, "hook result").deliveryOrigin, "delivery origin"), - { - channel: "matrix", - accountId: "ops", - to: "room:!roomAbc:technerik.com", - threadId: "$thread-ops", - }, - ); - }); - - it("uses 'default' as accountId when requester.accountId is absent", async () => { - bindMock.mockResolvedValue({ - conversation: { - accountId: "default", - conversationId: "$thread-default", - parentConversationId: "!room123:example.org", - }, - }); - await handleMatrixSubagentSpawning(fakeApi, makeSpawnEvent({ accountId: undefined as never })); - expect(getCapabilitiesMock).toHaveBeenCalledWith({ - channel: "matrix", - accountId: "default", - }); - const bindParams = requireBindCallWithTarget("agent:default:subagent:child"); - expect(requireRecord(bindParams.conversation, "bind conversation").accountId).toBe("default"); - }); - - it("returns error when bind() throws", async () => { - bindMock.mockRejectedValue(new Error("provider auth failed")); - const result = await handleMatrixSubagentSpawning(fakeApi, makeSpawnEvent()); - expectErrorResult(result, "provider auth failed"); - }); - - it("respects per-account threadBindings override over base config", async () => { - bindMock.mockResolvedValue({ conversation: {} }); - - const result = await handleMatrixSubagentSpawning( - apiWithMatrixConfig({ - threadBindings: { enabled: true, spawnSessions: false }, - accounts: { forge: { threadBindings: { spawnSessions: true } } }, - }), - makeSpawnEvent({ accountId: "forge" }), - ); - expectResultFields(result, { status: "ok", threadBindingReady: true }); - }); -}); - describe("matrix subagent hook registration", () => { beforeEach(() => { - resetSpawningMocks(); listBindingsForAccountMock.mockReset(); - listAllBindingsMock.mockReset(); - }); - - it("binds thread routing through the lazy registration barrel", async () => { - const handlers = registerHandlersForTest(); - const handler = getRequiredHookHandler(handlers, "subagent_spawning"); - - const result = await handler(makeSpawnEvent(), {}); - - expect(bindMock).toHaveBeenCalledTimes(1); - expectResultFields(result, { - status: "ok", - threadBindingReady: true, - }); - expectRecordFields( - requireRecord(requireRecord(result, "hook result").deliveryOrigin, "delivery origin"), - { - channel: "matrix", - accountId: "default", - to: "room:!room123:example.org", - threadId: "$thread-root", - }, - ); }); it("resolves delivery targets through the lazy registration barrel", async () => { - listBindingsForAccountMock.mockReturnValue([ - makeBinding({ - conversationId: "$thread-ops", - parentConversationId: "!roomAbc:technerik.com", - targetSessionKey: "agent:ops:subagent:worker", - }), - ]); - const handlers = registerHandlersForTest(); + listBindingsForAccountMock.mockReturnValue([makeBinding()]); + const handlers = registerHookHandlersForTest({ + config: {}, + register: registerMatrixSubagentHooks, + }); const handler = getRequiredHookHandler(handlers, "subagent_delivery_target"); - await expect( - handler( - makeDeliveryEvent({ - childSessionKey: "agent:ops:subagent:worker", - to: "room:!roomAbc:technerik.com", - threadId: "$thread-ops", - }), - {}, - ), - ).resolves.toEqual( - makeDeliveryResult({ to: "room:!roomAbc:technerik.com", threadId: "$thread-ops" }), - ); - }); -}); - -describe("handleMatrixSubagentEnded", () => { - const mockManager = { persist: vi.fn() }; - - beforeEach(() => { - getManagerMock.mockReset(); - listAllBindingsMock.mockReset(); - listBindingsForAccountMock.mockReset(); - removeBindingRecordMock.mockReset(); - unbindMock.mockReset(); - mockManager.persist.mockReset(); - }); - - it("does nothing when no matching bindings exist", async () => { - listBindingsForAccountMock.mockReturnValue([]); - await handleMatrixSubagentEnded({ - targetSessionKey: "agent:ops:subagent:child", - targetKind: "subagent", - accountId: "ops", - }); - expect(getManagerMock).not.toHaveBeenCalled(); - }); - - it("removes matching bindings and calls persist on the manager", async () => { - const binding = makeBinding(); - listBindingsForAccountMock.mockReturnValue([binding]); - removeBindingRecordMock.mockReturnValue(true); - getManagerMock.mockReturnValue(mockManager); - mockManager.persist.mockResolvedValue(undefined); - - await handleMatrixSubagentEnded({ - targetSessionKey: "agent:ops:subagent:child", - targetKind: "subagent", - accountId: "ops", - }); - - expect(removeBindingRecordMock).toHaveBeenCalledWith(binding); - expect(getManagerMock).toHaveBeenCalledWith("ops"); - expect(mockManager.persist).toHaveBeenCalled(); - }); - - it("sends farewell through the binding service when requested", async () => { - const binding = makeBinding(); - listBindingsForAccountMock.mockReturnValue([binding]); - unbindMock.mockResolvedValue([ - { - bindingId: "ops:!room:example:$thread", - targetSessionKey: "agent:ops:subagent:child", - targetKind: "subagent", - conversation: { - channel: "matrix", - accountId: "ops", - conversationId: "$thread", - parentConversationId: "!room:example", - }, - status: "active", - boundAt: 0, + await expect(handler(makeDeliveryEvent(), {})).resolves.toEqual({ + origin: { + channel: "matrix", + accountId: "ops", + to: `room:${ROOM_ID}`, + threadId: "$thread", }, - ]); - - await handleMatrixSubagentEnded({ - targetSessionKey: "agent:ops:subagent:child", - targetKind: "subagent", - accountId: "ops", - reason: "spawn-failed", - sendFarewell: true, }); - - expect(unbindMock).toHaveBeenCalledWith({ - bindingId: "ops:!room:example:$thread", - reason: "spawn-failed", - }); - expect(removeBindingRecordMock).not.toHaveBeenCalled(); - expect(getManagerMock).not.toHaveBeenCalled(); - }); - - it("skips persist when removeBindingRecord returns false (binding not found in store)", async () => { - const binding = makeBinding({ targetSessionKey: "agent:ops:subagent:orphan" }); - listBindingsForAccountMock.mockReturnValue([binding]); - removeBindingRecordMock.mockReturnValue(false); - - await handleMatrixSubagentEnded({ - targetSessionKey: "agent:ops:subagent:orphan", - targetKind: "subagent", - accountId: "ops", - }); - - expect(getManagerMock).not.toHaveBeenCalled(); - }); - - it("falls back to listAllBindings when accountId is absent", async () => { - const binding = makeBinding(); - listAllBindingsMock.mockReturnValue([binding]); - removeBindingRecordMock.mockReturnValue(true); - getManagerMock.mockReturnValue(mockManager); - mockManager.persist.mockResolvedValue(undefined); - - await handleMatrixSubagentEnded({ - targetSessionKey: "agent:ops:subagent:child", - targetKind: "subagent", - }); - - expect(listAllBindingsMock).toHaveBeenCalled(); - expect(listBindingsForAccountMock).not.toHaveBeenCalled(); - expect(mockManager.persist).toHaveBeenCalled(); - }); - - it("does not double-persist when multiple bindings share the same account", async () => { - listBindingsForAccountMock.mockReturnValue([ - makeBinding({ conversationId: "$t1" }), - makeBinding({ conversationId: "$t2" }), - ]); - removeBindingRecordMock.mockReturnValue(true); - getManagerMock.mockReturnValue(mockManager); - mockManager.persist.mockResolvedValue(undefined); - - await handleMatrixSubagentEnded({ - targetSessionKey: "agent:ops:subagent:child", - targetKind: "subagent", - accountId: "ops", - }); - - // persist must be called exactly once per unique accountId, not once per binding - expect(mockManager.persist).toHaveBeenCalledTimes(1); }); }); @@ -565,163 +111,116 @@ describe("handleMatrixSubagentDeliveryTarget", () => { listBindingsForAccountMock.mockReset(); }); - it("returns undefined when expectsCompletionMessage is false", () => { - const result = handleMatrixSubagentDeliveryTarget( - makeDeliveryEvent({ expectsCompletionMessage: false }), - ); - expect(result).toBeUndefined(); + it("returns undefined when completion delivery is not requested", () => { + expect( + handleMatrixSubagentDeliveryTarget(makeDeliveryEvent({ expectsCompletionMessage: false })), + ).toBeUndefined(); }); - it("returns undefined when requester channel is not matrix", () => { - listBindingsForAccountMock.mockReturnValue([]); - const result = handleMatrixSubagentDeliveryTarget(makeDeliveryEvent({ channel: "slack" })); - expect(result).toBeUndefined(); + it("returns undefined for another channel or a missing binding", () => { + expect( + handleMatrixSubagentDeliveryTarget(makeDeliveryEvent({ channel: "discord" })), + ).toBeUndefined(); + expect(handleMatrixSubagentDeliveryTarget(makeDeliveryEvent())).toBeUndefined(); }); - it("returns undefined when no bindings match the child session key", () => { - listBindingsForAccountMock.mockReturnValue([ - makeBinding({ targetSessionKey: "agent:ops:subagent:OTHER" }), - ]); - const result = handleMatrixSubagentDeliveryTarget(makeDeliveryEvent()); - expect(result).toBeUndefined(); - }); + it("returns the bound Matrix room and thread", () => { + listBindingsForAccountMock.mockReturnValue([makeBinding()]); - it("returns origin with threadId when binding has a distinct parentConversationId", () => { - const binding = makeBinding({ conversationId: "$thread123" }); - listBindingsForAccountMock.mockReturnValue([binding]); - - const result = handleMatrixSubagentDeliveryTarget( - makeDeliveryEvent({ threadId: "$thread123" }), - ); - - expect(result).toEqual(makeDeliveryResult({ threadId: "$thread123" })); - }); - - it("returns origin without threadId when conversationId equals parentConversationId", () => { - const binding = makeBinding({ conversationId: DEFAULT_ROOM_ID }); - listBindingsForAccountMock.mockReturnValue([binding]); - - const result = handleMatrixSubagentDeliveryTarget(makeDeliveryEvent()); - - expect(result).toEqual(makeDeliveryResult()); - expect(result?.origin).not.toHaveProperty("threadId"); - }); - - it("returns origin without threadId when binding has no parentConversationId", () => { - const binding = makeBinding({ - conversationId: DEFAULT_ROOM_ID, - parentConversationId: undefined, - }); - listBindingsForAccountMock.mockReturnValue([binding]); - - const result = handleMatrixSubagentDeliveryTarget(makeDeliveryEvent()); - - expect(result).toEqual(makeDeliveryResult()); - }); - - it("falls back to the single binding when requesterOrigin threadId does not match any binding", () => { - const binding = makeBinding({ conversationId: "$thread123" }); - listBindingsForAccountMock.mockReturnValue([binding]); - - const result = handleMatrixSubagentDeliveryTarget( - makeDeliveryEvent({ threadId: "$threadOTHER" }), - ); - - // No threadId match, but single binding → falls back to it - expect(result).toEqual(makeDeliveryResult({ threadId: "$thread123" })); - }); - - it("returns undefined when multiple bindings exist and threadId matches none", () => { - listBindingsForAccountMock.mockReturnValue([ - makeBinding({ conversationId: "$t1" }), - makeBinding({ conversationId: "$t2" }), - ]); - - const result = handleMatrixSubagentDeliveryTarget(makeDeliveryEvent({ threadId: "$tNONE" })); - - expect(result).toBeUndefined(); - }); - - it("uses listAllBindings when requesterOrigin has no accountId", () => { - const binding = makeBinding({ conversationId: "$thread123" }); - listAllBindingsMock.mockReturnValue([binding]); - - const result = handleMatrixSubagentDeliveryTarget(makeDeliveryEvent({ accountId: undefined })); - - expect(listAllBindingsMock).toHaveBeenCalled(); - expect(listBindingsForAccountMock).not.toHaveBeenCalled(); - expect(result).toEqual(makeDeliveryResult({ threadId: "$thread123" })); - }); -}); - -describe("concurrent spawns across accounts", () => { - function spawnForAccount(accountId: "ops" | "forge") { - return handleMatrixSubagentSpawning(fakeApi, { - threadRequested: true, - requester: { + expect(handleMatrixSubagentDeliveryTarget(makeDeliveryEvent())).toEqual({ + origin: { channel: "matrix", - accountId, - to: `room:!room-${accountId}:example.org`, - }, - childSessionKey: `agent:${accountId}:subagent:child-${accountId}`, - agentId: `worker-${accountId}`, - }); - } - - beforeEach(() => { - resetSpawningMocks(); - bindMock.mockReset(); - }); - - it("resolves both spawns independently when two accounts fire simultaneously", async () => { - // Each account gets its own bind call that resolves with a distinct conversation - bindMock - .mockResolvedValueOnce({ conversation: { accountId: "ops", conversationId: "$t-ops" } }) - .mockResolvedValueOnce({ conversation: { accountId: "forge", conversationId: "$t-forge" } }); - - const [opsResult, forgeResult] = await Promise.all([ - spawnForAccount("ops"), - spawnForAccount("forge"), - ]); - - expectResultFields(opsResult, { status: "ok", threadBindingReady: true }); - expectResultFields(forgeResult, { status: "ok", threadBindingReady: true }); - expect(bindMock).toHaveBeenCalledTimes(2); - - // Each bind call targeted the correct account's room - expectRecordFields( - requireRecord( - requireBindCallWithTarget("agent:ops:subagent:child-ops").conversation, - "ops bind conversation", - ), - { accountId: "ops", - conversationId: "!room-ops:example.org", + to: `room:${ROOM_ID}`, + threadId: "$thread", }, - ); - expectRecordFields( - requireRecord( - requireBindCallWithTarget("agent:forge:subagent:child-forge").conversation, - "forge bind conversation", - ), - { - accountId: "forge", - conversationId: "!room-forge:example.org", - }, - ); + }); }); - it("one account bind failure does not affect the other account's spawn", async () => { - bindMock - .mockRejectedValueOnce(new Error("ops provider auth failed")) - .mockResolvedValueOnce({ conversation: { accountId: "forge", conversationId: "$t-forge" } }); - - const [opsResult, forgeResult] = await Promise.all([ - spawnForAccount("ops"), - spawnForAccount("forge"), + it("omits threadId for a top-level room binding", () => { + listBindingsForAccountMock.mockReturnValue([ + makeBinding({ conversationId: ROOM_ID, parentConversationId: ROOM_ID }), ]); - expectErrorResult(opsResult, "ops provider auth failed"); - expectResultFields(forgeResult, { status: "ok", threadBindingReady: true }); + expect(handleMatrixSubagentDeliveryTarget(makeDeliveryEvent())).toEqual({ + origin: { channel: "matrix", accountId: "ops", to: `room:${ROOM_ID}` }, + }); + }); + + it("fails closed when multiple bindings do not match the requester thread", () => { + listBindingsForAccountMock.mockReturnValue([ + makeBinding({ conversationId: "$thread-1" }), + makeBinding({ conversationId: "$thread-2" }), + ]); + + expect( + handleMatrixSubagentDeliveryTarget(makeDeliveryEvent({ threadId: "$other" })), + ).toBeUndefined(); + }); + + it("scans all bindings when requester accountId is absent", () => { + listAllBindingsMock.mockReturnValue([makeBinding()]); + + expect(handleMatrixSubagentDeliveryTarget(makeDeliveryEvent({ accountId: undefined }))).toEqual( + { + origin: { + channel: "matrix", + accountId: "ops", + to: `room:${ROOM_ID}`, + threadId: "$thread", + }, + }, + ); + expect(listAllBindingsMock).toHaveBeenCalledTimes(1); + expect(listBindingsForAccountMock).not.toHaveBeenCalled(); + }); +}); + +describe("handleMatrixSubagentEnded", () => { + beforeEach(() => { + unbindMock.mockReset(); + getManagerMock.mockReset(); + listAllBindingsMock.mockReset(); + listBindingsForAccountMock.mockReset(); + removeBindingRecordMock.mockReset(); + }); + + it("removes matching bindings and persists each affected account once", async () => { + const persist = vi.fn(async () => {}); + listBindingsForAccountMock.mockReturnValue([ + makeBinding(), + makeBinding({ targetSessionKey: "agent:ops:subagent:other" }), + ]); + removeBindingRecordMock.mockReturnValue(true); + getManagerMock.mockReturnValue({ persist }); + + await handleMatrixSubagentEnded({ + targetSessionKey: CHILD_SESSION_KEY, + targetKind: "subagent", + accountId: "ops", + }); + + expect(removeBindingRecordMock).toHaveBeenCalledTimes(1); + expect(persist).toHaveBeenCalledTimes(1); + }); + + it("uses the binding service for requested farewell cleanup", async () => { + const binding = makeBinding(); + listBindingsForAccountMock.mockReturnValue([binding]); + unbindMock.mockResolvedValue([{ ...binding, bindingId: "ops:!room:example:$thread" }]); + + await handleMatrixSubagentEnded({ + targetSessionKey: CHILD_SESSION_KEY, + targetKind: "subagent", + accountId: "ops", + reason: "subagent-complete", + sendFarewell: true, + }); + + expect(unbindMock).toHaveBeenCalledWith({ + bindingId: "ops:!room:example:$thread", + reason: "subagent-complete", + }); + expect(removeBindingRecordMock).not.toHaveBeenCalled(); }); }); diff --git a/extensions/matrix/src/matrix/subagent-hooks.ts b/extensions/matrix/src/matrix/subagent-hooks.ts index 37259aec361b..729df3b6caab 100644 --- a/extensions/matrix/src/matrix/subagent-hooks.ts +++ b/extensions/matrix/src/matrix/subagent-hooks.ts @@ -1,17 +1,6 @@ // Matrix plugin module implements subagent hooks behavior. -import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/account-id"; -import { - getSessionBindingService, - type SessionBindingRecord, -} from "openclaw/plugin-sdk/conversation-binding-runtime"; -import { - formatThreadBindingDisabledError, - formatThreadBindingSpawnDisabledError, - resolveThreadBindingSpawnPolicy, -} from "openclaw/plugin-sdk/conversation-runtime"; -import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core"; +import { getSessionBindingService } from "openclaw/plugin-sdk/conversation-binding-runtime"; import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime"; -import { resolveMatrixTargetIdentity } from "./target-ids.js"; import { getMatrixThreadBindingManager, listAllBindings, @@ -20,19 +9,6 @@ import { resolveBindingKey, } from "./thread-bindings-shared.js"; -type MatrixSubagentSpawningEvent = { - threadRequested: boolean; - requester?: { - channel?: string; - accountId?: string; - to?: string; - threadId?: string | number; - }; - childSessionKey: string; - agentId: string; - label?: string; -}; - type MatrixSubagentEndedEvent = { targetSessionKey: string; targetKind: string; @@ -59,159 +35,10 @@ type MatrixDeliveryOrigin = { threadId?: string; }; -type SpawningResult = - | { - status: "ok"; - threadBindingReady?: boolean; - deliveryOrigin?: MatrixDeliveryOrigin; - } - | { status: "error"; error: string }; - type DeliveryTargetResult = { origin: MatrixDeliveryOrigin; }; -function summarizeError(err: unknown): string { - if (err instanceof Error) { - return err.message; - } - if (typeof err === "string") { - return err; - } - return "error"; -} - -function resolveMatrixBindingThreadId(binding: SessionBindingRecord): string | undefined { - const { conversationId, parentConversationId } = binding.conversation; - return parentConversationId && parentConversationId !== conversationId - ? conversationId - : undefined; -} - -function resolveMatrixBindingDeliveryOrigin( - binding: SessionBindingRecord, - fallbackAccountId: string, -): MatrixDeliveryOrigin { - const boundRoomId = - binding.conversation.parentConversationId ?? binding.conversation.conversationId; - const threadId = resolveMatrixBindingThreadId(binding); - return { - channel: "matrix", - accountId: binding.conversation.accountId ?? fallbackAccountId, - to: `room:${boundRoomId}`, - ...(threadId ? { threadId } : {}), - }; -} - -export async function handleMatrixSubagentSpawning( - api: OpenClawPluginApi, - event: MatrixSubagentSpawningEvent, -): Promise { - if (!event.threadRequested) { - return undefined; - } - const channel = event.requester?.channel?.trim().toLowerCase(); - if (channel !== "matrix") { - return undefined; - } - - // Normalize early so per-account config and manager lookup use the same id. - // Falls back to DEFAULT_ACCOUNT_ID so accounts.default.threadBindings.* is - // respected even when the requester omits accountId. - const accountId = normalizeOptionalString(event.requester?.accountId) || DEFAULT_ACCOUNT_ID; - const policy = resolveThreadBindingSpawnPolicy({ - cfg: api.config, - channel: "matrix", - accountId, - kind: "subagent", - }); - - if (!policy.enabled) { - return { - status: "error", - error: formatThreadBindingDisabledError({ - channel: policy.channel, - accountId: policy.accountId, - kind: "subagent", - }), - } satisfies SpawningResult; - } - if (!policy.spawnEnabled) { - return { - status: "error", - error: formatThreadBindingSpawnDisabledError({ - channel: policy.channel, - accountId: policy.accountId, - kind: "subagent", - }), - }; - } - - // Resolve the raw Matrix room ID from the requester's `to` field - // (e.g. "room:!abc123:example.org" → "!abc123:example.org"). - const rawTo = normalizeOptionalString(event.requester?.to) ?? ""; - const matrixTarget = rawTo ? resolveMatrixTargetIdentity(rawTo) : null; - const roomId = matrixTarget?.kind === "room" ? matrixTarget.id : ""; - - if (!roomId) { - return { - status: "error", - error: - "Cannot create Matrix thread binding: no room target in spawn request (requester.to must be a Matrix room ID).", - }; - } - - const bindingService = getSessionBindingService(); - const capabilities = bindingService.getCapabilities({ channel: "matrix", accountId }); - if (!capabilities.adapterAvailable || !capabilities.bindSupported) { - return { - status: "error", - error: `No Matrix session binding adapter available for account "${accountId}". Is the Matrix channel running?`, - }; - } - if (!capabilities.placements.includes("child")) { - return { - status: "error", - error: `Matrix session binding adapter for account "${accountId}" does not support child thread bindings.`, - }; - } - - try { - // placement="child" tells the Matrix SessionBindingAdapter to: - // 1. Send an intro message to the room, creating a new thread root event - // 2. Use the returned event ID as boundConversationId (the thread ID) - // 3. Register the binding record in the in-memory store and persist it - // - // We do NOT call setBindingRecord here — the adapter's bind() handles - // record creation, thread creation, and persistence atomically. - const binding = await bindingService.bind({ - targetSessionKey: event.childSessionKey, - targetKind: "subagent", - conversation: { - channel: "matrix", - accountId, - conversationId: roomId, - }, - placement: "child", - metadata: { - agentId: event.agentId?.trim() || undefined, - label: normalizeOptionalString(event.label) || undefined, - boundBy: "system", - }, - }); - return { - status: "ok", - threadBindingReady: true, - deliveryOrigin: resolveMatrixBindingDeliveryOrigin(binding, accountId), - } satisfies SpawningResult; - } catch (err) { - return { - status: "error", - error: `Matrix thread bind failed: ${summarizeError(err)}`, - }; - } -} - export async function handleMatrixSubagentEnded(event: MatrixSubagentEndedEvent): Promise { const accountId = normalizeOptionalString(event.accountId) || undefined; // Use the targeted account list when available; fall back to a full scan diff --git a/extensions/memory-core/src/memory/embeddings.test.ts b/extensions/memory-core/src/memory/embeddings.test.ts index f5ca8e5b8564..2d1d276b0406 100644 --- a/extensions/memory-core/src/memory/embeddings.test.ts +++ b/extensions/memory-core/src/memory/embeddings.test.ts @@ -63,8 +63,6 @@ vi.mock("openclaw/plugin-sdk/memory-core-host-engine-embeddings", () => ({ }, listMemoryEmbeddingProviders: () => [...mockEmbeddingRegistry.adapters], listRegisteredMemoryEmbeddingProviderAdapters: () => [...mockEmbeddingRegistry.adapters], - listRegisteredMemoryEmbeddingProviders: () => - mockEmbeddingRegistry.adapters.map((adapter) => ({ adapter })), })); const missingBedrockCredentialsError = new Error( @@ -116,7 +114,7 @@ function createMissingCredentialsAdapter( }; } -function clearMemoryEmbeddingProviders(): void { +function clearTestMemoryAdapters(): void { mockEmbeddingRegistry.genericAdapters = []; mockEmbeddingRegistry.adapters = []; mockEmbeddingRegistry.genericLookupConfigs = []; @@ -129,7 +127,7 @@ function registerGenericEmbeddingProvider(adapter: EmbeddingProviderAdapter): vo mockEmbeddingRegistry.genericAdapters.push(adapter); } -function registerMemoryEmbeddingProvider(adapter: MemoryEmbeddingProviderAdapter): void { +function registerTestMemoryAdapter(adapter: MemoryEmbeddingProviderAdapter): void { mockEmbeddingRegistry.adapters = mockEmbeddingRegistry.adapters.filter( (candidate) => candidate.id !== adapter.id, ); @@ -138,17 +136,17 @@ function registerMemoryEmbeddingProvider(adapter: MemoryEmbeddingProviderAdapter describe("createEmbeddingProvider", () => { beforeEach(() => { - clearMemoryEmbeddingProviders(); + clearTestMemoryAdapters(); mockEmbeddingRegistry.acquireLocalService.mockReset(); }); afterEach(() => { - clearMemoryEmbeddingProviders(); + clearTestMemoryAdapters(); }); it("normalizes legacy auto mode to OpenAI", async () => { - registerMemoryEmbeddingProvider(createMissingCredentialsAdapter({ id: "bedrock" })); - registerMemoryEmbeddingProvider({ + registerTestMemoryAdapter(createMissingCredentialsAdapter({ id: "bedrock" })); + registerTestMemoryAdapter({ id: "openai", transport: "remote", autoSelectPriority: 20, @@ -169,7 +167,7 @@ describe("createEmbeddingProvider", () => { }); it("still throws missing credentials for an explicit provider request", async () => { - registerMemoryEmbeddingProvider(createMissingCredentialsAdapter()); + registerTestMemoryAdapter(createMissingCredentialsAdapter()); await expect(createEmbeddingProvider(createOptions("bedrock"))).rejects.toThrow( missingBedrockCredentialsError.message, @@ -191,7 +189,7 @@ describe("createEmbeddingProvider", () => { const create = vi.fn(async () => { throw new Error("synthetic primary provider unavailable"); }); - registerMemoryEmbeddingProvider({ id: "openai", create }); + registerTestMemoryAdapter({ id: "openai", create }); await expect( createEmbeddingProvider({ ...createOptions("openai"), fallback: "openai" }), @@ -214,8 +212,8 @@ describe("createEmbeddingProvider", () => { embedBatch: async (texts) => texts.map(() => [1]), }, })); - registerMemoryEmbeddingProvider({ id: "openai", create: primaryCreate }); - registerMemoryEmbeddingProvider({ id: fallback, create: fallbackCreate }); + registerTestMemoryAdapter({ id: "openai", create: primaryCreate }); + registerTestMemoryAdapter({ id: fallback, create: fallbackCreate }); const sharedRemote = { nonBatchConcurrency: 3, @@ -292,8 +290,8 @@ describe("createEmbeddingProvider", () => { ); it("does not run priority-based auto-selection after a skippable setup failure", async () => { - registerMemoryEmbeddingProvider(createMissingCredentialsAdapter({ autoSelectPriority: 10 })); - registerMemoryEmbeddingProvider({ + registerTestMemoryAdapter(createMissingCredentialsAdapter({ autoSelectPriority: 10 })); + registerTestMemoryAdapter({ id: "openai", transport: "remote", autoSelectPriority: 20, @@ -396,7 +394,7 @@ describe("createEmbeddingProvider", () => { }, }), }); - registerMemoryEmbeddingProvider({ + registerTestMemoryAdapter({ id: "openai-compatible", create: async () => ({ provider: { @@ -421,7 +419,7 @@ describe("createEmbeddingProvider", () => { }); it("does not auto-select generic providers by priority policy", async () => { - registerMemoryEmbeddingProvider({ + registerTestMemoryAdapter({ id: "openai-compatible", transport: "remote", autoSelectPriority: 20, diff --git a/extensions/memory-core/src/memory/generic-embedding-provider.bridge.test.ts b/extensions/memory-core/src/memory/generic-embedding-provider.bridge.test.ts index c5dfffa275e7..367c3aff978b 100644 --- a/extensions/memory-core/src/memory/generic-embedding-provider.bridge.test.ts +++ b/extensions/memory-core/src/memory/generic-embedding-provider.bridge.test.ts @@ -16,10 +16,6 @@ import { listRegisteredEmbeddingProviders, type RegisteredEmbeddingProvider, restoreRegisteredEmbeddingProviders, - clearMemoryEmbeddingProviders, - listRegisteredMemoryEmbeddingProviders, - type RegisteredMemoryEmbeddingProvider, - restoreRegisteredMemoryEmbeddingProviders, setActivePluginRegistry, } from "openclaw/plugin-sdk/plugin-test-runtime"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; @@ -32,7 +28,6 @@ type CapturedCall = { }; let embeddingProvidersSnapshot: RegisteredEmbeddingProvider[]; -let memoryEmbeddingProvidersSnapshot: RegisteredMemoryEmbeddingProvider[]; let previousPluginRegistry: ReturnType; function createOptions(config: OpenClawConfig) { @@ -49,17 +44,13 @@ function createOptions(config: OpenClawConfig) { beforeEach(() => { previousPluginRegistry = getActivePluginRegistry(); embeddingProvidersSnapshot = listRegisteredEmbeddingProviders(); - memoryEmbeddingProvidersSnapshot = listRegisteredMemoryEmbeddingProviders(); clearEmbeddingProviders(); - clearMemoryEmbeddingProviders(); }); afterEach(() => { clearEmbeddingProviders(); - clearMemoryEmbeddingProviders(); setActivePluginRegistry(previousPluginRegistry ?? createEmptyPluginRegistry()); restoreRegisteredEmbeddingProviders(embeddingProvidersSnapshot); - restoreRegisteredMemoryEmbeddingProviders(memoryEmbeddingProvidersSnapshot); }); describe("memory-core generic embedding provider bridge", () => { @@ -156,8 +147,6 @@ describe("memory-core generic embedding provider bridge", () => { expect(registry.registry.embeddingProviders.map((entry) => entry.provider.id)).toEqual([ "virtual-generic", ]); - expect(listRegisteredMemoryEmbeddingProviders()).toEqual([]); - expect(resolveEmbeddingProviderIndexIdentity(createOptions(config))).toEqual({ provider: { id: "virtual-generic", model: "virtual-model" }, cacheKeyData: { diff --git a/extensions/memory-core/src/memory/generic-embedding-provider.integration.test.ts b/extensions/memory-core/src/memory/generic-embedding-provider.integration.test.ts index 70cebbb20f8b..de41f61894cf 100644 --- a/extensions/memory-core/src/memory/generic-embedding-provider.integration.test.ts +++ b/extensions/memory-core/src/memory/generic-embedding-provider.integration.test.ts @@ -4,12 +4,8 @@ import type { AddressInfo } from "node:net"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; import { clearEmbeddingProviders, - clearMemoryEmbeddingProviders, - getActivePluginRegistry, listRegisteredEmbeddingProviders, - listRegisteredMemoryEmbeddingProviders, restoreRegisteredEmbeddingProviders, - restoreRegisteredMemoryEmbeddingProviders, } from "openclaw/plugin-sdk/plugin-test-runtime"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { createEmbeddingProvider } from "./embeddings.js"; @@ -29,10 +25,6 @@ type TestServer = { const servers: TestServer[] = []; let registeredEmbeddingProvidersSnapshot: ReturnType; -let registeredMemoryEmbeddingProvidersSnapshot: ReturnType< - typeof listRegisteredMemoryEmbeddingProviders ->; -let restoreActiveMemoryEmbeddingProviders: (() => void) | undefined; async function readJsonBody(req: IncomingMessage): Promise> { const chunks: Buffer[] = []; @@ -129,35 +121,19 @@ function createMemoryEmbeddingOptions(overrides?: { beforeEach(() => { registeredEmbeddingProvidersSnapshot = listRegisteredEmbeddingProviders(); - registeredMemoryEmbeddingProvidersSnapshot = listRegisteredMemoryEmbeddingProviders(); clearEmbeddingProviders(); - clearMemoryEmbeddingProviders(); - - const activeRegistry = getActivePluginRegistry(); - if (activeRegistry) { - const memoryEmbeddingProviders = activeRegistry.memoryEmbeddingProviders; - activeRegistry.memoryEmbeddingProviders = []; - restoreActiveMemoryEmbeddingProviders = () => { - activeRegistry.memoryEmbeddingProviders = memoryEmbeddingProviders; - }; - } else { - restoreActiveMemoryEmbeddingProviders = undefined; - } }); afterEach(async () => { const pendingServers = servers.splice(0); await Promise.all(pendingServers.map((server) => server.close())); restoreRegisteredEmbeddingProviders(registeredEmbeddingProvidersSnapshot); - restoreRegisteredMemoryEmbeddingProviders(registeredMemoryEmbeddingProvidersSnapshot); - restoreActiveMemoryEmbeddingProviders?.(); }); describe("memory-core generic embedding provider bridge", () => { it("uses the core OpenAI-compatible provider through the generic registry and memory bridge", async () => { const server = await startEmbeddingServer(); - expect(listRegisteredMemoryEmbeddingProviders()).toEqual([]); expect(listRegisteredEmbeddingProviders()).toMatchObject([ { ownerPluginId: "core", diff --git a/extensions/memory-core/src/memory/manager-index.test-support.ts b/extensions/memory-core/src/memory/manager-index.test-support.ts index 00fcf0051c14..603aced6e84d 100644 --- a/extensions/memory-core/src/memory/manager-index.test-support.ts +++ b/extensions/memory-core/src/memory/manager-index.test-support.ts @@ -1,9 +1,9 @@ import { mkdirSync, rmSync } from "node:fs"; import fs from "node:fs/promises"; import path from "node:path"; -import { clearMemoryEmbeddingProviders as clearRegistry } from "openclaw/plugin-sdk/memory-core-host-engine-embeddings"; import type { OpenClawConfig } from "openclaw/plugin-sdk/memory-core-host-engine-foundation"; import { resolveSessionTranscriptsDirForAgent } from "openclaw/plugin-sdk/memory-core-host-runtime-core"; +import { clearEmbeddingProviders as clearRegistry } from "openclaw/plugin-sdk/plugin-test-runtime"; import { upsertSessionEntry } from "openclaw/plugin-sdk/session-store-runtime"; import { appendSessionTranscriptMessageByIdentity } from "openclaw/plugin-sdk/session-transcript-runtime"; import { diff --git a/extensions/memory-core/src/memory/manager-provider-lifecycle-fallback.test.ts b/extensions/memory-core/src/memory/manager-provider-lifecycle-fallback.test.ts index 650ee44f9972..1835ce9f0632 100644 --- a/extensions/memory-core/src/memory/manager-provider-lifecycle-fallback.test.ts +++ b/extensions/memory-core/src/memory/manager-provider-lifecycle-fallback.test.ts @@ -276,7 +276,7 @@ describe("memory index", () => { const manager = await getFreshManager(cfg); try { await expect(manager.search("Alpha")).rejects.toThrow( - /Memory search unavailable: embedding provider "openai" is configured but unavailable\.[\s\S]*agentId=main purpose=default[\s\S]*registeredMemoryEmbeddingProviders=none/, + /Memory search unavailable: embedding provider "openai" is configured but unavailable\.[\s\S]*agentId=main purpose=default[\s\S]*registeredMemoryEmbeddingProviders=openai-compatible/, ); await expect(manager.sync({ reason: "test" })).rejects.toThrow( /Memory sync unavailable: embedding provider "openai" is configured but unavailable\./, diff --git a/extensions/memory-core/src/memory/manager.watcher-config.test.ts b/extensions/memory-core/src/memory/manager.watcher-config.test.ts index 94678a102e62..e3dc464f1caa 100644 --- a/extensions/memory-core/src/memory/manager.watcher-config.test.ts +++ b/extensions/memory-core/src/memory/manager.watcher-config.test.ts @@ -169,7 +169,7 @@ vi.mock("./embeddings.js", () => ({ }), })); -import { clearMemoryEmbeddingProviders as clearRegistry } from "openclaw/plugin-sdk/memory-core-host-engine-embeddings"; +import { clearEmbeddingProviders as clearRegistry } from "openclaw/plugin-sdk/plugin-test-runtime"; import { closeAllMemorySearchManagers, getMemorySearchManager } from "./index.js"; import type { MemoryIndexManager } from "./manager.js"; import { isolateMemoryManagerTestConfig } from "./test-config-helpers.js"; diff --git a/extensions/mistral/index.ts b/extensions/mistral/index.ts index fadaeafc2916..a8e913ebf497 100644 --- a/extensions/mistral/index.ts +++ b/extensions/mistral/index.ts @@ -1,3 +1,4 @@ +import { adaptMemoryEmbeddingProviderAdapter } from "openclaw/plugin-sdk/memory-core-host-engine-embeddings"; import { defineSingleProviderPluginEntry } from "openclaw/plugin-sdk/provider-entry"; import { applyMistralModelCompat, @@ -44,7 +45,9 @@ export default defineSingleProviderPluginEntry({ buildReplayPolicy: () => buildMistralReplayPolicy(), }, register(api) { - api.registerMemoryEmbeddingProvider(mistralMemoryEmbeddingProviderAdapter); + api.registerEmbeddingProvider( + adaptMemoryEmbeddingProviderAdapter(mistralMemoryEmbeddingProviderAdapter), + ); api.registerMediaUnderstandingProvider(mistralMediaUnderstandingProvider); api.registerRealtimeTranscriptionProvider(buildMistralRealtimeTranscriptionProvider()); }, diff --git a/extensions/mistral/openclaw.plugin.json b/extensions/mistral/openclaw.plugin.json index af45d5fb25fb..df6ae84f7e3e 100644 --- a/extensions/mistral/openclaw.plugin.json +++ b/extensions/mistral/openclaw.plugin.json @@ -156,7 +156,7 @@ } ], "contracts": { - "memoryEmbeddingProviders": ["mistral"], + "embeddingProviders": ["mistral"], "mediaUnderstandingProviders": ["mistral"], "realtimeTranscriptionProviders": ["mistral"] }, diff --git a/extensions/ollama/index.ts b/extensions/ollama/index.ts index f59d4ecc207b..a4be634634fb 100644 --- a/extensions/ollama/index.ts +++ b/extensions/ollama/index.ts @@ -4,7 +4,10 @@ import { collectConfiguredModelRefValues } from "@openclaw/model-catalog-core/co import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; import { createLazyRuntimeModule } from "openclaw/plugin-sdk/lazy-runtime"; import type { MediaUnderstandingProvider } from "openclaw/plugin-sdk/media-understanding"; -import type { MemoryEmbeddingProviderAdapter } from "openclaw/plugin-sdk/memory-core-host-engine-embeddings"; +import { + adaptMemoryEmbeddingProviderAdapter, + type MemoryEmbeddingProviderAdapter, +} from "openclaw/plugin-sdk/memory-core-host-engine-embeddings"; import { resolvePluginConfigObject } from "openclaw/plugin-sdk/plugin-config-runtime"; import { definePluginEntry, @@ -890,7 +893,9 @@ export default definePluginEntry({ if (api.registrationMode === "full") { void checkWsl2CrashLoopRiskLazily(api); } - api.registerMemoryEmbeddingProvider(lazyOllamaMemoryEmbeddingProviderAdapter); + api.registerEmbeddingProvider( + adaptMemoryEmbeddingProviderAdapter(lazyOllamaMemoryEmbeddingProviderAdapter), + ); api.registerMediaUnderstandingProvider(lazyOllamaMediaUnderstandingProvider); if (startupPluginConfig.nodeInference?.enabled !== false) { for (const command of createLazyOllamaNodeHostCommands()) { diff --git a/extensions/ollama/lazy-import.test.ts b/extensions/ollama/lazy-import.test.ts index a7a8e6b6dbbf..2c4433dd1f15 100644 --- a/extensions/ollama/lazy-import.test.ts +++ b/extensions/ollama/lazy-import.test.ts @@ -1,5 +1,5 @@ +import type { EmbeddingProviderAdapter } from "openclaw/plugin-sdk/embedding-providers"; import type { MediaUnderstandingProvider } from "openclaw/plugin-sdk/media-understanding"; -import type { MemoryEmbeddingProviderAdapter } from "openclaw/plugin-sdk/memory-core-host-engine-embeddings"; import type { AnyAgentTool, OpenClawPluginNodeHostCommand, @@ -138,7 +138,7 @@ describe("ollama lazy imports", () => { }); const { default: ollamaPlugin } = await import("./index.js"); - let embeddingAdapter: MemoryEmbeddingProviderAdapter | undefined; + let embeddingAdapter: EmbeddingProviderAdapter | undefined; let mediaProvider: MediaUnderstandingProvider | undefined; const nodeCommands: OpenClawPluginNodeHostCommand[] = []; const providers: ProviderPlugin[] = []; @@ -151,7 +151,7 @@ describe("ollama lazy imports", () => { source: "test", config: {}, runtime: createPluginRuntimeMock(), - registerMemoryEmbeddingProvider: (adapter) => { + registerEmbeddingProvider: (adapter) => { embeddingAdapter = adapter; }, registerMediaUnderstandingProvider: (provider) => { diff --git a/extensions/ollama/openclaw.plugin.json b/extensions/ollama/openclaw.plugin.json index 98f9090dfe6d..90a3aa3836a4 100644 --- a/extensions/ollama/openclaw.plugin.json +++ b/extensions/ollama/openclaw.plugin.json @@ -188,7 +188,7 @@ } }, "contracts": { - "memoryEmbeddingProviders": [ + "embeddingProviders": [ "ollama" ], "tools": [ diff --git a/extensions/openai/index.ts b/extensions/openai/index.ts index b6b05cfa0d24..01f1ad8a2dc2 100644 --- a/extensions/openai/index.ts +++ b/extensions/openai/index.ts @@ -1,5 +1,6 @@ // Openai plugin entrypoint registers its OpenClaw integration. import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; +import { adaptMemoryEmbeddingProviderAdapter } from "openclaw/plugin-sdk/memory-core-host-engine-embeddings"; import { resolvePluginConfigObject } from "openclaw/plugin-sdk/plugin-config-runtime"; import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry"; import { buildProviderToolCompatFamilyHooks } from "openclaw/plugin-sdk/provider-tools"; @@ -73,7 +74,9 @@ export default definePluginEntry({ }, }); api.registerProvider(buildProviderWithPromptContribution(buildOpenAIProvider())); - api.registerMemoryEmbeddingProvider(openAiMemoryEmbeddingProviderAdapter); + api.registerEmbeddingProvider( + adaptMemoryEmbeddingProviderAdapter(openAiMemoryEmbeddingProviderAdapter), + ); api.registerImageGenerationProvider(buildOpenAIImageGenerationProvider()); api.registerRealtimeTranscriptionProvider(buildOpenAIRealtimeTranscriptionProvider()); api.registerRealtimeVoiceProvider( diff --git a/extensions/openai/openclaw.plugin.json b/extensions/openai/openclaw.plugin.json index 2c2f40d27991..37357f9ffeaa 100644 --- a/extensions/openai/openclaw.plugin.json +++ b/extensions/openai/openclaw.plugin.json @@ -244,7 +244,7 @@ "speechProviders": ["openai"], "realtimeTranscriptionProviders": ["openai"], "realtimeVoiceProviders": ["openai"], - "memoryEmbeddingProviders": ["openai"], + "embeddingProviders": ["openai"], "mediaUnderstandingProviders": ["openai"], "imageGenerationProviders": ["openai"], "videoGenerationProviders": ["openai"], diff --git a/extensions/qa-lab/src/live-transports/matrix/scenarios/scenario-runtime-thread.ts b/extensions/qa-lab/src/live-transports/matrix/scenarios/scenario-runtime-thread.ts index 66dc1ecf6962..cbfd21a80ae6 100644 --- a/extensions/qa-lab/src/live-transports/matrix/scenarios/scenario-runtime-thread.ts +++ b/extensions/qa-lab/src/live-transports/matrix/scenarios/scenario-runtime-thread.ts @@ -23,9 +23,6 @@ import type { MatrixQaCanaryArtifact, MatrixQaScenarioExecution } from "./scenar type MatrixQaThreadScenarioResult = Awaited>; -const MATRIX_SUBAGENT_THREAD_HOOK_ERROR_RE = - /thread=true is unavailable because no channel plugin registered subagent_spawning hooks/i; - function assertMatrixQaInReplyTarget(params: { actualEventId?: string; expectedEventId: string; @@ -59,9 +56,6 @@ function buildMatrixQaThreadArtifacts(result: MatrixQaThreadScenarioResult) { function failIfMatrixSubagentThreadHookError(event: MatrixQaObservedEvent) { const body = event.body ?? ""; - if (MATRIX_SUBAGENT_THREAD_HOOK_ERROR_RE.test(body)) { - throw new Error(`Matrix subagent thread spawn hit missing hook error: ${body || ""}`); - } if (/\bsessions_spawn failed:/i.test(body)) { throw new Error(`Matrix subagent thread spawn failed: ${body || ""}`); } diff --git a/extensions/voyage/index.ts b/extensions/voyage/index.ts index bfdb7048e033..19e80f2a8027 100644 --- a/extensions/voyage/index.ts +++ b/extensions/voyage/index.ts @@ -1,3 +1,4 @@ +import { adaptMemoryEmbeddingProviderAdapter } from "openclaw/plugin-sdk/memory-core-host-engine-embeddings"; // Voyage plugin entrypoint registers its OpenClaw integration. import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry"; import { voyageMemoryEmbeddingProviderAdapter } from "./memory-embedding-adapter.js"; @@ -7,6 +8,8 @@ export default definePluginEntry({ name: "Voyage Embeddings", description: "Voyage memory embedding provider plugin", register(api) { - api.registerMemoryEmbeddingProvider(voyageMemoryEmbeddingProviderAdapter); + api.registerEmbeddingProvider( + adaptMemoryEmbeddingProviderAdapter(voyageMemoryEmbeddingProviderAdapter), + ); }, }); diff --git a/extensions/voyage/openclaw.plugin.json b/extensions/voyage/openclaw.plugin.json index e9a6302c618f..c3aeaf986f9a 100644 --- a/extensions/voyage/openclaw.plugin.json +++ b/extensions/voyage/openclaw.plugin.json @@ -5,7 +5,7 @@ }, "enabledByDefault": true, "contracts": { - "memoryEmbeddingProviders": ["voyage"] + "embeddingProviders": ["voyage"] }, "setup": { "providers": [ diff --git a/extensions/whatsapp/api.ts b/extensions/whatsapp/api.ts index 19cdf4379e94..1201017fcc45 100644 --- a/extensions/whatsapp/api.ts +++ b/extensions/whatsapp/api.ts @@ -42,10 +42,8 @@ export { export { type ActiveWebListener, type ActiveWebSendOptions, - type LegacyFlatWebInboundMessage, type WebInboundCallbackMessage, type WebInboundMessage, - type WebInboundMessageInput, type WebListenerCloseReason, type WhatsAppStructuredContactContext, } from "./src/inbound/types.js"; diff --git a/extensions/whatsapp/runtime-api.ts b/extensions/whatsapp/runtime-api.ts index 5a3e958642b2..a35d2cab85aa 100644 --- a/extensions/whatsapp/runtime-api.ts +++ b/extensions/whatsapp/runtime-api.ts @@ -49,10 +49,8 @@ export { extractText, monitorWebInbox, resetWebInboundDedupe, - type LegacyFlatWebInboundMessage, type WebInboundCallbackMessage, type WebInboundMessage, - type WebInboundMessageInput, type WebListenerCloseReason, type WhatsAppInboundAdmission, } from "./src/inbound.js"; diff --git a/extensions/whatsapp/src/auto-reply.broadcast-groups.test-harness.ts b/extensions/whatsapp/src/auto-reply.broadcast-groups.test-harness.ts index fc4a90e15df2..44be8ba75c30 100644 --- a/extensions/whatsapp/src/auto-reply.broadcast-groups.test-harness.ts +++ b/extensions/whatsapp/src/auto-reply.broadcast-groups.test-harness.ts @@ -7,7 +7,7 @@ import { sendWebDirectInboundMessage, } from "./auto-reply.test-harness.js"; import { monitorWebChannel } from "./auto-reply/monitor.js"; -import type { WebInboundMessageInput } from "./inbound.js"; +import type { WebInboundCallbackMessage } from "./inbound.js"; vi.mock("openclaw/plugin-sdk/channel-inbound", async (importOriginal) => { const actual = await importOriginal(); @@ -44,7 +44,7 @@ vi.mock("openclaw/plugin-sdk/channel-inbound", async (importOriginal) => { export async function monitorWebChannelWithCapture(resolver: unknown): Promise<{ spies: ReturnType; - onMessage: (msg: WebInboundMessageInput) => Promise; + onMessage: (msg: WebInboundCallbackMessage) => Promise; }> { const spies = createWebInboundDeliverySpies(); const { listenerFactory, getOnMessage } = createWebListenerFactoryCapture(); diff --git a/extensions/whatsapp/src/auto-reply.test-harness.ts b/extensions/whatsapp/src/auto-reply.test-harness.ts index c246960c0d10..433fc4029653 100644 --- a/extensions/whatsapp/src/auto-reply.test-harness.ts +++ b/extensions/whatsapp/src/auto-reply.test-harness.ts @@ -10,7 +10,7 @@ import { resetLogger, setLoggerOverride } from "openclaw/plugin-sdk/runtime-env" import { mockPinnedHostnameResolution } from "openclaw/plugin-sdk/test-env"; import { afterAll, afterEach, beforeAll, beforeEach, vi, type Mock } from "vitest"; import type { WebChannelStatus } from "./auto-reply/types.js"; -import type { WebInboundMessageInput, WebListenerCloseReason } from "./inbound.js"; +import type { WebInboundCallbackMessage, WebListenerCloseReason } from "./inbound.js"; import type { WhatsAppSendResult } from "./inbound/send-result.js"; import { createAcceptedWhatsAppSendResult as createAcceptedWhatsAppSendResultForHarness } from "./inbound/send-result.test-helper.js"; import { createTestWebInboundMessage } from "./inbound/test-message.test-helper.js"; @@ -252,19 +252,19 @@ export function installWebAutoReplyUnitTestHooks(opts?: { pinDns?: boolean }) { } export function createWebListenerFactoryCapture(): AnyExport { - let capturedOnMessage: ((msg: WebInboundMessageInput) => Promise) | undefined; + let capturedOnMessage: ((msg: WebInboundCallbackMessage) => Promise) | undefined; let capturedOptions: | { - onMessage: (msg: WebInboundMessageInput) => Promise; - shouldDebounce?: (msg: WebInboundMessageInput) => boolean; + onMessage: (msg: WebInboundCallbackMessage) => Promise; + shouldDebounce?: (msg: WebInboundCallbackMessage) => boolean; debounceMs?: number; appendReplyWindow?: { afterMs: number; untilMs: number; maxAgeMs: number }; selfChatMode?: boolean; } | undefined; const listenerFactory = async (opts: { - onMessage: (msg: WebInboundMessageInput) => Promise; - shouldDebounce?: (msg: WebInboundMessageInput) => boolean; + onMessage: (msg: WebInboundCallbackMessage) => Promise; + shouldDebounce?: (msg: WebInboundCallbackMessage) => boolean; debounceMs?: number; appendReplyWindow?: { afterMs: number; untilMs: number; maxAgeMs: number }; selfChatMode?: boolean; @@ -306,12 +306,12 @@ export function createMockWebListener(): MockWebListener { } export function createScriptedWebListenerFactory(): AnyExport { - const onMessages: Array<(msg: WebInboundMessageInput) => Promise> = []; + const onMessages: Array<(msg: WebInboundCallbackMessage) => Promise> = []; const closeResolvers: Array<(reason: unknown) => void> = []; const listeners: MockWebListener[] = []; const listenerFactory = vi.fn( - async (opts: { onMessage: (msg: WebInboundMessageInput) => Promise }) => { + async (opts: { onMessage: (msg: WebInboundCallbackMessage) => Promise }) => { onMessages.push(opts.onMessage); let resolveClose: (reason: unknown) => void = () => {}; const onClose = new Promise((res) => { @@ -391,7 +391,7 @@ export function startWebAutoReplyMonitor(params: { } export async function sendWebGroupInboundMessage(params: { - onMessage: (msg: WebInboundMessageInput) => Promise; + onMessage: (msg: WebInboundCallbackMessage) => Promise; body: string; id: string; senderE164: string; @@ -445,7 +445,7 @@ export async function sendWebGroupInboundMessage(params: { } export async function sendWebDirectInboundMessage(params: { - onMessage: (msg: WebInboundMessageInput) => Promise; + onMessage: (msg: WebInboundCallbackMessage) => Promise; body: string; id: string; from: string; diff --git a/extensions/whatsapp/src/auto-reply.web-auto-reply.connection-and-logging.e2e.test.ts b/extensions/whatsapp/src/auto-reply.web-auto-reply.connection-and-logging.e2e.test.ts index b4a55848348e..c969c7d31084 100644 --- a/extensions/whatsapp/src/auto-reply.web-auto-reply.connection-and-logging.e2e.test.ts +++ b/extensions/whatsapp/src/auto-reply.web-auto-reply.connection-and-logging.e2e.test.ts @@ -30,11 +30,7 @@ import { } from "./auto-reply/deliver-reply.js"; import { buildInboundLine } from "./auto-reply/monitor/message-line.js"; import type { WebChannelStatus } from "./auto-reply/types.js"; -import { - createTestLegacyFlatWebInboundMessage, - createTestWebInboundMessage, -} from "./inbound/test-message.test-helper.js"; -import type { WebInboundMessageInput } from "./inbound/types.js"; +import { createTestWebInboundMessage } from "./inbound/test-message.test-helper.js"; import { waitForWaConnection } from "./session.js"; type DrainSelectionEntry = { @@ -864,71 +860,6 @@ describe("web auto-reply connection", () => { expect(capture.getLastOptions()?.debounceMs).toBe(250); }); - it("normalizes legacy flat listener messages and rejects partial nested input", async () => { - const capture = createWebListenerFactoryCapture(); - const { sendMedia, sendComposing, reply } = createWebInboundDeliverySpies(); - const resolver = vi.fn().mockResolvedValue(undefined); - - await monitorWebChannel(false, capture.listenerFactory as never, false, resolver); - const onMessage = requireOnMessage(capture.getOnMessage()); - const msg = createTestLegacyFlatWebInboundMessage({ - from: "+1", - conversationId: "+1", - chatId: "+1", - to: "+2", - accessControlPassed: false, - reply, - }); - - expect(capture.getLastOptions()?.shouldDebounce?.(msg)).toBe(true); - expect( - capture - .getLastOptions() - ?.shouldDebounce?.(createTestWebInboundMessage({ payload: { body: " " } })), - ).toBe(false); - expect( - capture.getLastOptions()?.shouldDebounce?.( - createTestWebInboundMessage({ - payload: { - body: "/stop\n\n[whatsapp attachment unavailable]", - commandBody: "/stop", - }, - platform: { sendComposing, reply, sendMedia }, - }), - ), - ).toBe(false); - await onMessage(msg); - - expect(resolver).not.toHaveBeenCalled(); - expect(reply).not.toHaveBeenCalled(); - await expect( - onMessage({ - event: { id: "canonical-no-admission" }, - payload: { body: "canonical" }, - platform: { - chatJid: "+3", - recipientJid: "+4", - sendComposing, - reply, - sendMedia, - }, - from: "+3", - conversationId: "+3", - accountId: "default", - chatType: "direct", - }), - ).rejects.toThrow(/missing admission facts/); - - expect(reply).not.toHaveBeenCalled(); - await expect( - onMessage({ - ...msg, - id: "partial-msg", - payload: { body: "partial nested" }, - } as unknown as WebInboundMessageInput), - ).rejects.toThrow(/legacy flat or canonical nested/); - }); - it("raises the process listener budget before opening the web listener", async () => { const originalMax = process.getMaxListeners(); process.setMaxListeners?.(1); diff --git a/extensions/whatsapp/src/auto-reply/monitor.ts b/extensions/whatsapp/src/auto-reply/monitor.ts index f1d774f4f51a..c3eae812bb33 100644 --- a/extensions/whatsapp/src/auto-reply/monitor.ts +++ b/extensions/whatsapp/src/auto-reply/monitor.ts @@ -33,9 +33,8 @@ import { type WhatsAppBaileysMessageCache, } from "../inbound/baileys-cache.js"; import type { WhatsAppGroupMetadataCache } from "../inbound/group-metadata-cache.js"; -import { normalizeAdmittedWebInboundMessage } from "../inbound/message-aliases.js"; import { attachWebInboxToSocket } from "../inbound/monitor.js"; -import type { WebInboundMessageInput } from "../inbound/types.js"; +import type { WebInboundCallbackMessage } from "../inbound/types.js"; import { newConnectionId, resolveHeartbeatSeconds, @@ -228,15 +227,13 @@ export async function monitorWebChannel( cfg, channel: "whatsapp", }); - const shouldDebounce = (msg: WebInboundMessageInput) => { - const admitted = normalizeAdmittedWebInboundMessage(msg); - return shouldDebounceTextInbound({ - text: admitted.payload.commandBody ?? admitted.payload.body, + const shouldDebounce = (msg: WebInboundCallbackMessage) => + shouldDebounceTextInbound({ + text: msg.payload.commandBody ?? msg.payload.body, cfg, - hasMedia: Boolean(admitted.payload.media?.path || admitted.payload.media?.type), - allowDebounce: !(admitted.payload.location || admitted.quote?.id || admitted.quote?.body), + hasMedia: Boolean(msg.payload.media?.path || msg.payload.media?.type), + allowDebounce: !(msg.payload.location || msg.quote?.id || msg.quote?.body), }); - }; let connection; try { @@ -294,14 +291,11 @@ export async function monitorWebChannel( groupMetadataCache, recentMessageKeys, baileysGroupMetaCache, - onMessage: async (msg: WebInboundMessageInput) => { - // Keep the deprecated injected-listener input contract at the WhatsApp edge. - // Auto-reply only receives the admitted canonical message. - const admitted = normalizeAdmittedWebInboundMessage(msg); + onMessage: async (msg: WebInboundCallbackMessage) => { const inboundAt = Date.now(); controller.noteInbound(inboundAt); statusController.noteInbound(inboundAt); - await onMessage(admitted); + await onMessage(msg); }, onPendingWorkChanged: (pendingWorkCount, at) => { statusController.noteBusy(pendingWorkCount > 0, at); diff --git a/extensions/whatsapp/src/auto-reply/monitor/on-message.audio-preflight.test.ts b/extensions/whatsapp/src/auto-reply/monitor/on-message.audio-preflight.test.ts index 6b62c33112e2..616fe18f92fa 100644 --- a/extensions/whatsapp/src/auto-reply/monitor/on-message.audio-preflight.test.ts +++ b/extensions/whatsapp/src/auto-reply/monitor/on-message.audio-preflight.test.ts @@ -95,70 +95,50 @@ vi.mock("openclaw/plugin-sdk/routing", () => ({ }), })); -import { normalizeAdmittedWebInboundMessage } from "../../inbound/message-aliases.js"; -import { - createTestLegacyFlatWebInboundMessage, - createTestWebAudioInboundMessage, -} from "../../inbound/test-message.test-helper.js"; -import type { AdmittedWebInboundMessage, WebInboundMessageInput } from "../../inbound/types.js"; +import { createTestWebAudioInboundMessage } from "../../inbound/test-message.test-helper.js"; +import type { AdmittedWebInboundMessage } from "../../inbound/types.js"; import { createWebOnMessageHandler } from "./on-message.js"; -function admitTestMessage(msg: WebInboundMessageInput): AdmittedWebInboundMessage { - return normalizeAdmittedWebInboundMessage(msg); -} - function makeAudioMsg(): AdmittedWebInboundMessage { - return admitTestMessage(createTestWebAudioInboundMessage()); -} - -function makeLegacyAudioMsg() { - return createTestLegacyFlatWebInboundMessage({ - body: "", - mediaPath: "/tmp/voice.ogg", - mediaType: "audio/ogg; codecs=opus", - }); + return createTestWebAudioInboundMessage(); } function makeGroupAudioMsg(): AdmittedWebInboundMessage { - return admitTestMessage( - createTestWebAudioInboundMessage({ - platform: { chatJid: "1203630@g.us" }, - admission: { - conversation: { - kind: "group", - id: "1203630@g.us", - }, - senderAccess: { - reasonCode: "group_policy_allowed", - }, + return createTestWebAudioInboundMessage({ + platform: { chatJid: "1203630@g.us" }, + admission: { + conversation: { + kind: "group", + id: "1203630@g.us", }, - wasMentioned: false, - }), - ); + senderAccess: { + reasonCode: "group_policy_allowed", + }, + }, + wasMentioned: false, + }); } function makeBlockedDirectAudioMsg(): AdmittedWebInboundMessage { - return admitTestMessage( - createTestWebAudioInboundMessage({ - admission: { - ingress: { - admission: "drop", - decision: "block", - reasonCode: "dm_policy_not_allowlisted", - }, - senderAccess: { - allowed: false, - decision: "block", - reasonCode: "dm_policy_not_allowlisted", - }, - activationAccess: { - allowed: false, - shouldSkip: true, - reasonCode: "dm_policy_not_allowlisted", - }, + return createTestWebAudioInboundMessage({ + admission: { + ingress: { + admission: "drop", + decision: "block", + reasonCode: "dm_policy_not_allowlisted", }, - }), - ); + senderAccess: { + allowed: false, + decision: "block", + reasonCode: "dm_policy_not_allowlisted", + }, + activationAccess: { + allowed: false, + shouldSkip: true, + reasonCode: "dm_policy_not_allowlisted", + }, + }, + }); } function mockObjectArg(mockFn: ReturnType, label: string, callIndex = 0) { @@ -277,49 +257,6 @@ describe("createWebOnMessageHandler audio preflight", () => { expect(processMessageMock).not.toHaveBeenCalled(); }); - it("skips early DM ack/preflight for legacy audio without explicit access proof", async () => { - const handler = makeHandler(); - - await handler(admitTestMessage(makeLegacyAudioMsg())); - - expect(events).toStrictEqual([]); - expect(transcribeFirstAudioMock).not.toHaveBeenCalled(); - expect(maybeSendAckReactionMock).not.toHaveBeenCalled(); - expect(processMessageMock).toHaveBeenCalledTimes(1); - const processParams = mockObjectArg(processMessageMock, "processMessage"); - expect(processParams).not.toHaveProperty("preflightAudioTranscript"); - expect(processParams).not.toHaveProperty("ackAlreadySent"); - expect(processParams).not.toHaveProperty("ackReaction"); - }); - - it("shares one transcript for legacy direct broadcasts without explicit access proof", async () => { - maybeBroadcastMessageMock.mockImplementation( - async (params: { preflightAudioTranscript?: string | null }) => { - expect(params.preflightAudioTranscript).toBe("transcribed voice note"); - return true; - }, - ); - const handler = makeHandler({ - cfg: { - channels: { - whatsapp: { - ackReaction: { enabled: true }, - }, - }, - broadcast: { - "+15551234567": ["main", "backup"], - }, - } as never, - }); - - await handler(admitTestMessage(makeLegacyAudioMsg())); - - expect(events).toStrictEqual(["stt"]); - expect(transcribeFirstAudioMock).toHaveBeenCalledTimes(1); - expect(maybeSendAckReactionMock).not.toHaveBeenCalled(); - expect(processMessageMock).not.toHaveBeenCalled(); - }); - it("preserves per-agent ack checks for group broadcast voice notes", async () => { maybeBroadcastMessageMock.mockImplementation( async (params: { diff --git a/extensions/whatsapp/src/auto-reply/monitor/on-message.ts b/extensions/whatsapp/src/auto-reply/monitor/on-message.ts index e5fad391052d..1abb1fc755de 100644 --- a/extensions/whatsapp/src/auto-reply/monitor/on-message.ts +++ b/extensions/whatsapp/src/auto-reply/monitor/on-message.ts @@ -13,15 +13,8 @@ import { logVerbose } from "openclaw/plugin-sdk/runtime-env"; import { resolveWhatsAppAccount } from "../../accounts.js"; import { resolveWhatsAppGroupSessionRoute } from "../../group-session-key.js"; import { getPrimaryIdentityId, getSenderIdentity } from "../../identity.js"; -import { - requireAdmittedWhatsAppInboundMessage, - requireWhatsAppInboundAdmission, -} from "../../inbound/admission.js"; -import { withDeprecatedWebInboundMessageFlatAliases } from "../../inbound/message-aliases.js"; -import type { - AdmittedWebInboundMessage, - DeprecatedWebInboundAdmissionTopLevelFields, -} from "../../inbound/types.js"; +import { requireWhatsAppInboundAdmission } from "../../inbound/admission.js"; +import type { AdmittedWebInboundMessage } from "../../inbound/types.js"; import { normalizeE164 } from "../../text-runtime.js"; import { buildMentionConfig } from "../mentions.js"; import type { MentionConfig } from "../mentions.js"; @@ -37,15 +30,6 @@ import { type StatusReactionController, } from "./status-reaction.js"; -function readDeprecatedAccessControlPassed(msg: AdmittedWebInboundMessage): boolean | undefined { - // The admitted type hides deprecated flat aliases, but normalized legacy - // listener inputs retain this one tri-state proof for preflight safety. - return ( - msg as AdmittedWebInboundMessage & - Pick - ).accessControlPassed; -} - export function createWebOnMessageHandler(params: { cfg: OpenClawConfig; loadConfig?: () => OpenClawConfig; @@ -62,12 +46,8 @@ export function createWebOnMessageHandler(params: { account: { authDir?: string; accountId?: string; selfChatMode?: boolean }; buildContext?: typeof import("openclaw/plugin-sdk/channel-inbound").buildChannelInboundEventContext; }) { - const hasExplicitlyPassedInboundAccess = (msg: AdmittedWebInboundMessage): boolean => { - if (msg.admission.ingress.decisiveGateId === "legacy-flat-compat") { - return readDeprecatedAccessControlPassed(msg) === true; - } - return msg.admission.ingress.decision === "allow"; - }; + const hasExplicitlyPassedInboundAccess = (msg: AdmittedWebInboundMessage): boolean => + msg.admission.ingress.decision === "allow"; const withDirectSenderPeer = ( msg: AdmittedWebInboundMessage, @@ -86,16 +66,14 @@ export function createWebOnMessageHandler(params: { if (!normalized) { return msg; } - return requireAdmittedWhatsAppInboundMessage( - withDeprecatedWebInboundMessageFlatAliases({ - ...msg, - platform: { - ...msg.platform, - sender: { ...msg.platform.sender, e164: normalized }, - senderE164: normalized, - }, - }), - ); + return { + ...msg, + platform: { + ...msg.platform, + sender: { ...msg.platform.sender, e164: normalized }, + senderE164: normalized, + }, + }; }; const processForRoute = async ( diff --git a/extensions/whatsapp/src/inbound.ts b/extensions/whatsapp/src/inbound.ts index 1aa6acce88cc..d6a14655a1dd 100644 --- a/extensions/whatsapp/src/inbound.ts +++ b/extensions/whatsapp/src/inbound.ts @@ -4,9 +4,7 @@ export { extractContactContext, extractLocationData, extractText } from "./inbou export { monitorWebInbox } from "./inbound/monitor.js"; export type { WhatsAppInboundAdmission } from "./inbound/admission.js"; export type { - LegacyFlatWebInboundMessage, WebInboundCallbackMessage, WebInboundMessage, - WebInboundMessageInput, WebListenerCloseReason, } from "./inbound/types.js"; diff --git a/extensions/whatsapp/src/inbound/admission-types.ts b/extensions/whatsapp/src/inbound/admission-types.ts deleted file mode 100644 index 1afbb3df80da..000000000000 --- a/extensions/whatsapp/src/inbound/admission-types.ts +++ /dev/null @@ -1,18 +0,0 @@ -export type DeprecatedWebInboundAdmissionTopLevelFields = { - /** @deprecated Use `admission.conversation.id`. */ - from: string; // conversation id: E.164 for direct chats, group JID for groups - /** @deprecated Use `admission.conversation.id`. */ - conversationId: string; // alias for clarity (same as from) - /** @deprecated Use `admission.accountId`. */ - accountId: string; - /** - * @deprecated Use `admission.ingress.decision === "allow"`. - * - * Set by the real inbound monitor after access-control / pairing checks pass. - * On messages with `admission`, this is a derived compatibility view; writes - * are retained only for legacy inputs without an admission envelope. - */ - accessControlPassed?: boolean; - /** @deprecated Use `admission.conversation.kind`. */ - chatType: "direct" | "group"; -}; diff --git a/extensions/whatsapp/src/inbound/admission.ts b/extensions/whatsapp/src/inbound/admission.ts index dcf229f7a03f..31c14b3383b6 100644 --- a/extensions/whatsapp/src/inbound/admission.ts +++ b/extensions/whatsapp/src/inbound/admission.ts @@ -3,8 +3,6 @@ import type { ResolvedChannelMessageIngress, } from "openclaw/plugin-sdk/channel-ingress-runtime"; import type { ReplyToMode } from "openclaw/plugin-sdk/config-contracts"; -import type { WhatsAppIdentity } from "../identity.js"; -import type { DeprecatedWebInboundAdmissionTopLevelFields } from "./admission-types.js"; import { resolveWhatsAppGroupConversationId } from "./group-conversation.js"; type WhatsAppInboundIngressDecision = Pick< @@ -47,30 +45,10 @@ type WhatsAppInboundAdmissionPolicy = { isSamePhone: (value?: string | null) => boolean; }; -type DeprecatedFlatWhatsAppInboundAdmissionInput = - Partial & { - platform?: { - sender?: WhatsAppIdentity; - senderE164?: string | null; - senderJid?: string | null; - senderName?: string | null; - }; - senderE164?: string | null; - senderJid?: string | null; - senderName?: string | null; - }; - type WhatsAppInboundAdmissionCarrier = { admission?: WhatsAppInboundAdmission; }; -type AdmittedWhatsAppInboundMessage = Omit< - T, - keyof DeprecatedWebInboundAdmissionTopLevelFields | "admission" -> & { - admission: WhatsAppInboundAdmission; -}; - /** * Public-safe accepted inbound facts resolved by access control. * @@ -191,76 +169,6 @@ export async function resolveWhatsAppAdmissionChannelIngress( return await ingressResolverByAdmission.get(admission)?.(contextBinding); } -export function buildDeprecatedFlatWhatsAppInboundAdmission( - msg: DeprecatedFlatWhatsAppInboundAdmissionInput, -): WhatsAppInboundAdmission { - const conversationId = msg.conversationId || msg.from; - if (!conversationId || !msg.accountId || !msg.chatType) { - throw new Error( - "WhatsApp legacy flat inbound messages must include deprecated top-level admission fields.", - ); - } - const accountId = msg.accountId; - const admitted = msg.accessControlPassed !== false; - const platformSender = msg.platform?.sender; - const senderE164 = platformSender?.e164 ?? msg.platform?.senderE164 ?? msg.senderE164; - const senderJid = platformSender?.jid ?? msg.platform?.senderJid ?? msg.senderJid; - const senderName = platformSender?.name ?? msg.platform?.senderName ?? msg.senderName; - const senderId = - msg.chatType === "group" - ? (senderE164 ?? senderJid ?? senderName ?? conversationId) - : (senderE164 ?? conversationId); - const reasonCode = admitted - ? msg.chatType === "group" - ? "group_policy_allowed" - : "dm_policy_allowlisted" - : "no_policy_match"; - - // Compatibility only: deprecated listenerFactory flat inputs predate the - // admission envelope, so convert them through the canonical admission builder. - // Canonical nested inputs without admission remain malformed for runtime use. - return buildWhatsAppInboundAdmission({ - policy: { - account: { - accountId, - enabled: true, - sendReadReceipts: true, - }, - isSelfChat: false, - isSamePhone: () => false, - }, - access: { - ingress: { - admission: admitted ? "dispatch" : "drop", - decision: admitted ? "allow" : "block", - decisiveGateId: "legacy-flat-compat", - reasonCode, - }, - senderAccess: { - allowed: admitted, - decision: admitted ? "allow" : "block", - reasonCode, - providerMissingFallbackApplied: false, - }, - commandAccess: { - requested: false, - authorized: false, - shouldBlockControlCommand: false, - reasonCode: "command_authorized", - }, - activationAccess: { - ran: false, - allowed: admitted, - shouldSkip: !admitted, - reasonCode: admitted ? "activation_allowed" : "activation_skipped", - }, - }, - isGroup: msg.chatType === "group", - conversationId, - senderId, - }); -} - export function requireWhatsAppInboundAdmission( params: WhatsAppInboundAdmissionCarrier, ): WhatsAppInboundAdmission { @@ -269,10 +177,3 @@ export function requireWhatsAppInboundAdmission( } return params.admission; } - -export function requireAdmittedWhatsAppInboundMessage( - msg: T, -): AdmittedWhatsAppInboundMessage { - requireWhatsAppInboundAdmission(msg); - return msg as AdmittedWhatsAppInboundMessage; -} diff --git a/extensions/whatsapp/src/inbound/message-aliases.test.ts b/extensions/whatsapp/src/inbound/message-aliases.test.ts deleted file mode 100644 index b06c740f6bf8..000000000000 --- a/extensions/whatsapp/src/inbound/message-aliases.test.ts +++ /dev/null @@ -1,440 +0,0 @@ -// WhatsApp tests cover inbound message alias compatibility. -import { describe, expect, expectTypeOf, it, vi } from "vitest"; -import { - normalizeAdmittedWebInboundMessage, - withDeprecatedWebInboundMessageFlatAliases, -} from "./message-aliases.js"; -import type { monitorWebInbox } from "./monitor.js"; -import { createAcceptedWhatsAppSendResult } from "./send-result.test-helper.js"; -import { - createTestLegacyFlatWebInboundMessage, - createTestWhatsAppInboundAdmission, -} from "./test-message.test-helper.js"; -import type { - LegacyFlatWebInboundMessage, - WebInboundCallbackMessage, - WebInboundMessageInput, -} from "./types.js"; - -type MonitorWebInboxMessage = Parameters[0]["onMessage"]>[0]; -type MonitorWebChannel = typeof import("../auto-reply/monitor.js").monitorWebChannel; -type ListenerFactory = NonNullable[1]>; -type ListenerFactoryMessage = Parameters[0]["onMessage"]>[0]; - -function createCanonicalMessage(overrides: Partial = {}) { - return withDeprecatedWebInboundMessageFlatAliases({ - event: { - id: "event-1", - timestamp: 1_700_000_000, - isBatched: false, - }, - payload: { - body: "hello", - media: { - path: "/tmp/image.jpg", - type: "image/jpeg", - fileName: "image.jpg", - url: "https://example.com/image.jpg", - }, - channelStructuredContext: [ - { - label: "WhatsApp contact", - source: "whatsapp", - type: "contact", - payload: { name: "Alice" }, - }, - ], - }, - platform: { - chatJid: "123@g.us", - recipientJid: "+15550000001", - senderJid: "15550000002@s.whatsapp.net", - senderE164: "+15550000002", - senderName: "Alice", - pushName: "Alice P", - selfE164: "+15550000001", - fromMe: false, - sendComposing: vi.fn(async () => undefined), - reply: vi.fn(async () => createAcceptedWhatsAppSendResult("text", "reply-1")), - sendMedia: vi.fn(async () => createAcceptedWhatsAppSendResult("media", "media-1")), - }, - from: "123@g.us", - conversationId: "123@g.us", - accountId: "default", - chatType: "group", - quote: { - id: "quote-1", - body: "quoted", - sender: { - displayName: "Bob", - jid: "15550000003@s.whatsapp.net", - e164: "+15550000003", - }, - }, - group: { - subject: "Test Group", - participants: ["15550000002@s.whatsapp.net"], - mentions: { - jids: ["15550000001@s.whatsapp.net"], - }, - }, - ...overrides, - }); -} - -const callbackMessageWithoutAdmissionFacts = { - event: { - id: "event-missing-admission", - }, - payload: { - body: "missing admission", - }, - platform: { - chatJid: "123@g.us", - recipientJid: "+15550000001", - sendComposing: async () => undefined, - reply: async () => createAcceptedWhatsAppSendResult("text", "reply-missing-admission"), - sendMedia: async () => createAcceptedWhatsAppSendResult("media", "media-missing-admission"), - }, -}; - -describe("WhatsApp inbound flat aliases", () => { - it("rejects callback messages without admission facts", () => { - expectTypeOf( - callbackMessageWithoutAdmissionFacts, - ).not.toMatchTypeOf(); - }); - - it("keeps deprecated admission fields typed on monitor callbacks", () => { - expectTypeOf().toMatchTypeOf<{ - admission: NonNullable; - from: string; - conversationId: string; - accountId: string; - accessControlPassed?: boolean; - chatType: "direct" | "group"; - }>(); - }); - - it("keeps deprecated flat inputs at both WhatsApp listener boundaries", () => { - expectTypeOf().toMatchTypeOf(); - - const admitted = normalizeAdmittedWebInboundMessage(createTestLegacyFlatWebInboundMessage()); - expect(admitted.admission.ingress).toMatchObject({ - admission: "dispatch", - decisiveGateId: "legacy-flat-compat", - }); - }); - - it("keeps deprecated flat aliases live against canonical contexts", async () => { - const msg = createCanonicalMessage(); - const nextReply = vi.fn(async () => createAcceptedWhatsAppSendResult("text", "reply-2")); - - expect(msg.body).toBe("hello"); - msg.payload.body = "nested body"; - expect(msg.body).toBe("nested body"); - msg.body = "flat body"; - expect(msg.payload.body).toBe("flat body"); - - msg.platform.chatJid = "456@g.us"; - expect(msg.chatId).toBe("456@g.us"); - msg.chatId = "789@g.us"; - expect(msg.platform.chatJid).toBe("789@g.us"); - - msg.payload.media = { path: "/tmp/next.jpg", type: "image/png" }; - expect(msg.mediaPath).toBe("/tmp/next.jpg"); - expect(msg.mediaType).toBe("image/png"); - msg.mediaFileName = "next.jpg"; - msg.mediaUrl = "https://example.com/next.jpg"; - expect(msg.payload.media).toMatchObject({ - fileName: "next.jpg", - url: "https://example.com/next.jpg", - }); - - msg.group.mentions!.jids = ["first@s.whatsapp.net"]; - expect(msg.mentions).toEqual(["first@s.whatsapp.net"]); - msg.mentionedJids = ["second@s.whatsapp.net"]; - expect(msg.group?.mentions?.jids).toEqual(["second@s.whatsapp.net"]); - expect(msg.mentions).toEqual(["second@s.whatsapp.net"]); - - msg.reply = nextReply; - expect(msg.platform.reply).toBe(nextReply); - await msg.platform.reply("ok"); - expect(nextReply).toHaveBeenCalledWith("ok"); - - expect(Object.keys(msg)).toContain("body"); - expect(Object.keys(msg)).toContain("chatId"); - }); - - it("keeps the deprecated structured-context alias live in both directions", () => { - const msg = createCanonicalMessage(); - const legacyValue = [ - { - label: "Legacy metadata", - payload: { value: "legacy" }, - }, - ]; - - expect(msg.untrustedStructuredContext).toEqual(msg.payload.channelStructuredContext); - msg.untrustedStructuredContext = legacyValue; - expect(msg.payload.channelStructuredContext).toEqual(legacyValue); - - const currentValue = [ - { - label: "Current metadata", - payload: { value: "current" }, - }, - ]; - msg.payload.channelStructuredContext = currentValue; - expect(msg.untrustedStructuredContext).toEqual(currentValue); - }); - - it("normalizes the shipped nested structured-context key with new-name precedence", () => { - const deprecatedValue = [ - { - label: "Deprecated nested metadata", - payload: { value: "deprecated" }, - }, - ]; - const currentValue = [ - { - label: "Current nested metadata", - payload: { value: "current" }, - }, - ]; - const deprecatedOnly = createCanonicalMessage({ - payload: { - body: "hello", - untrustedStructuredContext: deprecatedValue, - }, - }); - const msg = createCanonicalMessage({ - payload: { - body: "hello", - channelStructuredContext: currentValue, - untrustedStructuredContext: deprecatedValue, - }, - }); - - expect(deprecatedOnly.payload.channelStructuredContext).toEqual(deprecatedValue); - expect(deprecatedOnly.payload.untrustedStructuredContext).toEqual(deprecatedValue); - expect(msg.payload.channelStructuredContext).toEqual(currentValue); - expect(msg.payload.untrustedStructuredContext).toEqual(currentValue); - }); - - it("keeps deprecated admission top-level fields aligned with admission", () => { - const msg = createCanonicalMessage({ - admission: createTestWhatsAppInboundAdmission({ - conversation: { - kind: "group", - id: "123@g.us", - }, - sender: { - id: "+15550000002", - }, - senderAccess: { - reasonCode: "group_policy_allowed", - }, - }), - }); - - expect(msg.from).toBe("123@g.us"); - msg.admission!.conversation.id = "456@g.us"; - expect(msg.from).toBe("456@g.us"); - expect(msg.conversationId).toBe("456@g.us"); - - msg.conversationId = "789@g.us"; - expect(msg.admission?.conversation.id).toBe("789@g.us"); - expect(msg.admission?.conversation.groupSessionId).toBe("789@g.us"); - expect(msg.from).toBe("789@g.us"); - - msg.admission!.accountId = "work"; - expect(msg.accountId).toBe("work"); - msg.accountId = "ops"; - expect(msg.admission?.accountId).toBe("ops"); - expect(msg.admission?.account.accountId).toBe("ops"); - - msg.admission!.conversation.kind = "direct"; - expect(msg.chatType).toBe("direct"); - msg.chatType = "group"; - expect(msg.admission?.conversation.kind).toBe("group"); - - expect(msg.accessControlPassed).toBe(true); - msg.admission!.ingress.decision = "block"; - expect(msg.accessControlPassed).toBe(false); - msg.accessControlPassed = true; - expect(msg.admission?.ingress.decision).toBe("block"); - expect(msg.accessControlPassed).toBe(false); - }); - - it("keeps deprecated admission top-level fields usable without admission", () => { - const msg = createCanonicalMessage(); - - expect(msg.from).toBe("123@g.us"); - msg.from = "+15550000002"; - expect(msg.from).toBe("+15550000002"); - expect(msg.conversationId).toBe("+15550000002"); - - expect(msg.accessControlPassed).toBeUndefined(); - msg.accessControlPassed = true; - expect(msg.accessControlPassed).toBe(true); - }); - - it("rejects canonical messages without admission at the listener boundary", () => { - expect(() => - normalizeAdmittedWebInboundMessage({ - event: { - id: "group-no-admission", - timestamp: 1_700_000_456, - }, - payload: { - body: "hello group", - }, - platform: { - chatJid: "123@g.us", - recipientJid: "+15550000001", - senderJid: "15550000002@s.whatsapp.net", - senderE164: "+15550000002", - senderName: "Alice", - sendComposing: vi.fn(async () => undefined), - reply: vi.fn(async () => createAcceptedWhatsAppSendResult("text", "reply-group")), - sendMedia: vi.fn(async () => createAcceptedWhatsAppSendResult("media", "media-group")), - }, - from: "123@g.us", - conversationId: "123@g.us", - accountId: "work", - accessControlPassed: true, - chatType: "group", - }), - ).toThrow("WhatsApp inbound message is missing admission facts"); - }); - - it("normalizes legacy flat messages into canonical contexts with live aliases", () => { - const legacyReply = vi.fn(async () => createAcceptedWhatsAppSendResult("text", "reply-legacy")); - const legacy: LegacyFlatWebInboundMessage = { - id: "legacy-1", - timestamp: 1_700_000_123, - from: "+15550000002", - conversationId: "+15550000002", - accountId: "default", - chatType: "direct", - to: "+15550000001", - body: "legacy body", - chatId: "15550000002@s.whatsapp.net", - replyToId: "quote-legacy", - replyToBody: "legacy quoted", - replyToSender: "Legacy Sender", - replyToSenderJid: "15550000003@s.whatsapp.net", - groupSubject: "Legacy Group", - mentionedJids: ["15550000001@s.whatsapp.net"], - sendComposing: vi.fn(async () => undefined), - reply: legacyReply, - sendMedia: vi.fn(async () => createAcceptedWhatsAppSendResult("media", "media-legacy")), - mediaPath: "/tmp/legacy.jpg", - mediaType: "image/jpeg", - untrustedStructuredContext: [ - { - label: "Legacy metadata", - payload: { value: "legacy" }, - }, - ], - isBatched: true, - }; - - const normalized = normalizeAdmittedWebInboundMessage(legacy); - - expect(normalized.event).toMatchObject({ - id: "legacy-1", - timestamp: 1_700_000_123, - isBatched: true, - }); - expect(normalized.payload.body).toBe("legacy body"); - expect(normalized.payload.media).toMatchObject({ - path: "/tmp/legacy.jpg", - type: "image/jpeg", - }); - expect(normalized.payload.channelStructuredContext).toEqual([ - { - label: "Legacy metadata", - payload: { value: "legacy" }, - }, - ]); - expect(normalized.untrustedStructuredContext).toEqual( - normalized.payload.channelStructuredContext, - ); - expect(normalized.platform).toMatchObject({ - chatJid: "15550000002@s.whatsapp.net", - recipientJid: "+15550000001", - }); - expect(normalized.admission?.ingress).toMatchObject({ - admission: "dispatch", - decision: "allow", - decisiveGateId: "legacy-flat-compat", - reasonCode: "dm_policy_allowlisted", - }); - expect(normalized.accessControlPassed).toBeUndefined(); - expect(normalized.quote).toMatchObject({ - id: "quote-legacy", - body: "legacy quoted", - sender: { - displayName: "Legacy Sender", - jid: "15550000003@s.whatsapp.net", - }, - }); - expect(normalized.group).toMatchObject({ - subject: "Legacy Group", - mentions: { - jids: ["15550000001@s.whatsapp.net"], - }, - }); - - normalized.payload.body = "canonical update"; - expect(normalized.body).toBe("canonical update"); - normalized.replyToSender = "Updated Sender"; - expect(normalized.quote?.sender?.displayName).toBe("Updated Sender"); - }); - - it("normalizes blocked legacy flat admission fields through the compatibility seam", () => { - const normalized = normalizeAdmittedWebInboundMessage({ - id: "legacy-blocked", - from: "+15550000002", - conversationId: "+15550000002", - accountId: "default", - accessControlPassed: false, - chatType: "direct", - to: "+15550000001", - body: "blocked legacy body", - chatId: "15550000002@s.whatsapp.net", - sendComposing: vi.fn(async () => undefined), - reply: vi.fn(async () => createAcceptedWhatsAppSendResult("text", "reply-blocked")), - sendMedia: vi.fn(async () => createAcceptedWhatsAppSendResult("media", "media-blocked")), - }); - - expect(normalized.admission).toMatchObject({ - ingress: { - admission: "drop", - decision: "block", - decisiveGateId: "legacy-flat-compat", - reasonCode: "no_policy_match", - }, - senderAccess: { - allowed: false, - decision: "block", - reasonCode: "no_policy_match", - }, - }); - expect(normalized.accessControlPassed).toBe(false); - }); - - it("preserves explicit legacy access proof through normalization", () => { - const normalized = normalizeAdmittedWebInboundMessage({ - ...createTestLegacyFlatWebInboundMessage(), - accessControlPassed: true, - }); - - expect(normalized.admission?.ingress.decisiveGateId).toBe("legacy-flat-compat"); - expect(normalized.accessControlPassed).toBe(true); - normalized.accessControlPassed = false; - expect(normalized.accessControlPassed).toBe(true); - }); -}); diff --git a/extensions/whatsapp/src/inbound/message-aliases.ts b/extensions/whatsapp/src/inbound/message-aliases.ts deleted file mode 100644 index 688359aa0a85..000000000000 --- a/extensions/whatsapp/src/inbound/message-aliases.ts +++ /dev/null @@ -1,468 +0,0 @@ -import { - buildDeprecatedFlatWhatsAppInboundAdmission, - requireAdmittedWhatsAppInboundMessage, -} from "./admission.js"; -import { resolveWhatsAppGroupConversationId } from "./group-conversation.js"; -import type { - AdmittedWebInboundCallbackMessage, - DeprecatedWebInboundAdmissionTopLevelFields, - DeprecatedWebInboundMessageFlatAliases, - LegacyFlatWebInboundMessage, - WebInboundCallbackMessage, - WebInboundMessage, - WebInboundMessageInput, - WhatsAppInboundGroupContext, - WhatsAppInboundQuote, -} from "./types.js"; - -type QuoteSender = NonNullable; -type AliasDescriptor = { - get: () => unknown; - set: (value: unknown) => void; -}; - -function normalizeQuoteSender(sender: QuoteSender | undefined): QuoteSender | undefined { - if (!sender?.displayName && !sender?.jid && !sender?.e164) { - return undefined; - } - return sender; -} - -function buildQuoteFromFlatAliases( - msg: LegacyFlatWebInboundMessage, -): WhatsAppInboundQuote | undefined { - if (msg.replyTo) { - return { - context: msg.replyTo, - id: msg.replyTo.id, - body: msg.replyTo.body, - sender: normalizeQuoteSender({ - displayName: msg.replyTo.sender?.label ?? msg.replyToSender, - jid: msg.replyTo.sender?.jid ?? msg.replyToSenderJid, - e164: msg.replyTo.sender?.e164 ?? msg.replyToSenderE164, - }), - }; - } - if ( - !msg.replyToId && - !msg.replyToBody && - !msg.replyToSender && - !msg.replyToSenderJid && - !msg.replyToSenderE164 - ) { - return undefined; - } - return { - id: msg.replyToId, - body: msg.replyToBody, - sender: normalizeQuoteSender({ - displayName: msg.replyToSender, - jid: msg.replyToSenderJid, - e164: msg.replyToSenderE164, - }), - }; -} - -function buildGroupFromFlatAliases( - msg: LegacyFlatWebInboundMessage, -): WhatsAppInboundGroupContext | undefined { - const mentionJids = msg.mentions ?? msg.mentionedJids; - if (!msg.groupSubject && !msg.groupParticipants?.length && !mentionJids?.length) { - return undefined; - } - return { - subject: msg.groupSubject, - participants: msg.groupParticipants, - mentions: mentionJids?.length ? { jids: mentionJids } : undefined, - }; -} - -function ensureQuote(msg: WebInboundCallbackMessage): WhatsAppInboundQuote { - return (msg.quote ??= {}); -} - -function ensureQuoteSender(msg: WebInboundCallbackMessage): QuoteSender { - const quote = ensureQuote(msg); - return (quote.sender ??= {}); -} - -function ensureGroup(msg: WebInboundCallbackMessage): WhatsAppInboundGroupContext { - return (msg.group ??= {}); -} - -function ensureGroupMentions(msg: WebInboundCallbackMessage): { jids?: string[]; text?: string[] } { - const group = ensureGroup(msg); - return (group.mentions ??= {}); -} - -function ensureMedia( - msg: WebInboundCallbackMessage, -): NonNullable { - return (msg.payload.media ??= {}); -} - -function setMediaField>( - msg: WebInboundCallbackMessage, - key: K, - value: NonNullable[K] | undefined, -) { - if (value === undefined && !msg.payload.media) { - return; - } - ensureMedia(msg)[key] = value; -} - -function defineDeprecatedAccessors( - msg: T, - descriptors: Record, -): T { - Object.defineProperties( - msg, - Object.fromEntries( - Object.entries(descriptors).map(([key, descriptor]) => [ - key, - { - configurable: true, - enumerable: true, - get: descriptor.get, - set: descriptor.set, - }, - ]), - ), - ); - return msg; -} - -function defineDeprecatedAliasAccessors( - msg: T, - descriptors: Record, -): T & WebInboundMessage { - defineDeprecatedAccessors(msg, descriptors); - return msg as T & WebInboundMessage; -} - -function defineDeprecatedStructuredContextPayloadAlias(msg: WebInboundCallbackMessage): void { - const channelStructuredContext = - msg.payload.channelStructuredContext ?? msg.payload.untrustedStructuredContext; - if (channelStructuredContext !== undefined) { - msg.payload.channelStructuredContext = channelStructuredContext; - } - Object.defineProperty(msg.payload, "untrustedStructuredContext", { - configurable: true, - enumerable: true, - get: () => msg.payload.channelStructuredContext, - set: (value) => { - msg.payload.channelStructuredContext = value; - }, - }); -} - -function defineDeprecatedAdmissionTopLevelAccessors( - msg: T, -): T { - let fallbackConversationId = msg.conversationId || msg.from; - let fallbackAccountId = msg.accountId; - let fallbackAccessControlPassed = msg.accessControlPassed; - let fallbackChatType = msg.chatType; - - const conversationId = () => msg.admission?.conversation.id ?? fallbackConversationId; - const setConversationId = (value: unknown) => { - const next = value as string; - fallbackConversationId = next; - if (msg.admission) { - msg.admission.conversation.id = next; - msg.admission.conversation.groupSessionId = resolveWhatsAppGroupConversationId(next); - } - }; - - const descriptors: Record = { - from: { - get: conversationId, - set: setConversationId, - }, - conversationId: { - get: conversationId, - set: setConversationId, - }, - accountId: { - get: () => msg.admission?.accountId ?? fallbackAccountId, - set: (value) => { - const next = value as string; - fallbackAccountId = next; - if (msg.admission) { - msg.admission.accountId = next; - msg.admission.account.accountId = next; - } - }, - }, - accessControlPassed: { - get: () => { - // Legacy flat inputs used absence to mean access was not explicitly proven. - // Preserve that tri-state after normalization so preflight work cannot run early. - if (msg.admission?.ingress.decisiveGateId === "legacy-flat-compat") { - return fallbackAccessControlPassed; - } - return msg.admission - ? msg.admission.ingress.decision === "allow" - : fallbackAccessControlPassed; - }, - set: (value) => { - // The legacy boolean is derived from the ingress graph; writes only preserve - // no-admission legacy inputs instead of fabricating a partial graph update. - if (!msg.admission) { - fallbackAccessControlPassed = value as boolean | undefined; - } - }, - }, - chatType: { - get: () => msg.admission?.conversation.kind ?? fallbackChatType, - set: (value) => { - const next = value as "direct" | "group"; - fallbackChatType = next; - if (msg.admission) { - msg.admission.conversation.kind = next; - } - }, - }, - }; - - return defineDeprecatedAccessors(msg, descriptors); -} - -export function withDeprecatedWebInboundMessageFlatAliases( - msg: T, -): T & WebInboundMessage { - // Keep the shipped callback shape alive while nested/admission contexts remain canonical. - defineDeprecatedStructuredContextPayloadAlias(msg); - const withAdmissionAliases = defineDeprecatedAdmissionTopLevelAccessors(msg); - return defineDeprecatedAliasAccessors(withAdmissionAliases, { - id: { get: () => msg.event.id, set: (value) => (msg.event.id = value as string | undefined) }, - to: { - get: () => msg.platform.recipientJid, - set: (value) => (msg.platform.recipientJid = value as string), - }, - body: { get: () => msg.payload.body, set: (value) => (msg.payload.body = value as string) }, - pushName: { - get: () => msg.platform.pushName, - set: (value) => (msg.platform.pushName = value as string | undefined), - }, - timestamp: { - get: () => msg.event.timestamp, - set: (value) => (msg.event.timestamp = value as number | undefined), - }, - chatId: { - get: () => msg.platform.chatJid, - set: (value) => (msg.platform.chatJid = value as string), - }, - sender: { - get: () => msg.platform.sender, - set: (value) => (msg.platform.sender = value as typeof msg.platform.sender), - }, - senderJid: { - get: () => msg.platform.senderJid, - set: (value) => (msg.platform.senderJid = value as string | undefined), - }, - senderE164: { - get: () => msg.platform.senderE164, - set: (value) => (msg.platform.senderE164 = value as string | undefined), - }, - senderName: { - get: () => msg.platform.senderName, - set: (value) => (msg.platform.senderName = value as string | undefined), - }, - replyTo: { - get: () => msg.quote?.context, - set: (value) => (ensureQuote(msg).context = value as WhatsAppInboundQuote["context"]), - }, - replyToId: { - get: () => msg.quote?.id ?? msg.quote?.context?.id, - set: (value) => (ensureQuote(msg).id = value as string | undefined), - }, - replyToBody: { - get: () => msg.quote?.body ?? msg.quote?.context?.body, - set: (value) => (ensureQuote(msg).body = value as string | undefined), - }, - replyToSender: { - get: () => msg.quote?.context?.sender?.label ?? msg.quote?.sender?.displayName, - set: (value) => { - const sender = ensureQuoteSender(msg); - sender.displayName = value as string | undefined; - if (msg.quote?.context?.sender) { - msg.quote.context.sender.label = value as string | undefined; - } - }, - }, - replyToSenderJid: { - get: () => msg.quote?.context?.sender?.jid ?? msg.quote?.sender?.jid, - set: (value) => { - const jid = value as string | undefined; - ensureQuoteSender(msg).jid = jid; - if (msg.quote?.context?.sender) { - msg.quote.context.sender.jid = jid; - } - }, - }, - replyToSenderE164: { - get: () => msg.quote?.context?.sender?.e164 ?? msg.quote?.sender?.e164, - set: (value) => { - const e164 = value as string | undefined; - ensureQuoteSender(msg).e164 = e164; - if (msg.quote?.context?.sender) { - msg.quote.context.sender.e164 = e164; - } - }, - }, - groupSubject: { - get: () => msg.group?.subject, - set: (value) => (ensureGroup(msg).subject = value as string | undefined), - }, - groupParticipants: { - get: () => msg.group?.participants, - set: (value) => (ensureGroup(msg).participants = value as string[] | undefined), - }, - mentions: { - get: () => msg.group?.mentions?.jids, - set: (value) => (ensureGroupMentions(msg).jids = value as string[] | undefined), - }, - mentionedJids: { - get: () => msg.group?.mentions?.jids, - set: (value) => (ensureGroupMentions(msg).jids = value as string[] | undefined), - }, - self: { - get: () => msg.platform.self, - set: (value) => (msg.platform.self = value as typeof msg.platform.self), - }, - selfJid: { - get: () => msg.platform.selfJid, - set: (value) => (msg.platform.selfJid = value as string | null | undefined), - }, - selfLid: { - get: () => msg.platform.selfLid, - set: (value) => (msg.platform.selfLid = value as string | null | undefined), - }, - selfE164: { - get: () => msg.platform.selfE164, - set: (value) => (msg.platform.selfE164 = value as string | null | undefined), - }, - fromMe: { - get: () => msg.platform.fromMe, - set: (value) => (msg.platform.fromMe = value as boolean | undefined), - }, - location: { - get: () => msg.payload.location, - set: (value) => (msg.payload.location = value as typeof msg.payload.location), - }, - sendComposing: { - get: () => msg.platform.sendComposing, - set: (value) => (msg.platform.sendComposing = value as typeof msg.platform.sendComposing), - }, - reply: { - get: () => msg.platform.reply, - set: (value) => (msg.platform.reply = value as typeof msg.platform.reply), - }, - sendMedia: { - get: () => msg.platform.sendMedia, - set: (value) => (msg.platform.sendMedia = value as typeof msg.platform.sendMedia), - }, - mediaPath: { - get: () => msg.payload.media?.path, - set: (value) => setMediaField(msg, "path", value as string | undefined), - }, - mediaType: { - get: () => msg.payload.media?.type, - set: (value) => setMediaField(msg, "type", value as string | undefined), - }, - mediaFileName: { - get: () => msg.payload.media?.fileName, - set: (value) => setMediaField(msg, "fileName", value as string | undefined), - }, - mediaUrl: { - get: () => msg.payload.media?.url, - set: (value) => setMediaField(msg, "url", value as string | undefined), - }, - channelStructuredContext: { - get: () => msg.payload.channelStructuredContext, - set: (value) => - (msg.payload.channelStructuredContext = - value as typeof msg.payload.channelStructuredContext), - }, - untrustedStructuredContext: { - get: () => msg.payload.channelStructuredContext, - set: (value) => - (msg.payload.channelStructuredContext = - value as typeof msg.payload.channelStructuredContext), - }, - isBatched: { - get: () => msg.event.isBatched, - set: (value) => (msg.event.isBatched = value as boolean | undefined), - }, - }); -} - -function normalizeLegacyFlatWebInboundMessage(msg: LegacyFlatWebInboundMessage): WebInboundMessage { - const media = - msg.mediaPath || msg.mediaType || msg.mediaFileName || msg.mediaUrl - ? { - path: msg.mediaPath, - type: msg.mediaType, - fileName: msg.mediaFileName, - url: msg.mediaUrl, - } - : undefined; - return withDeprecatedWebInboundMessageFlatAliases({ - ...msg, - admission: msg.admission ?? buildDeprecatedFlatWhatsAppInboundAdmission(msg), - event: { - id: msg.id, - timestamp: msg.timestamp, - isBatched: msg.isBatched, - }, - payload: { - body: msg.body, - media, - location: msg.location, - channelStructuredContext: msg.channelStructuredContext ?? msg.untrustedStructuredContext, - }, - platform: { - chatJid: msg.chatId, - recipientJid: msg.to, - sender: msg.sender, - senderJid: msg.senderJid, - senderE164: msg.senderE164, - senderName: msg.senderName, - pushName: msg.pushName, - self: msg.self, - selfJid: msg.selfJid, - selfLid: msg.selfLid, - selfE164: msg.selfE164, - fromMe: msg.fromMe, - sendComposing: msg.sendComposing, - reply: msg.reply, - sendMedia: msg.sendMedia, - }, - quote: buildQuoteFromFlatAliases(msg), - group: buildGroupFromFlatAliases(msg), - }); -} - -function normalizeWebInboundMessage(msg: WebInboundMessageInput): WebInboundMessage { - if (msg.event && msg.payload && msg.platform) { - return withDeprecatedWebInboundMessageFlatAliases(msg); - } - - if (msg.event || msg.payload || msg.platform || msg.quote || msg.group) { - throw new Error( - "WhatsApp inbound messages must be either legacy flat or canonical nested; partial nested contexts are not supported.", - ); - } - - return normalizeLegacyFlatWebInboundMessage(msg); -} - -export function normalizeAdmittedWebInboundMessage( - msg: WebInboundMessageInput, -): AdmittedWebInboundCallbackMessage { - return requireAdmittedWhatsAppInboundMessage( - normalizeWebInboundMessage(msg), - ) as AdmittedWebInboundCallbackMessage; -} diff --git a/extensions/whatsapp/src/inbound/message-debounce.ts b/extensions/whatsapp/src/inbound/message-debounce.ts index 97dbf449d473..1ca096d4846d 100644 --- a/extensions/whatsapp/src/inbound/message-debounce.ts +++ b/extensions/whatsapp/src/inbound/message-debounce.ts @@ -5,8 +5,7 @@ import { getPrimaryIdentityId } from "../identity.js"; import { requireWhatsAppInboundAdmission } from "./admission.js"; import type { WhatsAppIngressLifecycle, WhatsAppReadReceiptTarget } from "./durable-receive.js"; import { attachWhatsAppIngressLifecycle } from "./ingress-lifecycle.js"; -import { withDeprecatedWebInboundMessageFlatAliases } from "./message-aliases.js"; -import type { AdmittedWebInboundCallbackMessage, WebInboundMessageInput } from "./types.js"; +import type { AdmittedWebInboundCallbackMessage } from "./types.js"; export type WhatsAppQueuedInboundMessage = AdmittedWebInboundCallbackMessage & { debounceKey?: string; @@ -18,8 +17,8 @@ export type WhatsAppQueuedInboundMessage = AdmittedWebInboundCallbackMessage & { export function createWhatsAppInboundMessageDebouncer(options: { debounceMs?: number; - onMessage: (msg: WebInboundMessageInput) => Promise; - shouldDebounce?: (msg: WebInboundMessageInput) => boolean; + onMessage: (msg: AdmittedWebInboundCallbackMessage) => Promise; + shouldDebounce?: (msg: AdmittedWebInboundCallbackMessage) => boolean; markRead: (target: WhatsAppReadReceiptTarget | undefined) => Promise; onPendingWorkChanged: () => void; onError: (error: unknown) => void; @@ -121,7 +120,7 @@ export function createWhatsAppInboundMessageDebouncer(options: { ? { ...last.group, mentions: combinedMentions } : undefined; const combinedMessage: WhatsAppQueuedInboundMessage = attachWhatsAppIngressLifecycle( - withDeprecatedWebInboundMessageFlatAliases({ + { ...last, turnAdoptionLifecycle: admissionLifecycle, payload: { @@ -131,7 +130,7 @@ export function createWhatsAppInboundMessageDebouncer(options: { }, group: combinedGroup, event: { ...last.event, isBatched: true }, - }), + }, admissionLifecycle, ); await options.onMessage(combinedMessage); diff --git a/extensions/whatsapp/src/inbound/message-delivery.ts b/extensions/whatsapp/src/inbound/message-delivery.ts index dc9172a950af..77820b19783f 100644 --- a/extensions/whatsapp/src/inbound/message-delivery.ts +++ b/extensions/whatsapp/src/inbound/message-delivery.ts @@ -30,7 +30,6 @@ import { } from "./durable-receive.js"; import { extractMentionedJids } from "./extract.js"; import type { WhatsAppGroupMetadataCacheOwner } from "./group-metadata-cache.js"; -import { withDeprecatedWebInboundMessageFlatAliases } from "./message-aliases.js"; import { createWhatsAppInboundMessageDebouncer, type WhatsAppQueuedInboundMessage, @@ -46,7 +45,7 @@ import { import { addWhatsAppOutboundMentionsToContent } from "./outbound-mentions.js"; import { normalizeWhatsAppSendResult } from "./send-result.js"; import type { WhatsAppAttachedSocketSession } from "./socket-session.js"; -import type { WebInboundMessageInput } from "./types.js"; +import type { WebInboundCallbackMessage } from "./types.js"; const INBOUND_CLOSE_DRAIN_TIMEOUT_MS = 5_000; const WHATSAPP_INGRESS_DRAIN_INTERVAL_MS = 1_000; @@ -91,7 +90,7 @@ type WhatsAppMessageDeliveryOptions = { sock: WASocket; socketSession: WhatsAppAttachedSocketSession; groupMetadata: WhatsAppGroupMetadataCacheOwner; - onMessage: (msg: WebInboundMessageInput) => Promise; + onMessage: (msg: WebInboundCallbackMessage) => Promise; mediaMaxMb?: number; /** Send read receipts for incoming messages (default true). */ sendReadReceipts?: boolean; @@ -100,7 +99,7 @@ type WhatsAppMessageDeliveryOptions = { /** Bounded reconnect window for offline append auto-replies. */ appendReplyWindow?: WhatsAppAppendReplyWindow; /** Optional debounce gating predicate. */ - shouldDebounce?: (msg: WebInboundMessageInput) => boolean; + shouldDebounce?: (msg: WebInboundCallbackMessage) => boolean; onPendingWorkChanged?: (pendingWorkCount: number, at?: number) => void; durableInboundQueue?: WhatsAppDurableInboundQueue; }; @@ -337,61 +336,59 @@ export function createWhatsAppMessageDeliveryCoordinator(options: WhatsAppMessag ] : []), ]; - const inboundMessage: WhatsAppQueuedInboundMessage = withDeprecatedWebInboundMessageFlatAliases( - { - admission: inbound.access.admission, - event: { - id: inbound.id, - timestamp, - }, - payload: { - body: enriched.body, - commandBody: enriched.commandBody, - location: enriched.location ?? undefined, - channelStructuredContext: - channelStructuredContext.length > 0 ? channelStructuredContext : undefined, - media, - }, - platform: { - chatJid: inbound.remoteJid, - recipientJid: self.e164 ?? "me", - pushName: senderName, - sender: resolveComparableIdentity({ - jid: inbound.participantJid, - e164: inbound.senderE164 ?? undefined, - name: senderName, - }), - senderJid: inbound.participantJid, - senderE164: inbound.senderE164 ?? undefined, - senderName, - self, - selfJid: self.jid ?? undefined, - selfLid: self.lid ?? undefined, - selfE164: self.e164 ?? undefined, - fromMe: Boolean(msg.key?.fromMe), - sendComposing, - reply, - sendMedia, - }, - quote: enriched.replyContext - ? { - context: enriched.replyContext, - id: enriched.replyContext.id, - body: enriched.replyContext.body, - media: enriched.replyContext.media, - sender: { - displayName: enriched.replyContext.sender?.label ?? undefined, - jid: enriched.replyContext.sender?.jid ?? undefined, - e164: enriched.replyContext.sender?.e164 ?? undefined, - }, - } - : undefined, - group, - turnAdoptionLifecycle: durable.turnAdoptionLifecycle, - readReceipt: durable.readReceipt, - receiveOrder: durable.receiveOrder, + const inboundMessage: WhatsAppQueuedInboundMessage = { + admission: inbound.access.admission, + event: { + id: inbound.id, + timestamp, }, - ); + payload: { + body: enriched.body, + commandBody: enriched.commandBody, + location: enriched.location ?? undefined, + channelStructuredContext: + channelStructuredContext.length > 0 ? channelStructuredContext : undefined, + media, + }, + platform: { + chatJid: inbound.remoteJid, + recipientJid: self.e164 ?? "me", + pushName: senderName, + sender: resolveComparableIdentity({ + jid: inbound.participantJid, + e164: inbound.senderE164 ?? undefined, + name: senderName, + }), + senderJid: inbound.participantJid, + senderE164: inbound.senderE164 ?? undefined, + senderName, + self, + selfJid: self.jid ?? undefined, + selfLid: self.lid ?? undefined, + selfE164: self.e164 ?? undefined, + fromMe: Boolean(msg.key?.fromMe), + sendComposing, + reply, + sendMedia, + }, + quote: enriched.replyContext + ? { + context: enriched.replyContext, + id: enriched.replyContext.id, + body: enriched.replyContext.body, + media: enriched.replyContext.media, + sender: { + displayName: enriched.replyContext.sender?.label ?? undefined, + jid: enriched.replyContext.sender?.jid ?? undefined, + e164: enriched.replyContext.sender?.e164 ?? undefined, + }, + } + : undefined, + group, + turnAdoptionLifecycle: durable.turnAdoptionLifecycle, + readReceipt: durable.readReceipt, + receiveOrder: durable.receiveOrder, + }; if (inboundMessage.event.id) { const admission = requireWhatsAppInboundAdmission(inboundMessage); cacheInboundMessageMeta( diff --git a/extensions/whatsapp/src/inbound/monitor.ts b/extensions/whatsapp/src/inbound/monitor.ts index c19e1d778dc4..1e9397d7fb5b 100644 --- a/extensions/whatsapp/src/inbound/monitor.ts +++ b/extensions/whatsapp/src/inbound/monitor.ts @@ -19,14 +19,13 @@ import { type WhatsAppGroupMetadataCache, } from "./group-metadata-cache.js"; import { closeInboundMonitorSocket } from "./lifecycle.js"; -import { normalizeAdmittedWebInboundMessage } from "./message-aliases.js"; import { createWhatsAppMessageDeliveryCoordinator, type WhatsAppAppendReplyWindow, } from "./message-delivery.js"; import { createWebSendApi } from "./send-api.js"; import { createWhatsAppAttachedSocketSession } from "./socket-session.js"; -import type { AdmittedWebInboundCallbackMessage, WebInboundMessageInput } from "./types.js"; +import type { AdmittedWebInboundCallbackMessage } from "./types.js"; function logWhatsAppVerbose(enabled: boolean | undefined, message: string) { if (enabled) { @@ -80,8 +79,8 @@ type AttachWebInboxToSocketOptions = Omit< "onMessage" | "shouldDebounce" | "socketTiming" > & { socketTiming: Required; - onMessage: (msg: WebInboundMessageInput) => Promise; - shouldDebounce?: (msg: WebInboundMessageInput) => boolean; + onMessage: (msg: AdmittedWebInboundCallbackMessage) => Promise; + shouldDebounce?: (msg: AdmittedWebInboundCallbackMessage) => boolean; }; export async function attachWebInboxToSocket( @@ -198,12 +197,6 @@ export async function monitorWebInbox(options: MonitorWebInboxOptions) { } return attachWebInboxToSocket({ ...options, - onMessage: async (msg) => { - await options.onMessage(normalizeAdmittedWebInboundMessage(msg)); - }, - shouldDebounce: options.shouldDebounce - ? (msg) => options.shouldDebounce?.(normalizeAdmittedWebInboundMessage(msg)) ?? true - : undefined, socketTiming, sock, recentMessageKeys, diff --git a/extensions/whatsapp/src/inbound/test-message.test-helper.ts b/extensions/whatsapp/src/inbound/test-message.test-helper.ts index 4e87ee802c32..f9fa58e7ce61 100644 --- a/extensions/whatsapp/src/inbound/test-message.test-helper.ts +++ b/extensions/whatsapp/src/inbound/test-message.test-helper.ts @@ -1,13 +1,7 @@ import type { WhatsAppInboundAdmission } from "./admission.js"; import { resolveWhatsAppGroupConversationId } from "./group-conversation.js"; -import { withDeprecatedWebInboundMessageFlatAliases } from "./message-aliases.js"; import { createAcceptedWhatsAppSendResult } from "./send-result.test-helper.js"; -import type { - LegacyFlatWebInboundMessage, - AdmittedWebInboundMessage, - WebInboundCallbackMessage, - WebInboundMessage, -} from "./types.js"; +import type { WebInboundCallbackMessage, WebInboundMessage } from "./types.js"; type WhatsAppInboundEvent = WebInboundCallbackMessage["event"]; type WhatsAppInboundPayload = WebInboundCallbackMessage["payload"]; @@ -35,18 +29,7 @@ type TestWhatsAppInboundAdmissionOverrides = Partial< }; type TestInboundMessageOverrides = Partial< - Omit< - WebInboundCallbackMessage, - | "event" - | "payload" - | "platform" - | "admission" - | "from" - | "conversationId" - | "accountId" - | "accessControlPassed" - | "chatType" - > + Omit > & { admission?: TestWhatsAppInboundAdmissionOverrides; event?: Partial; @@ -54,7 +37,7 @@ type TestInboundMessageOverrides = Partial< platform?: Partial; }; -export function createTestWhatsAppInboundAdmission( +function createTestWhatsAppInboundAdmission( overrides: TestWhatsAppInboundAdmissionOverrides = {}, ): WhatsAppInboundAdmission { const conversationId = overrides.conversation?.id ?? "+15551234567"; @@ -115,10 +98,10 @@ export function createTestWhatsAppInboundAdmission( export function createTestWebInboundMessage( overrides: TestInboundMessageOverrides = {}, -): WebInboundMessage & AdmittedWebInboundMessage { +): WebInboundMessage { const { admission: admissionOverrides, event, payload, platform, ...message } = overrides; const admission = createTestWhatsAppInboundAdmission(admissionOverrides); - return withDeprecatedWebInboundMessageFlatAliases({ + return { event: { id: "msg-1", ...event, @@ -137,31 +120,12 @@ export function createTestWebInboundMessage( }, admission, ...message, - }) as WebInboundMessage & AdmittedWebInboundMessage; -} - -export function createTestLegacyFlatWebInboundMessage( - overrides: Partial = {}, -): LegacyFlatWebInboundMessage { - return { - id: "msg-1", - from: "+15551234567", - conversationId: "+15551234567", - accountId: "default", - chatType: "direct", - to: "+15559876543", - body: "hello", - chatId: "+15551234567", - sendComposing: async () => {}, - reply: async () => createAcceptedWhatsAppSendResult("text", "reply-1"), - sendMedia: async () => createAcceptedWhatsAppSendResult("media", "media-1"), - ...overrides, }; } export function createTestWebAudioInboundMessage( overrides: TestInboundMessageOverrides = {}, -): WebInboundMessage & AdmittedWebInboundMessage { +): WebInboundMessage { const { event, payload, platform, ...message } = overrides; const media = Object.hasOwn(payload ?? {}, "media") ? payload?.media diff --git a/extensions/whatsapp/src/inbound/types.ts b/extensions/whatsapp/src/inbound/types.ts index dd05d39acc52..d3dd9d041c4c 100644 --- a/extensions/whatsapp/src/inbound/types.ts +++ b/extensions/whatsapp/src/inbound/types.ts @@ -8,12 +8,9 @@ import type { import type { PollInput } from "openclaw/plugin-sdk/poll-runtime"; import type { WhatsAppIdentity, WhatsAppReplyContext, WhatsAppSelfIdentity } from "../identity.js"; import type { WhatsAppQuotedMessageKey } from "../quoted-message.js"; -import type { DeprecatedWebInboundAdmissionTopLevelFields } from "./admission-types.js"; import type { WhatsAppInboundAdmission } from "./admission.js"; import type { WhatsAppSendResult } from "./send-result.js"; -export type { DeprecatedWebInboundAdmissionTopLevelFields } from "./admission-types.js"; - export type WebListenerCloseReason = { status?: number; isLoggedOut: boolean; @@ -64,7 +61,7 @@ type WhatsAppInboundEvent = { isBatched?: boolean; }; -export type WhatsAppInboundQuote = { +type WhatsAppInboundQuote = { context?: WhatsAppReplyContext; id?: string; body?: string; @@ -76,7 +73,7 @@ export type WhatsAppInboundQuote = { }; }; -export type WhatsAppInboundGroupContext = { +type WhatsAppInboundGroupContext = { subject?: string; participants?: string[]; mentions?: { @@ -104,8 +101,6 @@ type WhatsAppInboundPayload = { }; location?: NormalizedLocation; channelStructuredContext?: WhatsAppInboundStructuredContextEntry[]; - /** @deprecated Use `channelStructuredContext`. Removal: 2026-08-30. */ - untrustedStructuredContext?: WhatsAppInboundStructuredContextEntry[]; }; type WhatsAppInboundPlatform = { @@ -129,85 +124,11 @@ type WhatsAppInboundPlatform = { ) => Promise; }; -export type DeprecatedWebInboundMessageFlatAliases = { - /** @deprecated Use `event.id`. */ - id?: string; - /** @deprecated Use `platform.recipientJid`. */ - to: string; - /** @deprecated Use `payload.body`. */ - body: string; - /** @deprecated Use `platform.pushName`. */ - pushName?: string; - /** @deprecated Use `event.timestamp`. */ - timestamp?: number; - /** @deprecated Use `platform.chatJid`. */ - chatId: string; - /** @deprecated Use `platform.sender`. */ - sender?: WhatsAppIdentity; - /** @deprecated Use `platform.senderJid`. */ - senderJid?: string; - /** @deprecated Use `platform.senderE164`. */ - senderE164?: string; - /** @deprecated Use `platform.senderName`. */ - senderName?: string; - /** @deprecated Use `quote.context`. */ - replyTo?: WhatsAppReplyContext; - /** @deprecated Use `quote.id`. */ - replyToId?: string; - /** @deprecated Use `quote.body`. */ - replyToBody?: string; - /** @deprecated Use `quote.sender.displayName`. */ - replyToSender?: string; - /** @deprecated Use `quote.sender.jid`. */ - replyToSenderJid?: string; - /** @deprecated Use `quote.sender.e164`. */ - replyToSenderE164?: string; - /** @deprecated Use `group.subject`. */ - groupSubject?: string; - /** @deprecated Use `group.participants`. */ - groupParticipants?: string[]; - /** @deprecated Use `group.mentions.jids`. */ - mentions?: string[]; - /** @deprecated Use `group.mentions.jids`. */ - mentionedJids?: string[]; - /** @deprecated Use `platform.self`. */ - self?: WhatsAppSelfIdentity; - /** @deprecated Use `platform.selfJid`. */ - selfJid?: string | null; - /** @deprecated Use `platform.selfLid`. */ - selfLid?: string | null; - /** @deprecated Use `platform.selfE164`. */ - selfE164?: string | null; - /** @deprecated Use `platform.fromMe`. */ - fromMe?: boolean; - /** @deprecated Use `payload.location`. */ - location?: NormalizedLocation; - /** @deprecated Use `platform.sendComposing`. */ - sendComposing: () => Promise; - /** @deprecated Use `platform.reply`. */ - reply: (text: string, options?: MiscMessageGenerationOptions) => Promise; - /** @deprecated Use `platform.sendMedia`. */ - sendMedia: ( - payload: AnyMessageContent, - options?: MiscMessageGenerationOptions, - ) => Promise; - /** @deprecated Use `payload.media.path`. */ - mediaPath?: string; - /** @deprecated Use `payload.media.type`. */ - mediaType?: string; - /** @deprecated Use `payload.media.fileName`. */ - mediaFileName?: string; - /** @deprecated Use `payload.media.url`. */ - mediaUrl?: string; - /** @deprecated Use `payload.channelStructuredContext`. */ - channelStructuredContext?: WhatsAppInboundStructuredContextEntry[]; - /** @deprecated Use `payload.channelStructuredContext`. Removal: 2026-08-30. */ - untrustedStructuredContext?: WhatsAppInboundStructuredContextEntry[]; - /** @deprecated Use `event.isBatched`. */ - isBatched?: boolean; -}; - -type WebInboundCallbackMessageCommon = { +export type WebInboundCallbackMessage = { + admission: WhatsAppInboundAdmission; + event: WhatsAppInboundEvent; + payload: WhatsAppInboundPayload; + platform: WhatsAppInboundPlatform; quote?: WhatsAppInboundQuote; group?: WhatsAppInboundGroupContext; wasMentioned?: boolean; @@ -217,41 +138,6 @@ type WebInboundCallbackMessageCommon = { }; }; -type WebInboundCallbackAdmissionFields = - | ({ admission: WhatsAppInboundAdmission } & Partial) - | ({ admission?: WhatsAppInboundAdmission } & DeprecatedWebInboundAdmissionTopLevelFields); - -export type WebInboundCallbackMessage = WebInboundCallbackMessageCommon & - WebInboundCallbackAdmissionFields & { - event: WhatsAppInboundEvent; - payload: WhatsAppInboundPayload; - platform: WhatsAppInboundPlatform; - }; - -export type WebInboundMessage = WebInboundCallbackMessage & - DeprecatedWebInboundAdmissionTopLevelFields & - DeprecatedWebInboundMessageFlatAliases; - -export type AdmittedWebInboundMessage = Omit< - WebInboundMessage, - keyof DeprecatedWebInboundAdmissionTopLevelFields | "admission" -> & { - admission: WhatsAppInboundAdmission; -}; - -export type AdmittedWebInboundCallbackMessage = WebInboundMessage & { - admission: WhatsAppInboundAdmission; -}; - -export type LegacyFlatWebInboundMessage = DeprecatedWebInboundAdmissionTopLevelFields & - Pick & { - admission?: WhatsAppInboundAdmission; - } & DeprecatedWebInboundMessageFlatAliases & { - event?: never; - payload?: never; - platform?: never; - quote?: never; - group?: never; - }; - -export type WebInboundMessageInput = LegacyFlatWebInboundMessage | WebInboundCallbackMessage; +export type WebInboundMessage = WebInboundCallbackMessage; +export type AdmittedWebInboundMessage = WebInboundCallbackMessage; +export type AdmittedWebInboundCallbackMessage = WebInboundCallbackMessage; diff --git a/extensions/whatsapp/src/monitor-inbox.delivery-and-dedupe.test.ts b/extensions/whatsapp/src/monitor-inbox.delivery-and-dedupe.test.ts index 84d9f2972d4c..b609bc677c8c 100644 --- a/extensions/whatsapp/src/monitor-inbox.delivery-and-dedupe.test.ts +++ b/extensions/whatsapp/src/monitor-inbox.delivery-and-dedupe.test.ts @@ -6,7 +6,6 @@ import type { WebInboundMessage } from "./inbound/types.js"; import { nextMessageId, inboundMessage, - expectDeprecatedAdmissionAliases, installStreamsInboundMessageHooks, } from "./monitor-inbox.streams-inbound-messages.test-support.js"; import { @@ -25,8 +24,8 @@ describe("web monitor inbox delivery and dedupe", () => { it("delivery coordinator streams inbound messages", async () => { const onMessage = vi.fn(async (msg) => { await msg.platform.sendComposing(); - await msg.reply("flat reply works"); - await msg.sendMedia({ text: "flat media works" }); + await msg.platform.reply("nested reply works"); + await msg.platform.sendMedia({ text: "nested media works" }); }); const { listener, sock } = await startInboxMonitor(onMessage as InboxOnMessage); @@ -60,7 +59,6 @@ describe("web monitor inbox delivery and dedupe", () => { decision: "allow", }, }); - expectDeprecatedAdmissionAliases(inbound); expect(sock.readMessages).toHaveBeenCalledWith([ { remoteJid: "999@s.whatsapp.net", @@ -72,10 +70,10 @@ describe("web monitor inbox delivery and dedupe", () => { expect(sock.sendPresenceUpdate).toHaveBeenCalledWith("available"); expect(sock.sendPresenceUpdate).toHaveBeenCalledWith("composing", "999@s.whatsapp.net"); expect(sock.sendMessage).toHaveBeenNthCalledWith(1, "999@s.whatsapp.net", { - text: "flat reply works", + text: "nested reply works", }); expect(sock.sendMessage).toHaveBeenNthCalledWith(2, "999@s.whatsapp.net", { - text: "flat media works", + text: "nested media works", }); await listener.close(); diff --git a/extensions/whatsapp/src/monitor-inbox.socket-lifecycle.test.ts b/extensions/whatsapp/src/monitor-inbox.socket-lifecycle.test.ts index 159dc28f464c..a910686a3cef 100644 --- a/extensions/whatsapp/src/monitor-inbox.socket-lifecycle.test.ts +++ b/extensions/whatsapp/src/monitor-inbox.socket-lifecycle.test.ts @@ -625,8 +625,8 @@ describe("web monitor inbox socket lifecycle", () => { }); try { - await inbound.reply("pong"); - await inbound.sendMedia({ text: "media after restart" }); + await inbound.platform.reply("pong"); + await inbound.platform.sendMedia({ text: "media after restart" }); expect(successorSock.sendMessage).toHaveBeenCalledTimes(2); expect(successorSock.sendMessage).toHaveBeenNthCalledWith(1, "999@s.whatsapp.net", { @@ -650,7 +650,7 @@ describe("web monitor inbox socket lifecycle", () => { }); try { - await inbound.reply("pong"); + await inbound.platform.reply("pong"); expect(successorSock.sendMessage).toHaveBeenCalledTimes(1); expect(successorSock.sendMessage).toHaveBeenCalledWith("999@s.whatsapp.net", { text: "pong", @@ -670,7 +670,7 @@ describe("web monitor inbox socket lifecycle", () => { }); try { - await expect(inbound.reply("pong")).rejects.toThrow( + await expect(inbound.platform.reply("pong")).rejects.toThrow( "no active socket - reconnection in progress", ); expect(successorSock.sendMessage).not.toHaveBeenCalled(); diff --git a/extensions/whatsapp/src/monitor-inbox.streams-inbound-messages.test-support.ts b/extensions/whatsapp/src/monitor-inbox.streams-inbound-messages.test-support.ts index 38556c9a6148..c2927eeb9ce5 100644 --- a/extensions/whatsapp/src/monitor-inbox.streams-inbound-messages.test-support.ts +++ b/extensions/whatsapp/src/monitor-inbox.streams-inbound-messages.test-support.ts @@ -82,14 +82,6 @@ export function inboundMessage(onMessage: ReturnType, index = 0): return msg as WebInboundMessage; } -export function expectDeprecatedAdmissionAliases(inbound: WebInboundMessage) { - expect(inbound.from).toBe(inbound.admission?.conversation.id); - expect(inbound.conversationId).toBe(inbound.admission?.conversation.id); - expect(inbound.accountId).toBe(inbound.admission?.accountId); - expect(inbound.chatType).toBe(inbound.admission?.conversation.kind); - expect(inbound.accessControlPassed).toBe(inbound.admission?.ingress.decision === "allow"); -} - export async function expectSocketOperationTimeout( operation: "sendMessage" | "sendPresenceUpdate", promise: Promise, diff --git a/package.json b/package.json index fc2aad412d89..3269b62b5ea4 100644 --- a/package.json +++ b/package.json @@ -1180,10 +1180,6 @@ "types": "./dist/plugin-sdk/media-understanding-runtime.d.ts", "default": "./dist/plugin-sdk/media-understanding-runtime.js" }, - "./plugin-sdk/messaging-targets": { - "types": "./dist/plugin-sdk/messaging-targets.d.ts", - "default": "./dist/plugin-sdk/messaging-targets.js" - }, "./plugin-sdk/request-url": { "default": "./dist/plugin-sdk/request-url.js" }, diff --git a/packages/memory-host-sdk/src/engine-embeddings.ts b/packages/memory-host-sdk/src/engine-embeddings.ts index 1d205de7dcfe..b0b0cd53e46d 100644 --- a/packages/memory-host-sdk/src/engine-embeddings.ts +++ b/packages/memory-host-sdk/src/engine-embeddings.ts @@ -2,7 +2,6 @@ export { getMemoryEmbeddingProvider, - listRegisteredMemoryEmbeddingProviders, listMemoryEmbeddingProviders, listRegisteredMemoryEmbeddingProviderAdapters, } from "./host/openclaw-runtime-memory.js"; diff --git a/packages/memory-host-sdk/src/host/openclaw-runtime-memory.ts b/packages/memory-host-sdk/src/host/openclaw-runtime-memory.ts index e6861345adde..59a3ff393e97 100644 --- a/packages/memory-host-sdk/src/host/openclaw-runtime-memory.ts +++ b/packages/memory-host-sdk/src/host/openclaw-runtime-memory.ts @@ -8,7 +8,6 @@ export { listActiveMemoryPublicArtifacts, listMemoryEmbeddingProviders, listRegisteredMemoryEmbeddingProviderAdapters, - listRegisteredMemoryEmbeddingProviders, resolveCanonicalRootMemoryFile, shouldSkipRootMemoryAuxiliaryPath, } from "./openclaw-runtime.js"; diff --git a/packages/memory-host-sdk/src/host/openclaw-runtime.ts b/packages/memory-host-sdk/src/host/openclaw-runtime.ts index 50ab766d2f1d..dec2db4fe96d 100644 --- a/packages/memory-host-sdk/src/host/openclaw-runtime.ts +++ b/packages/memory-host-sdk/src/host/openclaw-runtime.ts @@ -120,7 +120,6 @@ export { getMemoryEmbeddingProvider, listMemoryEmbeddingProviders, listRegisteredMemoryEmbeddingProviderAdapters, - listRegisteredMemoryEmbeddingProviders, } from "../../../../src/plugins/memory-embedding-provider-runtime.js"; export type { MemoryEmbeddingBatchChunk, diff --git a/qa/scenarios/plugins/kitchen-sink-live-openai.yaml b/qa/scenarios/plugins/kitchen-sink-live-openai.yaml index f6e9aef7f413..2a0e1a086df0 100644 --- a/qa/scenarios/plugins/kitchen-sink-live-openai.yaml +++ b/qa/scenarios/plugins/kitchen-sink-live-openai.yaml @@ -98,7 +98,6 @@ scenario: - hosted media resolver registration missing resolver - "http route registration missing or invalid auth: /kitchen-sink/http-route" - "plugin must declare contracts.embeddingProviders for adapter: kitchen-sink-embedding-provider" - - "plugin must own memory slot or declare contracts.memoryEmbeddingProviders for adapter: kitchen-sink-memory-embedding-provider" - "trusted tool policy registration requires id, description, and evaluate()" - memory prompt supplement registration missing builder - model catalog provider registration missing provider diff --git a/scripts/e2e/lib/kitchen-sink-plugin/assertions.mjs b/scripts/e2e/lib/kitchen-sink-plugin/assertions.mjs index c4123f4b5ae7..aa85fe4d5b6a 100644 --- a/scripts/e2e/lib/kitchen-sink-plugin/assertions.mjs +++ b/scripts/e2e/lib/kitchen-sink-plugin/assertions.mjs @@ -349,7 +349,6 @@ function assertExpectedDiagnostics(surfaceMode, errorMessages) { "node invoke policy registration missing commands", "trusted tool policy registration requires id, description, and evaluate()", "plugin must declare contracts.embeddingProviders for adapter: kitchen-sink-embedding-provider", - "plugin must own memory slot or declare contracts.memoryEmbeddingProviders for adapter: kitchen-sink-memory-embedding-provider", "plugin must declare contracts.tools for: kitchen-sink-tool", 'channel "kitchen-sink-channel-probe" registration missing required config helpers', 'agent harness "kitchen-sink-agent-harness" registration missing required runtime methods', diff --git a/scripts/generate-plugin-inventory-doc.mts b/scripts/generate-plugin-inventory-doc.mts index d40f7d223009..a04617d1773b 100644 --- a/scripts/generate-plugin-inventory-doc.mts +++ b/scripts/generate-plugin-inventory-doc.mts @@ -258,7 +258,7 @@ function resolveDescription({ manifest, packageJson }: PluginSourceEntry) { documentExtractors: "Adds document extraction for local attachments.", imageGenerationProviders: "Adds image generation provider support.", mediaUnderstandingProviders: "Adds media understanding provider support.", - memoryEmbeddingProviders: "Adds memory embedding provider support.", + embeddingProviders: "Adds embedding provider support, including memory search.", migrationProviders: "Adds migration import support.", musicGenerationProviders: "Adds music generation provider support.", realtimeTranscriptionProviders: "Adds realtime transcription provider support.", diff --git a/scripts/lib/official-external-provider-catalog.json b/scripts/lib/official-external-provider-catalog.json index c3a3566bd5ad..e42b51d66d4a 100644 --- a/scripts/lib/official-external-provider-catalog.json +++ b/scripts/lib/official-external-provider-catalog.json @@ -900,7 +900,7 @@ } ], "contracts": { - "memoryEmbeddingProviders": ["mistral"], + "embeddingProviders": ["mistral"], "mediaUnderstandingProviders": ["mistral"], "realtimeTranscriptionProviders": ["mistral"] }, @@ -2273,7 +2273,7 @@ } ], "contracts": { - "memoryEmbeddingProviders": [ + "embeddingProviders": [ "voyage" ] }, diff --git a/scripts/lib/plugin-sdk-deprecated-public-subpaths.json b/scripts/lib/plugin-sdk-deprecated-public-subpaths.json index 6f7699c8df34..a34ccde783bc 100644 --- a/scripts/lib/plugin-sdk-deprecated-public-subpaths.json +++ b/scripts/lib/plugin-sdk-deprecated-public-subpaths.json @@ -8,6 +8,5 @@ "discord", "inbound-reply-dispatch", "infra-runtime", - "messaging-targets", "telegram-account" ] diff --git a/scripts/lib/plugin-sdk-entrypoints.json b/scripts/lib/plugin-sdk-entrypoints.json index e06b3c31ff5c..22c6549089b9 100644 --- a/scripts/lib/plugin-sdk-entrypoints.json +++ b/scripts/lib/plugin-sdk-entrypoints.json @@ -245,7 +245,6 @@ "transcripts", "media-understanding", "media-understanding-runtime", - "messaging-targets", "request-url", "runtime-store", "json-store", diff --git a/scripts/plugin-sdk-surface-report.mts b/scripts/plugin-sdk-surface-report.mts index c9ec409aad10..1db2fdbac5e8 100644 --- a/scripts/plugin-sdk-surface-report.mts +++ b/scripts/plugin-sdk-surface-report.mts @@ -149,7 +149,7 @@ const defaultPublicDeprecatedExportsByEntrypointBudget = Object.freeze({ // +4: legacy AgentHarness, attempt, embedded-run, and side-question contracts remain // deprecated while external harnesses migrate to required-capability V2 contracts. "agent-harness": 2, - "agent-harness-runtime": 12, + "agent-harness-runtime": 9, "command-auth": 78, discord: 47, // +4: deprecated media projection type, builder, and turn aliases. @@ -169,10 +169,8 @@ const defaultPublicDeprecatedExportsByEntrypointBudget = Object.freeze({ "reply-runtime": 1, "security-runtime": 1, "session-store-runtime": 4, - // +2: shipped Slack and Discord setup helpers retained through their package migration window. - "setup-runtime": 2, + "setup-runtime": 0, "reply-history": 6, - "messaging-targets": 12, "provider-auth": 19, "telegram-account": 3, } satisfies Record); @@ -195,7 +193,8 @@ export function readPluginSdkSurfaceBudgets(env: NodeJS.ProcessEnv = process.env // (realtime-voice-activation is private-local and not counted here). // +1: registry-bound plugin command planning and exact selected execution. // +1: canonical Computer Use wire contract and node-host provider seam. - 145, + // -1: retire the deprecated messaging-targets subpath. + 144, env, ), publicExports: readPluginSdkSurfaceBudgetEnv( @@ -212,7 +211,6 @@ export function readPluginSdkSurfaceBudgets(env: NodeJS.ProcessEnv = process.env // +1: outbound formatting capability profile. // +3: plugin approval reviewer-detail cap/truncator and sanitize-with-status variant. // +1: canonical incognito session classifier for storage-safe plugin behavior. - // +2: shipped Slack and Discord setup compatibility helpers. // +3: typed channel partial-delivery error, creator, and structural guard. // +1: closed attempt-terminal merge, normalization, and projection helper. // +3: harness-native MCP App preview helper and its runtime/catalog contracts. @@ -285,7 +283,9 @@ export function readPluginSdkSurfaceBudgets(env: NodeJS.ProcessEnv = process.env // (voice-call/matrix runtime-doctor repair names, WhatsApp ack policy, // Slack progress-draft render) so installed plugins survive upgrade (#124041 class). // -1: remove the orphan diagnostic traceparent propagation export. - 4329, + // -17: retire the messaging-targets subpath, embedded Pi aliases, and shipped + // channel setup compatibility helpers. + 4312, env, ), publicFunctionExports: readPluginSdkSurfaceBudgetEnv( @@ -301,7 +301,6 @@ export function readPluginSdkSurfaceBudgets(env: NodeJS.ProcessEnv = process.env // +1: agent-harness transcript visibility projector. // +2: plugin approval detail truncator and sanitize-with-status variant. // +1: canonical incognito session classifier for storage-safe plugin behavior. - // +2: shipped Slack and Discord setup compatibility helpers. // +2: channel partial-delivery error creator and structural guard. // +1: harness-native MCP App preview helper. // +1: canonical unknown-value to Error coercion. @@ -359,13 +358,13 @@ export function readPluginSdkSurfaceBudgets(env: NodeJS.ProcessEnv = process.env // (voice-call/matrix runtime-doctor repair names, WhatsApp ack policy, // Slack progress-draft render) so installed plugins survive upgrade (#124041 class). // -1: remove the orphan diagnostic traceparent propagation export. - 2576, + // -12: retire the callable messaging-targets, embedded Pi, and channel setup helpers. + 2564, env, ), publicDeprecatedExports: readPluginSdkSurfaceBudgetEnv( "OPENCLAW_PLUGIN_SDK_MAX_PUBLIC_DEPRECATED_EXPORTS", // +3: canonical incognito classifier projected through deprecated compatibility barrels. - // +2: shipped Slack and Discord setup compatibility helpers. // +10: named media legacy projection deprecations across public compatibility barrels. // +2: channel prompt-context type and metadata builder compatibility aliases. // +1: shared ingress error factory projected through channel-message. @@ -378,7 +377,8 @@ export function readPluginSdkSurfaceBudgets(env: NodeJS.ProcessEnv = process.env // +5: load-only bridges for published pre-split plugin artifacts // (voice-call/matrix runtime-doctor repair names, WhatsApp ack policy, // Slack progress-draft render) so installed plugins survive upgrade (#124041 class). - 1151, + // -18: retire the expired August compatibility exports and messaging-targets subpath. + 1133, env, ), publicWildcardReexports: readPluginSdkSurfaceBudgetEnv( diff --git a/src/agents/memory-search.test.ts b/src/agents/memory-search.test.ts index 1cb7492b87a4..013c05bbb600 100644 --- a/src/agents/memory-search.test.ts +++ b/src/agents/memory-search.test.ts @@ -10,8 +10,8 @@ import { type RegisteredEmbeddingProvider, } from "../plugins/embedding-providers.js"; import { - clearMemoryEmbeddingProviders, - registerMemoryEmbeddingProvider, + adaptMemoryEmbeddingProviderAdapter, + type MemoryEmbeddingProviderAdapter, } from "../plugins/memory-embedding-providers.js"; import { SecretSurfaceUnavailableError, @@ -29,23 +29,27 @@ const asConfig = (cfg: OpenClawConfig): OpenClawConfig => ({ }); let registeredEmbeddingProvidersSnapshot: RegisteredEmbeddingProvider[]; +function registerTestMemoryAdapter(adapter: MemoryEmbeddingProviderAdapter): void { + registerEmbeddingProvider(adaptMemoryEmbeddingProviderAdapter(adapter)); +} + function registerBaseMemoryEmbeddingProviders(options?: { includeGemini?: boolean }): void { // Register provider contracts locally so config tests do not depend on the // plugin loader or live embedding backends. - registerMemoryEmbeddingProvider({ + registerTestMemoryAdapter({ id: "openai", defaultModel: "text-embedding-3-small", transport: "remote", create: async () => ({ provider: null }), }); - registerMemoryEmbeddingProvider({ + registerTestMemoryAdapter({ id: "local", defaultModel: "local-default", transport: "local", create: async () => ({ provider: null }), }); if (options?.includeGemini !== false) { - registerMemoryEmbeddingProvider({ + registerTestMemoryAdapter({ id: "gemini", defaultModel: "gemini-embedding-001", transport: "remote", @@ -57,25 +61,25 @@ function registerBaseMemoryEmbeddingProviders(options?: { includeGemini?: boolea create: async () => ({ provider: null }), }); } - registerMemoryEmbeddingProvider({ + registerTestMemoryAdapter({ id: "voyage", defaultModel: "voyage-4-large", transport: "remote", create: async () => ({ provider: null }), }); - registerMemoryEmbeddingProvider({ + registerTestMemoryAdapter({ id: "mistral", defaultModel: "mistral-embed", transport: "remote", create: async () => ({ provider: null }), }); - registerMemoryEmbeddingProvider({ + registerTestMemoryAdapter({ id: "lmstudio", defaultModel: "text-embedding-nomic-embed-text-v1.5", transport: "remote", create: async () => ({ provider: null }), }); - registerMemoryEmbeddingProvider({ + registerTestMemoryAdapter({ id: "ollama", defaultModel: "nomic-embed-text", transport: "remote", @@ -87,13 +91,11 @@ describe("memory search config", () => { beforeEach(() => { registeredEmbeddingProvidersSnapshot = listRegisteredEmbeddingProviders(); clearEmbeddingProviders(); - clearMemoryEmbeddingProviders(); registerBaseMemoryEmbeddingProviders(); }); afterEach(() => { setActiveDegradedSecretOwners([]); - clearMemoryEmbeddingProviders(); restoreRegisteredEmbeddingProviders(registeredEmbeddingProvidersSnapshot); }); @@ -490,7 +492,7 @@ describe("memory search config", () => { }); it("resolves fixed sync defaults without consulting embedding providers", () => { - clearMemoryEmbeddingProviders(); + clearEmbeddingProviders(); const cfg = asConfig({ memory: { search: { @@ -753,7 +755,7 @@ describe("memory search config", () => { }); it("accepts Gemini multimodal memory even when the runtime registry has not registered Gemini yet", () => { - clearMemoryEmbeddingProviders(); + clearEmbeddingProviders(); registerBaseMemoryEmbeddingProviders({ includeGemini: false }); const cfg = asConfig({ memory: { diff --git a/src/agents/openclaw-tools.subagents.sessions-spawn.lifecycle.test.ts b/src/agents/openclaw-tools.subagents.sessions-spawn.lifecycle.test.ts index 6be15aa1a2ab..0d6eed22a42e 100644 --- a/src/agents/openclaw-tools.subagents.sessions-spawn.lifecycle.test.ts +++ b/src/agents/openclaw-tools.subagents.sessions-spawn.lifecycle.test.ts @@ -26,7 +26,6 @@ const fastModeEnv = vi.hoisted(() => { }); const hookRunnerMocks = vi.hoisted(() => ({ - runSubagentSpawning: vi.fn(async () => undefined), runSubagentSpawned: vi.fn(async () => {}), runSubagentProgress: vi.fn(async () => {}), runSubagentEnded: vi.fn(async () => {}), @@ -182,7 +181,6 @@ describe("openclaw-tools: subagents (sessions_spawn lifecycle)", () => { }, }); resetSubagentRegistryForTests({ persist: false }); - hookRunnerMocks.runSubagentSpawning.mockClear(); hookRunnerMocks.runSubagentSpawned.mockClear(); hookRunnerMocks.runSubagentProgress.mockClear(); hookRunnerMocks.runSubagentEnded.mockClear(); @@ -191,7 +189,6 @@ describe("openclaw-tools: subagents (sessions_spawn lifecycle)", () => { hookName === "subagent_spawned" || hookName === "subagent_progress" || hookName === "subagent_ended", - runSubagentSpawning: hookRunnerMocks.runSubagentSpawning, runSubagentSpawned: hookRunnerMocks.runSubagentSpawned, runSubagentProgress: hookRunnerMocks.runSubagentProgress, runSubagentEnded: hookRunnerMocks.runSubagentEnded, diff --git a/src/agents/subagents/spawn/subagent-spawn.test-helpers.ts b/src/agents/subagents/spawn/subagent-spawn.test-helpers.ts index da8fc171a09d..06e256f2b1cf 100644 --- a/src/agents/subagents/spawn/subagent-spawn.test-helpers.ts +++ b/src/agents/subagents/spawn/subagent-spawn.test-helpers.ts @@ -16,7 +16,7 @@ type HookRunner = Pick & Partial< Pick< SubagentLifecycleHookRunner, - "runSubagentSpawning" | "runSubagentSpawned" | "runSubagentProgress" | "runSubagentEnded" + "runSubagentSpawned" | "runSubagentProgress" | "runSubagentEnded" > >; type SubagentSpawnModuleForTest = Awaited & { diff --git a/src/auto-reply/reply/group-id.test.ts b/src/auto-reply/reply/group-id.test.ts index 468bdb8da8fd..c89dbc4542cd 100644 --- a/src/auto-reply/reply/group-id.test.ts +++ b/src/auto-reply/reply/group-id.test.ts @@ -60,33 +60,17 @@ describe("extractExplicitGroupId", () => { name: "declared inferred shorthand", channel: "telegram", declaresNumericShorthand: true, - path: "inferred" as const, expected: "-100200300", }, { name: "undeclared inferred shorthand", channel: "plainchat", declaresNumericShorthand: false, - path: "inferred" as const, - expected: "-100200300:77", - }, - { - name: "declared parser shorthand", - channel: "telegram", - declaresNumericShorthand: true, - path: "parser" as const, - expected: "-100200300", - }, - { - name: "undeclared parser shorthand", - channel: "plainchat", - declaresNumericShorthand: false, - path: "parser" as const, expected: "-100200300:77", }, ])( "uses $name metadata after target normalization", - ({ channel, declaresNumericShorthand, path, expected }) => { + ({ channel, declaresNumericShorthand, expected }) => { setActivePluginRegistry( createTestRegistry([ { @@ -99,17 +83,8 @@ describe("extractExplicitGroupId", () => { }), messaging: { ...(declaresNumericShorthand ? { numericTopicShorthand: true as const } : {}), - ...(path === "inferred" - ? { - normalizeTarget: () => `${channel}:-100200300:77`, - inferTargetChatType: () => "group" as const, - } - : { - parseExplicitTarget: () => ({ - to: "group:-100200300:77", - chatType: "group" as const, - }), - }), + normalizeTarget: () => `${channel}:-100200300:77`, + inferTargetChatType: () => "group" as const, }, }, }, @@ -119,29 +94,4 @@ describe("extractExplicitGroupId", () => { expect(extractExplicitGroupId(`${channel}:-100200300:77`)).toBe(expected); }, ); - - it("keeps legacy parser-only group target extraction quarantined", () => { - setActivePluginRegistry( - createTestRegistry([ - { - pluginId: "legacygroup", - source: "test", - plugin: { - ...createChannelTestPluginBase({ - id: "legacygroup", - capabilities: { chatTypes: ["group"] }, - }), - messaging: { - parseExplicitTarget: ({ raw }: { raw: string }) => - raw.startsWith("legacygroup:") - ? { to: "group:room-a:topic:77", chatType: "group" as const } - : null, - }, - }, - }, - ]), - ); - - expect(extractExplicitGroupId("legacygroup:room-a:topic:77")).toBe("room-a"); - }); }); diff --git a/src/auto-reply/reply/group-id.ts b/src/auto-reply/reply/group-id.ts index af90e8a42074..b451cab1a4b0 100644 --- a/src/auto-reply/reply/group-id.ts +++ b/src/auto-reply/reply/group-id.ts @@ -45,28 +45,6 @@ function extractInferredGroupTargetId(params: { return undefined; } -function extractLegacyParsedGroupTargetId(params: { - raw: string; - channelId: string; - messaging?: ChannelMessagingAdapter; -}): string | undefined { - const parsed = params.messaging?.parseExplicitTarget?.({ raw: params.raw }); - if (parsed?.chatType === "direct" || parsed?.chatType == null) { - return undefined; - } - const target = stripTargetTopicSuffix( - stripOutboundTargetKindPrefix(stripTargetProviderPrefix(parsed.to, params.channelId), [ - "group", - "channel", - "conversation", - "room", - "thread", - ]), - { allowNumericShorthand: params.messaging?.numericTopicShorthand === true }, - ); - return target || undefined; -} - /** Extracts a group/channel target id from explicit channel target syntax. */ export function extractExplicitGroupId(raw: string | undefined | null): string | undefined { const trimmed = normalizeOptionalString(raw) ?? ""; @@ -84,16 +62,9 @@ export function extractExplicitGroupId(raw: string | undefined | null): string | if (!channelId) { return undefined; } - return ( - extractInferredGroupTargetId({ - raw: trimmed, - channelId, - messaging, - }) ?? - extractLegacyParsedGroupTargetId({ - raw: trimmed, - channelId, - messaging, - }) - ); + return extractInferredGroupTargetId({ + raw: trimmed, + channelId, + messaging, + }); } diff --git a/src/channels/conversation-resolution.test.ts b/src/channels/conversation-resolution.test.ts index 03f1f83c5e74..98288cfc99e8 100644 --- a/src/channels/conversation-resolution.test.ts +++ b/src/channels/conversation-resolution.test.ts @@ -285,31 +285,6 @@ describe("conversation resolution", () => { }, ); - it("keeps parser-only fallback conversation targets during the migration window", () => { - registerChannelPlugin({ - ...createChannelTestPluginBase({ id: "legacychat", label: "Legacy chat" }), - messaging: { - parseExplicitTarget: ({ raw }) => - raw === "room-a:topic:77" - ? { to: "room-a", threadId: 77, chatType: "group" as const } - : null, - }, - }); - - expect( - resolveCommandConversationResolution({ - cfg: testConfig, - channel: "legacychat", - accountId: "default", - originatingTo: "room-a:topic:77", - })?.canonical, - ).toEqual({ - channel: "legacychat", - accountId: "default", - conversationId: "room-a", - }); - }); - it("normalizes numeric command thread ids through the shared route contract", () => { registerChannelPlugin({ ...createChannelTestPluginBase({ id: "test-chat", label: "Test chat" }), diff --git a/src/channels/conversation-resolution.ts b/src/channels/conversation-resolution.ts index b02c8ed471d1..86ab3e6698f0 100644 --- a/src/channels/conversation-resolution.ts +++ b/src/channels/conversation-resolution.ts @@ -19,7 +19,6 @@ import { normalizeConversationTargetRef } from "../infra/outbound/session-bindin import { stringifyRouteThreadId } from "../plugin-sdk/channel-route.js"; import { getActivePluginChannelRegistry } from "../plugins/runtime.js"; import { getLoadedChannelPlugin, normalizeChannelId } from "./plugins/index.js"; -import { resolveExplicitDeliveryTargetCompat } from "./plugins/target-parsing-loaded.js"; import { resolveBundledChannelThreadBindingDefaultPlacement, resolveBundledChannelThreadBindingInboundConversation, @@ -254,18 +253,6 @@ function resolveChannelTargetId(params: { return conversationId || withoutProvider || normalizedTarget; } - const parsedTarget = resolveExplicitDeliveryTargetCompat({ - channel: params.channel, - rawTarget: target, - }); - if (parsedTarget?.to) { - return ( - resolveConversationIdFromTargets({ - targets: [parsedTarget.to], - }) ?? parsedTarget.to - ); - } - return target; } diff --git a/src/channels/plugins/contracts/test-helpers/surface-contract-suite.ts b/src/channels/plugins/contracts/test-helpers/surface-contract-suite.ts index 8a65a6f6b256..31602cf59f22 100644 --- a/src/channels/plugins/contracts/test-helpers/surface-contract-suite.ts +++ b/src/channels/plugins/contracts/test-helpers/surface-contract-suite.ts @@ -73,7 +73,6 @@ export function expectChannelSurfaceContract(params: { expect( [ messaging?.normalizeTarget, - messaging?.parseExplicitTarget, messaging?.inferTargetChatType, messaging?.buildCrossContextPresentation, messaging?.hasStructuredReplyPayload, diff --git a/src/channels/plugins/setup-wizard-legacy-compat.test.ts b/src/channels/plugins/setup-wizard-legacy-compat.test.ts deleted file mode 100644 index 69acdfac6caf..000000000000 --- a/src/channels/plugins/setup-wizard-legacy-compat.test.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { describe, expect, it, vi } from "vitest"; -import type { OpenClawConfig } from "../../config/types.openclaw.js"; -import type { WizardPrompter } from "../../wizard/prompts.js"; -import { - createLegacyCompatChannelDmPolicy, - promptLegacyChannelAllowFromForAccount, -} from "./setup-wizard-legacy-compat.js"; - -describe("legacy channel setup compatibility", () => { - it("preserves legacy DM policy behavior for shipped setup plugins", () => { - const policy = createLegacyCompatChannelDmPolicy({ label: "Slack", channel: "slack" }); - const initial = { - channels: { - slack: { - dm: { policy: "allowlist" }, - accounts: { work: { dmPolicy: "disabled", allowFrom: ["U1"] } }, - }, - }, - } as OpenClawConfig; - - expect(policy.getCurrent(initial, "work")).toBe("disabled"); - expect(policy.setPolicy(initial, "open", "work")).toMatchObject({ - channels: { - slack: { - accounts: { - work: { dmPolicy: "open", allowFrom: ["U1", "*"] }, - }, - }, - }, - }); - expect(policy.setPolicy(initial, "open", "default")).toMatchObject({ - channels: { - slack: { - dmPolicy: "open", - allowFrom: ["*"], - dm: { policy: "allowlist", enabled: true }, - }, - }, - }); - }); - - it("preserves the legacy allowlist prompt contract", async () => { - const note = vi.fn(async () => undefined); - const text = vi.fn(async () => "U2"); - const prompter = { note, text } as unknown as WizardPrompter; - const cfg = { - channels: { slack: { allowFrom: ["U1"] } }, - } as OpenClawConfig; - - const next = await promptLegacyChannelAllowFromForAccount({ - cfg, - channel: "slack", - prompter, - defaultAccountId: "default", - resolveAccount: () => ({ allowFrom: ["U1"] }), - resolveExisting: (account) => account.allowFrom, - resolveToken: () => null, - noteTitle: "Slack allowlist", - noteLines: ["Enter Slack user ids"], - message: "Allowed users", - placeholder: "U123", - parseId: (value) => (/^U\d+$/.test(value) ? value : null), - invalidWithoutTokenNote: "Use an id", - resolveEntries: async () => [], - }); - - expect(note).toHaveBeenCalledWith("Enter Slack user ids", "Slack allowlist"); - expect(next).toMatchObject({ - channels: { - slack: { allowFrom: ["U1", "U2"], dm: { enabled: true } }, - }, - }); - }); -}); diff --git a/src/channels/plugins/setup-wizard-legacy-compat.ts b/src/channels/plugins/setup-wizard-legacy-compat.ts deleted file mode 100644 index 8a86e6eda18d..000000000000 --- a/src/channels/plugins/setup-wizard-legacy-compat.ts +++ /dev/null @@ -1,181 +0,0 @@ -import type { DmPolicy } from "../../config/types.base.js"; -import type { OpenClawConfig } from "../../config/types.openclaw.js"; -import { DEFAULT_ACCOUNT_ID } from "../../routing/session-key.js"; -import type { WizardPrompter } from "../../wizard/prompts.js"; -import { resolveChannelDmAllowFrom, resolveChannelDmPolicy } from "./dm-access.js"; -import { - addWildcardAllowFrom, - patchChannelConfigForAccount, - promptResolvedAllowFrom, - resolveSetupAccountId, - splitSetupEntries, -} from "./setup-wizard-helpers.js"; -import type { ChannelSetupDmPolicy } from "./setup-wizard-types.js"; - -type AllowFromResolution = { - input: string; - resolved: boolean; - id?: string | null; -}; - -function patchLegacyChannelConfig(params: { - cfg: OpenClawConfig; - channel: string; - patch: Record; -}): OpenClawConfig { - const channelConfig = - (params.cfg.channels?.[params.channel] as Record | undefined) ?? {}; - const dmConfig = (channelConfig.dm as Record | undefined) ?? {}; - return { - ...params.cfg, - channels: { - ...params.cfg.channels, - [params.channel]: { - ...channelConfig, - ...params.patch, - dm: { - ...dmConfig, - enabled: typeof dmConfig.enabled === "boolean" ? dmConfig.enabled : true, - }, - }, - }, - }; -} - -function setLegacyChannelDmPolicy(params: { - cfg: OpenClawConfig; - channel: string; - dmPolicy: DmPolicy; -}): OpenClawConfig { - const channelConfig = - (params.cfg.channels?.[params.channel] as Record | undefined) ?? {}; - const existingAllowFrom = resolveChannelDmAllowFrom({ account: channelConfig }); - const allowFrom = - params.dmPolicy === "open" ? addWildcardAllowFrom(existingAllowFrom) : undefined; - return patchLegacyChannelConfig({ - cfg: params.cfg, - channel: params.channel, - patch: { - dmPolicy: params.dmPolicy, - ...(allowFrom ? { allowFrom } : {}), - }, - }); -} - -/** @deprecated Compatibility for plugins published before setup policy became plugin-owned. */ -export function createLegacyCompatChannelDmPolicy(params: { - label: string; - channel: string; - promptAllowFrom?: ChannelSetupDmPolicy["promptAllowFrom"]; -}): ChannelSetupDmPolicy { - return { - label: params.label, - channel: params.channel, - policyKey: `channels.${params.channel}.dmPolicy`, - allowFromKey: `channels.${params.channel}.allowFrom`, - resolveConfigKeys: (_cfg, accountId) => - accountId && accountId !== DEFAULT_ACCOUNT_ID - ? { - policyKey: `channels.${params.channel}.accounts.${accountId}.dmPolicy`, - allowFromKey: `channels.${params.channel}.accounts.${accountId}.allowFrom`, - } - : { - policyKey: `channels.${params.channel}.dmPolicy`, - allowFromKey: `channels.${params.channel}.allowFrom`, - }, - getCurrent: (cfg, accountId) => { - const channelConfig = - (cfg.channels?.[params.channel] as - | { - dmPolicy?: DmPolicy; - dm?: { policy?: DmPolicy }; - accounts?: Record; - } - | undefined) ?? {}; - const accountConfig = - accountId && accountId !== DEFAULT_ACCOUNT_ID - ? channelConfig.accounts?.[accountId] - : undefined; - return resolveChannelDmPolicy({ - account: accountConfig as Record | undefined, - parent: channelConfig as Record, - defaultPolicy: "pairing", - }) as DmPolicy; - }, - setPolicy: (cfg, policy, accountId) => - accountId && accountId !== DEFAULT_ACCOUNT_ID - ? patchChannelConfigForAccount({ - cfg, - channel: params.channel, - accountId, - patch: { - dmPolicy: policy, - ...(policy === "open" - ? { - allowFrom: addWildcardAllowFrom( - resolveChannelDmAllowFrom({ - account: ( - cfg.channels?.[params.channel] as - | { accounts?: Record> } - | undefined - )?.accounts?.[accountId], - parent: cfg.channels?.[params.channel] as - | Record - | undefined, - }), - ), - } - : {}), - }, - }) - : setLegacyChannelDmPolicy({ - cfg, - channel: params.channel, - dmPolicy: policy, - }), - ...(params.promptAllowFrom ? { promptAllowFrom: params.promptAllowFrom } : {}), - }; -} - -/** @deprecated Compatibility for plugins published before setup allowlists became plugin-owned. */ -export async function promptLegacyChannelAllowFromForAccount(params: { - cfg: OpenClawConfig; - channel: string; - prompter: WizardPrompter; - accountId?: string; - defaultAccountId: string; - resolveAccount: (cfg: OpenClawConfig, accountId: string) => TAccount; - resolveExisting: (account: TAccount, cfg: OpenClawConfig) => Array; - resolveToken: (account: TAccount) => string | null | undefined; - noteTitle: string; - noteLines: string[]; - message: string; - placeholder: string; - parseId: (value: string) => string | null; - invalidWithoutTokenNote: string; - resolveEntries: (params: { token: string; entries: string[] }) => Promise; -}): Promise { - const accountId = resolveSetupAccountId({ - accountId: params.accountId, - defaultAccountId: params.defaultAccountId, - }); - const account = params.resolveAccount(params.cfg, accountId); - await params.prompter.note(params.noteLines.join("\n"), params.noteTitle); - const allowFrom = await promptResolvedAllowFrom({ - prompter: params.prompter, - existing: params.resolveExisting(account, params.cfg), - token: params.resolveToken(account), - message: params.message, - placeholder: params.placeholder, - label: params.noteTitle, - parseInputs: splitSetupEntries, - parseId: params.parseId, - invalidWithoutTokenNote: params.invalidWithoutTokenNote, - resolveEntries: params.resolveEntries, - }); - return patchLegacyChannelConfig({ - cfg: params.cfg, - channel: params.channel, - patch: { allowFrom }, - }); -} diff --git a/src/channels/plugins/target-parsing-loaded.test.ts b/src/channels/plugins/target-parsing-loaded.test.ts deleted file mode 100644 index 7cf7cc6fe63a..000000000000 --- a/src/channels/plugins/target-parsing-loaded.test.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; - -const getLoadedChannelPluginForRead = vi.hoisted(() => vi.fn()); -const getChannelPlugin = vi.hoisted(() => vi.fn()); - -vi.mock("./index.js", () => ({ - getChannelPlugin, - normalizeChannelId: (raw?: string | null) => raw?.trim().toLowerCase() || null, -})); - -vi.mock("./registry-loaded.js", () => ({ - getLoadedChannelPluginForRead, -})); - -import { resolveExplicitDeliveryTargetCompat } from "./target-parsing-loaded.js"; - -describe("resolveExplicitDeliveryTargetCompat", () => { - beforeEach(() => { - getLoadedChannelPluginForRead.mockReset(); - getChannelPlugin.mockReset(); - }); - - it("keeps unloaded channels on generic parsing without activating a plugin", () => { - getLoadedChannelPluginForRead.mockReturnValue(undefined); - - expect( - resolveExplicitDeliveryTargetCompat({ - channel: " LegacyChat ", - rawTarget: " room-a ", - fallbackThreadId: "77", - }), - ).toEqual({ - channel: "legacychat", - rawTo: "room-a", - to: "room-a", - threadId: "77", - chatType: undefined, - }); - expect(getLoadedChannelPluginForRead).toHaveBeenCalledWith("legacychat"); - expect(getChannelPlugin).not.toHaveBeenCalled(); - }); - - it("preserves the deprecated parser contract for an already-loaded plugin", () => { - const parseExplicitTarget = vi.fn(() => ({ - to: "room-a", - threadId: 42, - chatType: "group" as const, - })); - getLoadedChannelPluginForRead.mockReturnValue({ - messaging: { parseExplicitTarget }, - }); - - expect( - resolveExplicitDeliveryTargetCompat({ - channel: "legacychat", - rawTarget: "room-a:topic:42", - }), - ).toEqual({ - channel: "legacychat", - rawTo: "room-a:topic:42", - to: "room-a", - threadId: 42, - chatType: "group", - }); - expect(parseExplicitTarget).toHaveBeenCalledWith({ raw: "room-a:topic:42" }); - expect(getChannelPlugin).not.toHaveBeenCalled(); - }); -}); diff --git a/src/channels/plugins/target-parsing-loaded.ts b/src/channels/plugins/target-parsing-loaded.ts deleted file mode 100644 index 1eb9b864dcf2..000000000000 --- a/src/channels/plugins/target-parsing-loaded.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { - normalizeLowercaseStringOrEmpty, - normalizeOptionalString, - normalizeOptionalThreadValue, -} from "@openclaw/normalization-core/string-coerce"; -import type { ChannelRouteParsedTarget } from "../../plugin-sdk/channel-route.js"; -import { normalizeChannelId } from "./index.js"; -import { getLoadedChannelPluginForRead } from "./registry-loaded.js"; - -/** Preserves the shipped `parseExplicitTarget` SDK contract until its deprecation window ends. */ -export function resolveExplicitDeliveryTargetCompat(params: { - channel: string; - rawTarget?: string | null; - fallbackThreadId?: string | number | null; -}): ChannelRouteParsedTarget | null { - const channel = normalizeLowercaseStringOrEmpty(params.channel); - const rawTo = normalizeOptionalString(params.rawTarget); - if (!channel || !rawTo) { - return null; - } - const normalizedChannel = normalizeChannelId(channel) ?? channel; - // This deprecated hook belongs to the active plugin. Source-loading a bundled - // plugin here turns every target parse into broad runtime discovery. - const plugin = getLoadedChannelPluginForRead(normalizedChannel); - const parsed = plugin?.messaging?.parseExplicitTarget?.({ raw: rawTo }); - return { - channel, - rawTo, - to: parsed?.to ?? rawTo, - threadId: normalizeOptionalThreadValue(parsed?.threadId ?? params.fallbackThreadId), - chatType: parsed?.chatType, - }; -} diff --git a/src/channels/plugins/types.core.ts b/src/channels/plugins/types.core.ts index a97490ae17a8..fa2e50486d0c 100644 --- a/src/channels/plugins/types.core.ts +++ b/src/channels/plugins/types.core.ts @@ -574,16 +574,6 @@ export type ChannelMessagingAdapter = { id: string; threadId?: string | null; }) => string | undefined; - /** - * @deprecated Use `targetResolver` for target id normalization and - * `resolveOutboundSessionRoute` for session/thread identity. This remains for - * compatibility with older route parsing helpers. - */ - parseExplicitTarget?: (params: { raw: string }) => { - to: string; - threadId?: string | number; - chatType?: ChatType; - } | null; /** * Lightweight chat-type inference used before directory lookup so plugins can * steer peer-vs-group resolution without reimplementing host search flow. diff --git a/src/commands/doctor/shared/missing-configured-plugin-install.ids.ts b/src/commands/doctor/shared/missing-configured-plugin-install.ids.ts index b6f9c706bc23..27f0425f9f32 100644 --- a/src/commands/doctor/shared/missing-configured-plugin-install.ids.ts +++ b/src/commands/doctor/shared/missing-configured-plugin-install.ids.ts @@ -42,13 +42,11 @@ function addConfiguredMemoryEmbeddingProviderPluginIds( if (configuredProviderIds.size === 0) { return; } - for (const contract of ["embeddingProviders", "memoryEmbeddingProviders"] as const) { - for (const pluginId of resolveOfficialExternalProviderContractPluginIds({ - contract, - providerIds: configuredProviderIds, - })) { - ids.add(pluginId); - } + for (const pluginId of resolveOfficialExternalProviderContractPluginIds({ + contract: "embeddingProviders", + providerIds: configuredProviderIds, + })) { + ids.add(pluginId); } } diff --git a/src/commands/doctor/shared/startup-plugin-convergence-plan.ts b/src/commands/doctor/shared/startup-plugin-convergence-plan.ts index 074065c765d1..3a50270bda93 100644 --- a/src/commands/doctor/shared/startup-plugin-convergence-plan.ts +++ b/src/commands/doctor/shared/startup-plugin-convergence-plan.ts @@ -73,7 +73,7 @@ function hasConfiguredCapabilityPlugin(config: OpenClawConfig, env: NodeJS.Proce if (memoryEmbeddingProviderIds.size > 0) { if ( hasOfficialExternalContractTarget({ - contract: "memoryEmbeddingProviders", + contract: "embeddingProviders", providerIds: memoryEmbeddingProviderIds, }) ) { diff --git a/src/commands/status-json-payload.test.ts b/src/commands/status-json-payload.test.ts index 4be19e191143..a02777cc2cd8 100644 --- a/src/commands/status-json-payload.test.ts +++ b/src/commands/status-json-payload.test.ts @@ -86,8 +86,8 @@ describe("status-json-payload", () => { pluginCompatibility: [ { pluginId: "legacy", - code: "deprecated-memory-embedding-provider-api", - severity: "warn", + code: "hook-only", + severity: "info", message: "warn", }, ], @@ -129,8 +129,8 @@ describe("status-json-payload", () => { warnings: [ { pluginId: "legacy", - code: "deprecated-memory-embedding-provider-api", - severity: "warn", + code: "hook-only", + severity: "info", message: "warn", }, ], diff --git a/src/commands/status-json-runtime.test.ts b/src/commands/status-json-runtime.test.ts index b5c47265e4ac..f55c52d4804f 100644 --- a/src/commands/status-json-runtime.test.ts +++ b/src/commands/status-json-runtime.test.ts @@ -55,8 +55,8 @@ function createScan() { pluginCompatibility: [ { pluginId: "legacy", - code: "deprecated-memory-embedding-provider-api", - severity: "warn", + code: "hook-only", + severity: "info", message: "warn", }, ], @@ -122,8 +122,8 @@ describe("status-json-runtime", () => { expect(payloadInput.pluginCompatibility).toStrictEqual([ { pluginId: "legacy", - code: "deprecated-memory-embedding-provider-api", - severity: "warn", + code: "hook-only", + severity: "info", message: "warn", }, ]); diff --git a/src/commands/status.scan-result.test.ts b/src/commands/status.scan-result.test.ts index 22e413001d99..12acb88a4d28 100644 --- a/src/commands/status.scan-result.test.ts +++ b/src/commands/status.scan-result.test.ts @@ -89,9 +89,9 @@ describe("buildStatusScanResult", () => { const pluginCompatibility = [ { pluginId: "legacy", - code: "deprecated-memory-embedding-provider-api" as const, - compatCode: "deprecated-memory-embedding-provider-api" as const, - severity: "warn" as const, + code: "hook-only" as const, + compatCode: "hook-only-plugin-shape" as const, + severity: "info" as const, message: "warn", }, ]; diff --git a/src/cron/isolated-agent/delivery-target.test.ts b/src/cron/isolated-agent/delivery-target.test.ts index b0d5f508df07..51e0c8a7d812 100644 --- a/src/cron/isolated-agent/delivery-target.test.ts +++ b/src/cron/isolated-agent/delivery-target.test.ts @@ -653,57 +653,6 @@ describe("resolveDeliveryTarget", () => { expect(result.error.message).toContain("directory auth failed"); }); - it("keeps parser-derived explicit thread ids for parser-only cron targets", async () => { - setMainSessionEntry(undefined); - setSingleOutboundTestPlugin({ - id: "alpha", - outbound: createStubOutbound("Alpha"), - messaging: { - targetPrefixes: ["alpha"], - parseExplicitTarget: ({ raw }) => - raw === "alpha:room-a:topic:77" - ? { to: "room-a", threadId: 77, chatType: "group" as const } - : null, - }, - }); - - const result = await resolveDeliveryTarget(makeCfg({ bindings: [] }), AGENT_ID, { - channel: "alpha", - to: "alpha:room-a:topic:77", - }); - - expect(result.ok).toBe(true); - expect(result.to).toBe("room-a"); - expect(result.threadId).toBe(77); - }); - - it("does not treat parser-only target normalization as a parser thread id", async () => { - setLastSessionEntry({ - sessionId: "sess-parser-stale-thread", - lastChannel: "alpha", - lastTo: "room-a", - lastThreadId: "stale-thread", - }); - setSingleOutboundTestPlugin({ - id: "alpha", - outbound: createStubOutbound("Alpha"), - messaging: { - targetPrefixes: ["alpha"], - parseExplicitTarget: ({ raw }) => - raw === "alpha:room-b" ? { to: "room-b", chatType: "group" as const } : null, - }, - }); - - const result = await resolveDeliveryTarget(makeCfg({ bindings: [] }), AGENT_ID, { - channel: "alpha", - to: "alpha:room-b", - }); - - expect(result.ok).toBe(true); - expect(result.to).toBe("room-b"); - expect(result.threadId).toBeUndefined(); - }); - it("preserves plugin-canonical targets that begin with the selected channel prefix", async () => { setMainSessionEntry(undefined); const canonicalTarget = "Bncr:tgBot:-1003891624016:6278285192"; diff --git a/src/cron/isolated-agent/delivery-target.ts b/src/cron/isolated-agent/delivery-target.ts index 8458d34e2d9f..2c7c94fdd408 100644 --- a/src/cron/isolated-agent/delivery-target.ts +++ b/src/cron/isolated-agent/delivery-target.ts @@ -1,7 +1,5 @@ /** Resolves isolated cron delivery requests into concrete outbound targets. */ -import { normalizeOptionalThreadValue } from "@openclaw/normalization-core/string-coerce"; import { uniqueStrings } from "@openclaw/normalization-core/string-normalization"; -import { resolveExplicitDeliveryTargetCompat } from "../../channels/plugins/target-parsing-loaded.js"; import type { ChannelId } from "../../channels/plugins/types.public.js"; import { resolveAgentMainSessionKey } from "../../config/sessions/main-session.js"; import { resolveSessionStorePathCore } from "../../config/sessions/paths.js"; @@ -447,17 +445,7 @@ export async function resolveDeliveryTarget( })() : null; - const parserExplicitThreadId = - explicitThreadId == null && explicitTo - ? normalizeOptionalThreadValue( - resolveExplicitDeliveryTargetCompat({ - channel, - rawTarget: explicitTo, - })?.threadId, - ) - : undefined; - // Thread precedence is explicit config, route canonicalization, parser-derived - // explicit target, then same-peer session history. + // Thread precedence is explicit config, route canonicalization, then same-peer session history. const canUseSessionThread = options?.inheritSessionThread !== false && shouldCarrySessionThread({ @@ -467,10 +455,7 @@ export async function resolveDeliveryTarget( lastRoute, }); const threadId = - explicitThreadId ?? - route?.threadId ?? - parserExplicitThreadId ?? - (canUseSessionThread ? resolved.threadId : undefined); + explicitThreadId ?? route?.threadId ?? (canUseSessionThread ? resolved.threadId : undefined); return { ok: true, channel, diff --git a/src/gateway/embeddings-http.test.ts b/src/gateway/embeddings-http.test.ts index d8bc2f31f369..c34780d4ce2d 100644 --- a/src/gateway/embeddings-http.test.ts +++ b/src/gateway/embeddings-http.test.ts @@ -17,6 +17,7 @@ import type { MemoryEmbeddingProviderAdapter, MemoryEmbeddingProviderCallOptions, } from "../plugins/memory-embedding-providers.js"; +import { adaptMemoryEmbeddingProviderAdapter } from "../plugins/memory-embedding-providers.js"; import { createPluginRegistry } from "../plugins/registry.js"; import type { PluginRuntime } from "../plugins/runtime/types.js"; import { startOpenAiCompatGatewayServer } from "./openai-compatible-http.test-helpers.js"; @@ -57,11 +58,10 @@ let embedBatchMock: ReturnType< > >; let closeEmbeddingProviderMock: ReturnType Promise | void>>; -let clearMemoryEmbeddingProviders: typeof import("../plugins/memory-embedding-providers.js").clearMemoryEmbeddingProviders; -let registerMemoryEmbeddingProvider: typeof import("../plugins/memory-embedding-providers.js").registerMemoryEmbeddingProvider; let openAiAdapter: MemoryEmbeddingProviderAdapter; let drainRetainedOpenAiEmbeddingProviders: typeof import("./embeddings-http.js").drainRetainedOpenAiEmbeddingProviders; let clearEmbeddingProviders: typeof import("../plugins/embedding-providers.js").clearEmbeddingProviders; +let registerEmbeddingProvider: typeof import("../plugins/embedding-providers.js").registerEmbeddingProvider; let enabledServer: Awaited>; let genericEmbeddingServer: { baseUrl: string; close: () => Promise }; let enabledPort: number; @@ -132,9 +132,8 @@ async function startGenericEmbeddingServer(): Promise<{ beforeAll(async () => { ({ drainRetainedOpenAiEmbeddingProviders } = await import("./embeddings-http.js")); - ({ clearMemoryEmbeddingProviders, registerMemoryEmbeddingProvider } = - await import("../plugins/memory-embedding-providers.js")); - ({ clearEmbeddingProviders } = await import("../plugins/embedding-providers.js")); + ({ clearEmbeddingProviders, registerEmbeddingProvider } = + await import("../plugins/embedding-providers.js")); embedBatchMock = vi.fn(async (texts: string[]) => texts.map((_text, index) => [index + 0.1, index + 0.2]), ); @@ -193,11 +192,10 @@ beforeEach(() => { activateGlobalSideEffects: true, }); setTestPluginRegistry(builder.registry); - registerMemoryEmbeddingProvider(openAiAdapter); + registerEmbeddingProvider(adaptMemoryEmbeddingProviderAdapter(openAiAdapter)); }); afterEach(() => { - clearMemoryEmbeddingProviders(); clearEmbeddingProviders(); resetTestPluginRegistry(); }); diff --git a/src/gateway/server-startup-plugins.test.ts b/src/gateway/server-startup-plugins.test.ts index 39cccdc33590..f00da3ced48b 100644 --- a/src/gateway/server-startup-plugins.test.ts +++ b/src/gateway/server-startup-plugins.test.ts @@ -540,8 +540,9 @@ describe("loadGatewayStartupPluginRuntime", () => { describe("warnUnregisteredConfiguredMemoryEmbeddingProviders", () => { function registry(providerIds: string[], options: { embeddingProviderIds?: string[] } = {}) { return { - memoryEmbeddingProviders: providerIds.map((id) => ({ provider: { id } })), - embeddingProviders: (options.embeddingProviderIds ?? []).map((id) => ({ provider: { id } })), + embeddingProviders: [...providerIds, ...(options.embeddingProviderIds ?? [])].map((id) => ({ + provider: { id }, + })), } as never; } diff --git a/src/gateway/server-startup-plugins.ts b/src/gateway/server-startup-plugins.ts index 933026167537..e8028b977462 100644 --- a/src/gateway/server-startup-plugins.ts +++ b/src/gateway/server-startup-plugins.ts @@ -197,7 +197,7 @@ export async function prepareGatewayPluginBootstrap(params: { */ export function warnUnregisteredConfiguredMemoryEmbeddingProviders(params: { config: OpenClawConfig; - pluginRegistry: Partial>; + pluginRegistry: Partial>; log: Pick; }): void { const unregistered = collectUnregisteredConfiguredMemoryEmbeddingProviders({ diff --git a/src/infra/outbound/outbound-session.test-helpers.ts b/src/infra/outbound/outbound-session.test-helpers.ts index c382a7868195..ed404b00cc78 100644 --- a/src/infra/outbound/outbound-session.test-helpers.ts +++ b/src/infra/outbound/outbound-session.test-helpers.ts @@ -599,17 +599,6 @@ export function setMinimalOutboundSessionPluginRegistryForTests(): void { targetPrefixes: ["fallbackchat"], }, }, - { - ...createChannelTestPluginBase({ - id: "legacyparser", - label: "Legacy Parser", - capabilities: { chatTypes: ["direct", "group", "channel"] }, - }), - messaging: { - parseExplicitTarget: ({ raw }) => - raw === "team-ops" ? { to: raw, chatType: "group" } : null, - }, - }, ]; setActivePluginRegistry( createTestRegistry( diff --git a/src/infra/outbound/outbound-session.test.ts b/src/infra/outbound/outbound-session.test.ts index d897be9e1b30..46c8d588813e 100644 --- a/src/infra/outbound/outbound-session.test.ts +++ b/src/infra/outbound/outbound-session.test.ts @@ -436,18 +436,6 @@ describe("resolveOutboundSessionRoute", () => { chatType: "channel", }, }, - { - name: "Legacy parser-only plugin chat type fallback", - cfg: baseConfig, - channel: "legacyparser", - target: "team-ops", - expected: { - sessionKey: "agent:main:legacyparser:group:team-ops", - from: "legacyparser:group:team-ops", - to: "channel:team-ops", - chatType: "group", - }, - }, ] satisfies NamedRouteCase[])("$name", async ({ name: _name, ...params }) => { await expectResolvedRoute(params); }); diff --git a/src/infra/outbound/outbound-session.ts b/src/infra/outbound/outbound-session.ts index 9910c52f3416..909a2473ef79 100644 --- a/src/infra/outbound/outbound-session.ts +++ b/src/infra/outbound/outbound-session.ts @@ -90,20 +90,6 @@ function inferPeerKindFromPlugin(params: { return undefined; } -function inferPeerKindFromLegacyParser(params: { - plugin: ReturnType; - targets: readonly string[]; -}): ChatType | undefined { - for (const target of params.targets) { - const parsed = params.plugin?.messaging?.parseExplicitTarget?.({ raw: target }); - const inferred = normalizeInferredPeerKind(parsed?.chatType); - if (inferred) { - return inferred; - } - } - return undefined; -} - function inferPeerKindFromFallbackPrefixes(targets: readonly string[]): ChatType | undefined { for (const target of targets) { for (const fallback of FALLBACK_TARGET_KIND_PREFIXES) { @@ -158,7 +144,6 @@ function inferPeerKind(params: { const targets = uniqueStrings([params.target, strippedTarget].filter(Boolean)); return ( inferPeerKindFromPlugin({ plugin, targets }) ?? - inferPeerKindFromLegacyParser({ plugin, targets }) ?? inferPeerKindFromFallbackPrefixes(targets) ?? inferPeerKindFromCapabilities(plugin) ?? "direct" diff --git a/src/infra/outbound/targets-session.ts b/src/infra/outbound/targets-session.ts index 2f93f740b077..2f0c927e59ee 100644 --- a/src/infra/outbound/targets-session.ts +++ b/src/infra/outbound/targets-session.ts @@ -5,7 +5,6 @@ import { normalizeOptionalString, normalizeOptionalThreadValue, } from "@openclaw/normalization-core/string-coerce"; -import { resolveExplicitDeliveryTargetCompat } from "../../channels/plugins/target-parsing-loaded.js"; import type { ChannelOutboundTargetMode } from "../../channels/plugins/types.public.js"; import type { SessionEntry } from "../../config/sessions.js"; import { channelRouteTargetsShareConversation } from "../../plugin-sdk/channel-route.js"; @@ -32,7 +31,7 @@ export type SessionDeliveryTarget = { lastThreadId?: string | number; }; -function resolveParsedRouteTarget(params: { +function resolveRouteTarget(params: { channel: string; accountId?: string; rawTarget?: string | null; @@ -43,19 +42,13 @@ function resolveParsedRouteTarget(params: { if (!channel || !rawTo) { return null; } - const parsed = resolveExplicitDeliveryTargetCompat({ - channel, - rawTarget: rawTo, - fallbackThreadId: params.fallbackThreadId, - }); - const threadId = normalizeOptionalThreadValue(parsed?.threadId ?? params.fallbackThreadId); + const threadId = normalizeOptionalThreadValue(params.fallbackThreadId); return { channel, accountId: params.accountId, rawTo, - to: parsed?.to ?? rawTo, + to: rawTo, ...(threadId != null ? { threadId } : {}), - chatType: parsed?.chatType, }; } @@ -85,7 +78,7 @@ export function resolveSessionDeliveryTarget(params: { const sessionLastChannel = context?.channel && isNormalizedMessageChannel(context.channel) ? context.channel : undefined; const parsedSessionTarget = sessionLastChannel - ? resolveParsedRouteTarget({ + ? resolveRouteTarget({ channel: sessionLastChannel, accountId: context?.accountId, rawTarget: context?.to, @@ -96,7 +89,7 @@ export function resolveSessionDeliveryTarget(params: { const hasTurnSourceChannel = params.turnSourceChannel != null; const parsedTurnSourceTarget = hasTurnSourceChannel && params.turnSourceChannel - ? resolveParsedRouteTarget({ + ? resolveRouteTarget({ channel: params.turnSourceChannel, accountId: params.turnSourceAccountId, rawTarget: params.turnSourceTo, @@ -152,17 +145,17 @@ export function resolveSessionDeliveryTarget(params: { channel = params.fallbackChannel; } - const parsedExplicitTarget = + const explicitTarget = channel && rawExplicitTo - ? resolveExplicitDeliveryTargetCompat({ + ? resolveRouteTarget({ channel, rawTarget: rawExplicitTo, fallbackThreadId: params.explicitThreadId, }) : null; - const explicitTo = parsedExplicitTarget?.to ?? rawExplicitTo; + const explicitTo = explicitTarget?.to ?? rawExplicitTo; const explicitThreadId = normalizeOptionalThreadValue( - parsedExplicitTarget?.threadId ?? params.explicitThreadId, + explicitTarget?.threadId ?? params.explicitThreadId, ); const explicitThreadIdSource = explicitThreadId != null ? "explicit" : undefined; diff --git a/src/infra/outbound/targets.test.ts b/src/infra/outbound/targets.test.ts index c5e0a2da40be..cd11e9abaf8a 100644 --- a/src/infra/outbound/targets.test.ts +++ b/src/infra/outbound/targets.test.ts @@ -310,63 +310,6 @@ describe("resolveSessionDeliveryTarget", () => { }); }); - it("keeps parser-only explicit target compatibility during the migration window", () => { - const alpha = createGenericTargetTestPlugin("alpha", "Alpha"); - setActivePluginRegistry( - createTargetsTestRegistry([ - { - ...alpha, - messaging: { - targetPrefixes: ["alpha"], - parseExplicitTarget: ({ raw }) => - raw === "alpha:room-a:topic:77" - ? { to: "room-a", threadId: 77, chatType: "group" as const } - : null, - }, - }, - ]), - ); - - const resolved = resolveSessionDeliveryTarget({ - requestedChannel: "alpha", - explicitTo: "alpha:room-a:topic:77", - }); - - expect(resolved.to).toBe("room-a"); - expect(resolved.threadId).toBe(77); - }); - - it("keeps parser-only session target thread compatibility during the migration window", () => { - const alpha = createGenericTargetTestPlugin("alpha", "Alpha"); - setActivePluginRegistry( - createTargetsTestRegistry([ - { - ...alpha, - messaging: { - targetPrefixes: ["alpha"], - parseExplicitTarget: ({ raw }) => - raw === "alpha:room-a:topic:77" - ? { to: "room-a", threadId: 77, chatType: "group" as const } - : null, - }, - }, - ]), - ); - - const resolved = resolveSessionDeliveryTarget({ - entry: { - sessionId: "sess-parser", - updatedAt: 1, - lastChannel: "alpha", - lastTo: "alpha:room-a:topic:77", - }, - requestedChannel: "last", - }); - - expect(resolved.to).toBe("room-a"); - expect(resolved.threadId).toBe(77); - }); - it("uses an explicit provider-prefixed target before last-session channel fallback", () => { const resolved = resolveSessionDeliveryTarget({ entry: { diff --git a/src/plugin-sdk/agent-harness-runtime.ts b/src/plugin-sdk/agent-harness-runtime.ts index d14e042b3cab..8ed79d704951 100644 --- a/src/plugin-sdk/agent-harness-runtime.ts +++ b/src/plugin-sdk/agent-harness-runtime.ts @@ -137,16 +137,8 @@ export type { ContextEngineOperation, ContextEngineProjection, } from "../context-engine/types.js"; -export type { - CompactEmbeddedAgentSessionParams, - /** @deprecated Use CompactEmbeddedAgentSessionParams. */ - CompactEmbeddedAgentSessionParams as CompactEmbeddedPiSessionParams, -} from "../agents/embedded-agent-runner/compact.js"; -export type { - EmbeddedAgentCompactResult, - /** @deprecated Use EmbeddedAgentCompactResult. */ - EmbeddedAgentCompactResult as EmbeddedPiCompactResult, -} from "../agents/embedded-agent-runner/types.js"; +export type { CompactEmbeddedAgentSessionParams } from "../agents/embedded-agent-runner/compact.js"; +export type { EmbeddedAgentCompactResult } from "../agents/embedded-agent-runner/types.js"; export type { AnyAgentTool } from "../agents/tools/common.js"; export type { MessagingToolSend, @@ -194,11 +186,7 @@ export { resolveAgentRunAbortLifecycleFields } from "../agents/run-termination.j export { isHostScopedAgentToolActive } from "../agents/agent-tools.ring-zero-context.js"; export { log as embeddedAgentLog } from "../agents/embedded-agent-runner/logger.js"; export { buildAgentRuntimePlan } from "../agents/runtime-plan/build.js"; -export { - classifyEmbeddedAgentRunResultForModelFallback, - /** @deprecated Use classifyEmbeddedAgentRunResultForModelFallback. */ - classifyEmbeddedAgentRunResultForModelFallback as classifyEmbeddedPiRunResultForModelFallback, -} from "../agents/embedded-agent-runner/result-fallback-classifier.js"; +export { classifyEmbeddedAgentRunResultForModelFallback } from "../agents/embedded-agent-runner/result-fallback-classifier.js"; export { resolveUserPath } from "../utils.js"; export { callGatewayTool } from "../agents/tools/gateway.js"; export type { NodeListNode } from "../agents/tools/nodes-utils.js"; diff --git a/src/plugin-sdk/bundled-channel-config-schema.ts b/src/plugin-sdk/bundled-channel-config-schema.ts index dc4907f27f87..6fae770182dc 100644 --- a/src/plugin-sdk/bundled-channel-config-schema.ts +++ b/src/plugin-sdk/bundled-channel-config-schema.ts @@ -6,7 +6,7 @@ * bundled channel schemas. Internal callers use this subpath only for the * bundled provider schemas; generic primitives come from channel-config-schema. */ -import { z, type ZodObject, type ZodOptional, type ZodType } from "zod"; +import type { ZodObject, ZodOptional, ZodType } from "zod"; import type { OpenClawConfig } from "./config-contracts.js"; import { createLazyFacadeObjectValue, @@ -33,23 +33,6 @@ export { ToolPolicySchema, } from "./channel-config-schema.js"; -function createLegacyExternalChannelConfigSchema() { - return z.object({}).passthrough(); -} - -/** - * @deprecated Compatibility for external channel packages published through 2026.7.1. - * Their package manifests remain the validation owner. Remove after the minimum supported - * Slack, Discord, Signal, and Teams packages use plugin-owned config schemas. - */ -export const SlackConfigSchema = createLegacyExternalChannelConfigSchema(); -/** @deprecated See SlackConfigSchema. */ -export const DiscordConfigSchema = createLegacyExternalChannelConfigSchema(); -/** @deprecated See SlackConfigSchema. */ -export const SignalConfigSchema = createLegacyExternalChannelConfigSchema(); -/** @deprecated See SlackConfigSchema. */ -export const MSTeamsConfigSchema = createLegacyExternalChannelConfigSchema(); - type ChannelConfig = NonNullable; type ConfigSchemaShape = { -readonly [K in keyof TOutput]-?: Pick extends Required> diff --git a/src/plugin-sdk/channel-route.test.ts b/src/plugin-sdk/channel-route.test.ts index c40a05d161d0..c3eac219af85 100644 --- a/src/plugin-sdk/channel-route.test.ts +++ b/src/plugin-sdk/channel-route.test.ts @@ -10,7 +10,6 @@ import { channelRoutesMatchExact, channelRoutesShareConversation, normalizeChannelRouteRef, - resolveChannelRouteTargetWithParser, stringifyRouteThreadId, } from "./channel-route.js"; @@ -169,26 +168,4 @@ describe("plugin-sdk channel-route", () => { }), ).toBe(false); }); - - it("keeps deprecated parser wrapper wired for public SDK compatibility", () => { - expect( - resolveChannelRouteTargetWithParser({ - channel: "Mock", - rawTarget: " room-a:topic:77 ", - fallbackThreadId: 11, - parseExplicitTarget: (_channel, rawTarget) => { - const match = /^(.*):topic:(\d+)$/u.exec(rawTarget); - return match - ? { to: match[1] ?? rawTarget, threadId: Number.parseInt(match[2] ?? "", 10) } - : null; - }, - }), - ).toEqual({ - channel: "mock", - rawTo: "room-a:topic:77", - to: "room-a", - threadId: 77, - chatType: undefined, - }); - }); }); diff --git a/src/plugin-sdk/channel-route.ts b/src/plugin-sdk/channel-route.ts index 67967d40d6d5..43bf9bdcb91d 100644 --- a/src/plugin-sdk/channel-route.ts +++ b/src/plugin-sdk/channel-route.ts @@ -68,22 +68,6 @@ export type ChannelRouteTargetInput = Pick< /** Route input accepted by compact-key helpers after legacy and normalized callers converge. */ export type ChannelRouteKeyInput = ChannelRouteRef | ChannelRouteTargetInput; -/** @deprecated Use `messaging.resolveOutboundSessionRoute` for provider-specific target grammar. */ -export type ChannelRouteExplicitTarget = { - /** Canonical destination id parsed from the provider-specific target string. */ - to: string; - /** Optional provider thread/topic/root id parsed from the target string. */ - threadId?: string | number; - /** Coarse destination shape parsed from the target string. */ - chatType?: ChannelRouteChatType; -}; - -/** @deprecated Use `messaging.resolveOutboundSessionRoute` for provider-specific target grammar. */ -export type ChannelRouteExplicitTargetParser = ( - channel: string, - rawTarget: string, -) => ChannelRouteExplicitTarget | null; - /** Normalizes a route thread id while preserving provider string ids. */ export function normalizeRouteThreadId(value: unknown): string | number | undefined { return normalizeOptionalThreadValue(value); @@ -152,47 +136,6 @@ export function normalizeChannelRouteTarget( return input ? normalizeChannelRouteRef(input) : undefined; } -/** Parsed target shape retained for deprecated explicit-target parser adapters. */ -export type ChannelRouteParsedTarget = ChannelRouteTargetInput & { - /** Normalized lowercase channel id. */ - channel: string; - /** Trimmed provider-specific target text originally supplied by the caller. */ - rawTo: string; - /** Canonical destination id used by route equality and delivery. */ - to: string; - /** Optional thread/topic/root id from the parser or fallback value. */ - threadId?: string | number; - /** Coarse destination shape parsed from the provider-specific target. */ - chatType?: ChannelRouteChatType; -}; - -/** @deprecated Use `messaging.resolveOutboundSessionRoute` for provider-specific target grammar. */ -export function resolveChannelRouteTargetWithParser(params: { - /** Channel id used for normalization and parser dispatch. */ - channel: string; - /** Provider-specific target text to parse. */ - rawTarget?: string | null; - /** Thread id to use when the parsed target omits one. */ - fallbackThreadId?: string | number | null; - /** Legacy parser that understands the channel's explicit-target grammar. */ - parseExplicitTarget: ChannelRouteExplicitTargetParser; -}): ChannelRouteParsedTarget | null { - const channel = normalizeLowercaseStringOrEmpty(params.channel); - const rawTo = normalizeOptionalString(params.rawTarget); - if (!channel || !rawTo) { - return null; - } - const parsed = params.parseExplicitTarget(channel, rawTo); - const fallbackThreadId = normalizeOptionalThreadValue(params.fallbackThreadId); - return { - channel, - rawTo, - to: parsed?.to ?? rawTo, - threadId: normalizeOptionalThreadValue(parsed?.threadId ?? fallbackThreadId), - chatType: parsed?.chatType, - }; -} - /** Builds a JSON route dedupe key that remains unambiguous when route parts contain separators. */ export function channelRouteDedupeKey(input?: ChannelRouteTargetInput | null): string { const route = normalizeChannelRouteTarget(input); diff --git a/src/plugin-sdk/memory-core-host-engine-embeddings.ts b/src/plugin-sdk/memory-core-host-engine-embeddings.ts index 2614c60108ea..5dd6435c1b61 100644 --- a/src/plugin-sdk/memory-core-host-engine-embeddings.ts +++ b/src/plugin-sdk/memory-core-host-engine-embeddings.ts @@ -67,16 +67,9 @@ export { getMemoryEmbeddingProvider, listMemoryEmbeddingProviders, listRegisteredMemoryEmbeddingProviderAdapters, - listRegisteredMemoryEmbeddingProviders, } from "../plugins/memory-embedding-provider-runtime.js"; export { registerRuntimeAuthProfileStoreMutationListener } from "../agents/auth-profiles/runtime-snapshots.js"; -export { clearMemoryEmbeddingProviders } from "../plugins/memory-embedding-providers.js"; -/** - * @deprecated New embedding providers should use `api.registerEmbeddingProvider(...)` - * and `contracts.embeddingProviders`. This memory-specific registrar remains - * available only for compatibility while existing providers migrate. - */ -export { registerMemoryEmbeddingProvider } from "../plugins/memory-embedding-providers.js"; +export { adaptMemoryEmbeddingProviderAdapter } from "../plugins/memory-embedding-providers.js"; export type { MemoryEmbeddingBatchChunk, MemoryEmbeddingBatchOptions, diff --git a/src/plugin-sdk/messaging-targets.ts b/src/plugin-sdk/messaging-targets.ts deleted file mode 100644 index 699a1a21d214..000000000000 --- a/src/plugin-sdk/messaging-targets.ts +++ /dev/null @@ -1,15 +0,0 @@ -/** @deprecated Use `openclaw/plugin-sdk/channel-targets`. */ -export { - buildMessagingTarget, - ensureTargetId, - normalizeTargetId, - parseAtUserTarget, - parseMentionPrefixOrAtUserTarget, - parseTargetMention, - parseTargetPrefix, - parseTargetPrefixes, - requireTargetKind, - type MessagingTarget, - type MessagingTargetKind, - type MessagingTargetParseOptions, -} from "../channels/targets.js"; diff --git a/src/plugin-sdk/plugin-test-api.ts b/src/plugin-sdk/plugin-test-api.ts index 1601cfecabef..c4b0ce09cfbb 100644 --- a/src/plugin-sdk/plugin-test-api.ts +++ b/src/plugin-sdk/plugin-test-api.ts @@ -87,7 +87,6 @@ export function createTestPluginApi(api: TestPluginApiInput = {}): OpenClawPlugi registerMemoryPromptSupplement() {}, registerMemoryPromptPreparation() {}, registerMemoryCorpusSupplement() {}, - registerMemoryEmbeddingProvider() {}, resolvePath(input: string) { return input; }, diff --git a/src/plugin-sdk/plugin-test-runtime.ts b/src/plugin-sdk/plugin-test-runtime.ts index e050c130de83..e261fd465e42 100644 --- a/src/plugin-sdk/plugin-test-runtime.ts +++ b/src/plugin-sdk/plugin-test-runtime.ts @@ -101,12 +101,6 @@ export { restoreRegisteredEmbeddingProviders, type RegisteredEmbeddingProvider, } from "../plugins/embedding-providers.js"; -export { - clearMemoryEmbeddingProviders, - listRegisteredMemoryEmbeddingProviders, - restoreRegisteredMemoryEmbeddingProviders, - type RegisteredMemoryEmbeddingProvider, -} from "../plugins/memory-embedding-providers.js"; export type { PluginRuntime } from "../plugins/runtime/types.js"; export type { PluginHookRegistration } from "../plugins/hook-types.js"; export type { RuntimeEnv } from "../runtime.js"; diff --git a/src/plugin-sdk/setup-runtime.ts b/src/plugin-sdk/setup-runtime.ts index d3fafc99b6a3..31a6c825859d 100644 --- a/src/plugin-sdk/setup-runtime.ts +++ b/src/plugin-sdk/setup-runtime.ts @@ -43,10 +43,6 @@ export { splitSetupEntries, } from "../channels/plugins/setup-wizard-helpers.js"; -export { - createLegacyCompatChannelDmPolicy, - promptLegacyChannelAllowFromForAccount, -} from "../channels/plugins/setup-wizard-legacy-compat.js"; export { createAllowlistSetupWizardProxy } from "../channels/plugins/setup-wizard-proxy.js"; export { createCliPathTextInput, diff --git a/src/plugin-sdk/shipped-channel-compat.test.ts b/src/plugin-sdk/shipped-channel-compat.test.ts deleted file mode 100644 index d745c99f767b..000000000000 --- a/src/plugin-sdk/shipped-channel-compat.test.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { - DiscordConfigSchema, - MSTeamsConfigSchema, - SignalConfigSchema, - SlackConfigSchema, -} from "./bundled-channel-config-schema.js"; -import { - createLegacyCompatChannelDmPolicy, - promptLegacyChannelAllowFromForAccount, -} from "./setup-runtime.js"; - -describe("shipped external channel compatibility", () => { - it("retains named config schema exports used by published channel packages", () => { - for (const schema of [ - SlackConfigSchema, - DiscordConfigSchema, - SignalConfigSchema, - MSTeamsConfigSchema, - ]) { - expect(schema.safeParse({ legacySetting: true })).toMatchObject({ success: true }); - expect(schema.toJSONSchema({ target: "draft-07" })).toMatchObject({ type: "object" }); - } - }); - - it("retains setup helpers used by published Slack and Discord packages", () => { - expect(createLegacyCompatChannelDmPolicy).toBeTypeOf("function"); - expect(promptLegacyChannelAllowFromForAccount).toBeTypeOf("function"); - }); -}); diff --git a/src/plugin-sdk/test-helpers/plugin-runtime-mock.ts b/src/plugin-sdk/test-helpers/plugin-runtime-mock.ts index 90af56a0035d..a04d99845991 100644 --- a/src/plugin-sdk/test-helpers/plugin-runtime-mock.ts +++ b/src/plugin-sdk/test-helpers/plugin-runtime-mock.ts @@ -547,7 +547,6 @@ export function createPluginRuntimeMock(overrides: DeepPartial = ], })) as unknown as PluginRuntime["agent"]["resolveThinkingPolicy"], runEmbeddedAgent: runEmbeddedAgentMock, - runEmbeddedPiAgent: runEmbeddedAgentMock, resolveAgentTimeoutMs: vi.fn( () => 30_000, ) as unknown as PluginRuntime["agent"]["resolveAgentTimeoutMs"], diff --git a/src/plugin-state/plugin-state-store.runtime.test.ts b/src/plugin-state/plugin-state-store.runtime.test.ts index 8cae4f33e850..24bcb09bb29f 100644 --- a/src/plugin-state/plugin-state-store.runtime.test.ts +++ b/src/plugin-state/plugin-state-store.runtime.test.ts @@ -40,7 +40,6 @@ function createPluginRecord( webFetchProviderIds: [], webSearchProviderIds: [], migrationProviderIds: [], - memoryEmbeddingProviderIds: [], agentHarnessIds: [], cliCommands: [], services: [], diff --git a/src/plugins/api-builder.ts b/src/plugins/api-builder.ts index d60168d087ec..b82ed42be42a 100644 --- a/src/plugins/api-builder.ts +++ b/src/plugins/api-builder.ts @@ -83,7 +83,6 @@ type BuildPluginApiParams = { | "registerMemoryPromptSupplement" | "registerMemoryPromptPreparation" | "registerMemoryCorpusSupplement" - | "registerMemoryEmbeddingProvider" | "on" > >; @@ -180,8 +179,6 @@ const noopRegisterMemoryPromptPreparation: OpenClawPluginApi["registerMemoryProm () => {}; const noopRegisterMemoryCorpusSupplement: OpenClawPluginApi["registerMemoryCorpusSupplement"] = () => {}; -const noopRegisterMemoryEmbeddingProvider: OpenClawPluginApi["registerMemoryEmbeddingProvider"] = - () => {}; const noopOn: OpenClawPluginApi["on"] = () => {}; export function buildPluginApi(params: BuildPluginApiParams): OpenClawPluginApi { @@ -292,8 +289,6 @@ export function buildPluginApi(params: BuildPluginApiParams): OpenClawPluginApi handlers.registerMemoryPromptPreparation ?? noopRegisterMemoryPromptPreparation, registerMemoryCorpusSupplement: handlers.registerMemoryCorpusSupplement ?? noopRegisterMemoryCorpusSupplement, - registerMemoryEmbeddingProvider: - handlers.registerMemoryEmbeddingProvider ?? noopRegisterMemoryEmbeddingProvider, resolvePath: params.resolvePath, on: handlers.on ?? noopOn, }; diff --git a/src/plugins/capability-provider-runtime.test.ts b/src/plugins/capability-provider-runtime.test.ts index a8c3e629b725..b3debb92a0b4 100644 --- a/src/plugins/capability-provider-runtime.test.ts +++ b/src/plugins/capability-provider-runtime.test.ts @@ -18,7 +18,6 @@ const mocks = vi.hoisted(() => ({ plugins: [], diagnostics: [], embeddingProviders: [], - memoryEmbeddingProviders: [], speechProviders: [], realtimeTranscriptionProviders: [], realtimeVoiceProviders: [], @@ -143,7 +142,7 @@ function expectNoResolvedCapabilityProviders(providers: Array<{ id: string }>) { type CapabilityFixtureRegistry = ReturnType; type CapabilityFixtureKey = - | "memoryEmbeddingProviders" + | "embeddingProviders" | "speechProviders" | "realtimeTranscriptionProviders" | "realtimeVoiceProviders" @@ -327,7 +326,7 @@ function setBundledCapabilityFixture( function expectCompatChainApplied(params: { key: - | "memoryEmbeddingProviders" + | "embeddingProviders" | "speechProviders" | "realtimeTranscriptionProviders" | "realtimeVoiceProviders" @@ -1569,7 +1568,7 @@ describe("resolvePluginCapabilityProviders", () => { }); it.each([ - ["memoryEmbeddingProviders", "memoryEmbeddingProviders"], + ["embeddingProviders", "embeddingProviders"], ["speechProviders", "speechProviders"], ["realtimeTranscriptionProviders", "realtimeTranscriptionProviders"], ["realtimeVoiceProviders", "realtimeVoiceProviders"], @@ -1848,7 +1847,7 @@ describe("resolvePluginCapabilityProviders", () => { }, }; const loaded = createEmptyPluginRegistry(); - loaded.memoryEmbeddingProviders.push({ + loaded.embeddingProviders.push({ pluginId: "google", pluginName: "google", source: "test", @@ -1862,12 +1861,12 @@ describe("resolvePluginCapabilityProviders", () => { { id: "google", origin: "bundled", - contracts: { memoryEmbeddingProviders: ["gemini"] }, + contracts: { embeddingProviders: ["gemini"] }, }, { id: "openai", origin: "bundled", - contracts: { memoryEmbeddingProviders: ["openai"] }, + contracts: { embeddingProviders: ["openai"] }, }, ] as never, diagnostics: [], @@ -1878,7 +1877,7 @@ describe("resolvePluginCapabilityProviders", () => { ); const provider = resolvePluginCapabilityProvider({ - key: "memoryEmbeddingProviders", + key: "embeddingProviders", providerId: "gemini", cfg, }); @@ -1894,7 +1893,7 @@ describe("resolvePluginCapabilityProviders", () => { it("does not load targeted non-speech capability providers when plugins are globally disabled", () => { const cfg = { plugins: { enabled: false, allow: ["custom-plugin"] } } as OpenClawConfig; const loaded = createEmptyPluginRegistry(); - loaded.memoryEmbeddingProviders.push({ + loaded.embeddingProviders.push({ pluginId: "google", pluginName: "google", source: "test", @@ -1908,12 +1907,12 @@ describe("resolvePluginCapabilityProviders", () => { { id: "google", origin: "bundled", - contracts: { memoryEmbeddingProviders: ["gemini"] }, + contracts: { embeddingProviders: ["gemini"] }, }, { id: "openai", origin: "bundled", - contracts: { memoryEmbeddingProviders: ["openai"] }, + contracts: { embeddingProviders: ["openai"] }, }, ] as never, diagnostics: [], @@ -1923,7 +1922,7 @@ describe("resolvePluginCapabilityProviders", () => { ); const provider = resolvePluginCapabilityProvider({ - key: "memoryEmbeddingProviders", + key: "embeddingProviders", providerId: "gemini", cfg, }); diff --git a/src/plugins/capability-provider-runtime.ts b/src/plugins/capability-provider-runtime.ts index 6e2ac2905c48..c360a7d20c82 100644 --- a/src/plugins/capability-provider-runtime.ts +++ b/src/plugins/capability-provider-runtime.ts @@ -21,7 +21,6 @@ import type { PluginRegistry } from "./registry-types.js"; type CapabilityProviderRegistryKey = | "embeddingProviders" - | "memoryEmbeddingProviders" | "speechProviders" | "realtimeTranscriptionProviders" | "realtimeVoiceProviders" @@ -33,7 +32,6 @@ type CapabilityProviderRegistryKey = type CapabilityContractKey = | "embeddingProviders" - | "memoryEmbeddingProviders" | "speechProviders" | "realtimeTranscriptionProviders" | "realtimeVoiceProviders" @@ -52,7 +50,6 @@ type CapabilityPluginResolution = { const CAPABILITY_CONTRACT_KEY: Record = { embeddingProviders: "embeddingProviders", - memoryEmbeddingProviders: "memoryEmbeddingProviders", speechProviders: "speechProviders", realtimeTranscriptionProviders: "realtimeTranscriptionProviders", realtimeVoiceProviders: "realtimeVoiceProviders", @@ -574,7 +571,7 @@ export function resolvePluginCapabilityProviders 0 && params.key !== "memoryEmbeddingProviders") { + if (activeProviders.length > 0) { if (!missingRequestedProviders && !shouldMergeManifestProvidersWhenActive(params.key)) { return activeProviders.map((entry) => entry.provider) as CapabilityProviderFor[]; } @@ -625,25 +622,22 @@ export function resolvePluginCapabilityProviders 0 && missingRequestedProviders ? filterLoadedProvidersForRequestedConfig({ key: params.key, - requested: requestedProviderFilter, - entries: loadedProviders, + requested: missingRequestedProviders, + entries: requestedLoadedProviders, }) - : loadedProviders; - const mergeLoadedProviders = - activeProviders.length > 0 && missingRequestedProviders - ? filterLoadedProvidersForRequestedConfig({ - key: params.key, - requested: missingRequestedProviders, - entries: requestedLoadedProviders, - }) - : requestedLoadedProviders; - return mergeCapabilityProviders(activeProviders, mergeLoadedProviders); - } - return mergeCapabilityProviders(activeProviders, loadedProviders); + : requestedLoadedProviders; + return mergeCapabilityProviders(activeProviders, mergeLoadedProviders); } export function prepareMediaCapabilityProviders(params: { diff --git a/src/plugins/captured-registration.test.ts b/src/plugins/captured-registration.test.ts index 3fceb8ae88f8..f02daa4b9d16 100644 --- a/src/plugins/captured-registration.test.ts +++ b/src/plugins/captured-registration.test.ts @@ -154,7 +154,6 @@ describe("captured plugin registration", () => { expect(captured.textTransforms[0]?.input).toHaveLength(1); expect(captured.agentToolResultMiddlewares).toHaveLength(1); expect(captured.agentToolResultMiddlewares[0]?.runtimes).toEqual(["codex"]); - expect(captured.api.registerMemoryEmbeddingProvider).toBeTypeOf("function"); }); it("enforces captured middleware runtime and tool scopes", async () => { diff --git a/src/plugins/captured-registration.ts b/src/plugins/captured-registration.ts index 6df303795fcf..90c9845ac8ab 100644 --- a/src/plugins/captured-registration.ts +++ b/src/plugins/captured-registration.ts @@ -22,7 +22,6 @@ import type { PluginToolMetadataRegistration, PluginTrustedToolPolicyRegistration, } from "./host-hooks.js"; -import type { MemoryEmbeddingProviderAdapter } from "./memory-embedding-providers.js"; import type { PluginAgentToolResultMiddlewareRegistration } from "./registry-types.js"; import type { PluginRuntime } from "./runtime/types.js"; import type { SessionCatalogProvider } from "./session-catalog.js"; @@ -80,7 +79,6 @@ export type CapturedPluginRegistration = { webSearchProviders: WebSearchProviderPlugin[]; workerProviders: WorkerProvider[]; migrationProviders: MigrationProviderPlugin[]; - memoryEmbeddingProviders: MemoryEmbeddingProviderAdapter[]; sessionExtensions: PluginSessionExtensionRegistration[]; trustedToolPolicies: PluginTrustedToolPolicyRegistration[]; toolMetadata: PluginToolMetadataRegistration[]; @@ -121,7 +119,6 @@ export function createCapturedPluginRegistration(params?: { const webSearchProviders: WebSearchProviderPlugin[] = []; const workerProviders: WorkerProvider[] = []; const migrationProviders: MigrationProviderPlugin[] = []; - const memoryEmbeddingProviders: MemoryEmbeddingProviderAdapter[] = []; const sessionExtensions: PluginSessionExtensionRegistration[] = []; const trustedToolPolicies: PluginTrustedToolPolicyRegistration[] = []; const toolMetadata: PluginToolMetadataRegistration[] = []; @@ -165,7 +162,6 @@ export function createCapturedPluginRegistration(params?: { webSearchProviders, workerProviders, migrationProviders, - memoryEmbeddingProviders, sessionExtensions, trustedToolPolicies, toolMetadata, @@ -314,9 +310,6 @@ export function createCapturedPluginRegistration(params?: { registerMigrationProvider(provider: MigrationProviderPlugin) { migrationProviders.push(provider); }, - registerMemoryEmbeddingProvider(adapter: MemoryEmbeddingProviderAdapter) { - memoryEmbeddingProviders.push(adapter); - }, registerSessionExtension(extension: PluginSessionExtensionRegistration) { sessionExtensions.push(extension); }, diff --git a/src/plugins/channel-plugin-ids.test.ts b/src/plugins/channel-plugin-ids.test.ts index ab15e77d868f..4cfe11ca1f1e 100644 --- a/src/plugins/channel-plugin-ids.test.ts +++ b/src/plugins/channel-plugin-ids.test.ts @@ -141,14 +141,14 @@ function createManifestRegistryFixture(): PluginManifestRegistry { realtimeVoiceProviders: ["openai"], imageGenerationProviders: ["openai"], videoGenerationProviders: ["openai"], - memoryEmbeddingProviders: ["openai"], + embeddingProviders: ["openai"], }, }, { id: "ollama", enabledByDefault: true, providers: ["ollama"], - contracts: { memoryEmbeddingProviders: ["ollama"] }, + contracts: { embeddingProviders: ["ollama"] }, }, { id: "generic-embedding", diff --git a/src/plugins/compat/registry-records.ts b/src/plugins/compat/registry-records.ts index ee6fc4b1770a..a36f9968671e 100644 --- a/src/plugins/compat/registry-records.ts +++ b/src/plugins/compat/registry-records.ts @@ -57,12 +57,9 @@ export const PLUGIN_COMPAT_RECORDS = [ }, { code: "legacy-subagent-spawning-hook", - status: "deprecated", + status: "removed", owner: "sdk", introduced: "2026-05-30", - deprecated: "2026-05-30", - warningStarts: "2026-05-30", - removeAfter: "2026-08-30", replacement: "`subagent_spawned` for post-launch observation; core session-binding adapters for thread routing", docsPath: "/plugins/hooks#upcoming-deprecations", @@ -72,10 +69,10 @@ export const PLUGIN_COMPAT_RECORDS = [ "PluginHookSubagentSpawningResult", "SubagentLifecycleHookRunner.runSubagentSpawning", ], - diagnostics: ["plugin runtime compatibility warning"], - tests: ["src/plugins/loader.test.ts", "src/plugins/compat/registry.test.ts"], + diagnostics: ["plugin compatibility registry and migration guide"], + tests: ["src/plugins/compat/registry.test.ts"], releaseNote: - '`api.on("subagent_spawning", ...)` remains wired only for older plugins; core now owns thread-bound subagent routing.', + '`api.on("subagent_spawning", ...)` was removed; core now owns thread-bound subagent routing, and `subagent_spawned` remains available for observation.', }, { code: "hook-only-plugin-shape", @@ -90,28 +87,21 @@ export const PLUGIN_COMPAT_RECORDS = [ }, { code: "deprecated-memory-embedding-provider-api", - status: "deprecated", + status: "removed", owner: "sdk", introduced: "2026-05-21", - deprecated: "2026-05-21", - warningStarts: "2026-05-21", - removeAfter: "2026-08-21", replacement: "`api.registerEmbeddingProvider(...)` and `contracts.embeddingProviders`", docsPath: "/plugins/sdk-migration#memory-embedding-provider-api", surfaces: [ "api.registerMemoryEmbeddingProvider(...)", "contracts.memoryEmbeddingProviders", "openclaw/plugin-sdk/memory-core-host-engine-embeddings registerMemoryEmbeddingProvider", - "plugins inspect compatibility notices", - ], - diagnostics: ["plugin compatibility notice", "plugin SDK package guardrail"], - tests: [ - "src/plugins/status.test.ts", - "src/plugins/compat/registry.test.ts", - "src/plugins/contracts/plugin-sdk-package-contract-guardrails.test.ts", + "plugin compatibility registry and migration guide", ], + diagnostics: ["plugin compatibility registry and migration guide"], + tests: ["src/plugins/compat/registry.test.ts"], releaseNote: - "Memory-specific embedding provider registration remains wired as a deprecated compatibility path while providers migrate to the generic embedding provider contract.", + "Memory-specific embedding provider registration was removed; plugins now use the generic embedding provider contract.", }, { code: "deprecated-session-store-beta5-api", @@ -217,12 +207,9 @@ export const PLUGIN_COMPAT_RECORDS = [ }, { code: "whatsapp-web-inbound-flat-message-aliases", - status: "deprecated", + status: "removed", owner: "channel", introduced: "2026-05-30", - deprecated: "2026-05-30", - warningStarts: "2026-05-30", - removeAfter: "2026-08-30", replacement: "WhatsApp `WebInboundCallbackMessage` nested contexts: `event`, `payload`, `quote`, `group`, and `platform`", docsPath: "/plugins/compatibility", @@ -231,19 +218,16 @@ export const PLUGIN_COMPAT_RECORDS = [ "WhatsApp monitorWebInbox onMessage callback", "WhatsApp monitorWebChannel listenerFactory injected messages", ], - diagnostics: ["TypeScript deprecated WebInboundMessage flat field annotations"], + diagnostics: ["plugin compatibility registry and compatibility guide"], tests: ["src/plugins/compat/registry.test.ts"], releaseNote: - "WhatsApp WebInboundMessage flat fields remain wired as deprecated aliases while callbacks migrate to nested inbound contexts.", + "WhatsApp WebInboundMessage flat fields were removed; callbacks now receive only nested inbound contexts.", }, { code: "whatsapp-web-inbound-admission-top-level-fields", - status: "deprecated", + status: "removed", owner: "channel", introduced: "2026-06-14", - deprecated: "2026-06-14", - warningStarts: "2026-06-14", - removeAfter: "2026-08-30", replacement: "WhatsApp `WebInboundMessage.admission` fields: `conversation.id`, `accountId`, `ingress.decision`, and `conversation.kind`", docsPath: "/plugins/compatibility", @@ -252,10 +236,10 @@ export const PLUGIN_COMPAT_RECORDS = [ "WhatsApp monitorWebInbox onMessage callback", "WhatsApp monitorWebChannel listenerFactory injected messages", ], - diagnostics: ["TypeScript deprecated WebInboundMessage admission field annotations"], + diagnostics: ["plugin compatibility registry and compatibility guide"], tests: ["src/plugins/compat/registry.test.ts"], releaseNote: - "WhatsApp WebInboundMessage top-level admission fields remain available while callbacks migrate to the admission envelope.", + "WhatsApp WebInboundMessage top-level admission fields were removed; callbacks now read the canonical admission envelope.", }, { code: "sdk-untrusted-context-identifier-aliases", @@ -300,12 +284,9 @@ export const PLUGIN_COMPAT_RECORDS = [ }, { code: "channel-explicit-target-parser", - status: "deprecated", + status: "removed", owner: "sdk", introduced: "2026-04-28", - deprecated: "2026-05-23", - warningStarts: "2026-05-23", - removeAfter: "2026-08-23", replacement: "`messaging.targetResolver` for target normalization and `messaging.resolveOutboundSessionRoute` for session/thread identity", docsPath: "/plugins/sdk-migration", @@ -320,15 +301,14 @@ export const PLUGIN_COMPAT_RECORDS = [ "src/channels/plugins/contracts/test-helpers/surface-contract-suite.ts", "src/plugins/compat/registry.test.ts", ], + releaseNote: + "The deprecated channel explicit-target parser was removed; plugins must normalize targets with `messaging.targetResolver` and project session identity with `messaging.resolveOutboundSessionRoute`.", }, { code: "channel-messaging-targets-subpath", - status: "deprecated", + status: "removed", owner: "sdk", introduced: "2026-04-28", - deprecated: "2026-05-23", - warningStarts: "2026-05-23", - removeAfter: "2026-08-23", replacement: "`openclaw/plugin-sdk/channel-targets`", docsPath: "/plugins/sdk-migration", surfaces: ["openclaw/plugin-sdk/messaging-targets"], @@ -337,6 +317,8 @@ export const PLUGIN_COMPAT_RECORDS = [ "src/plugins/compat/registry.test.ts", "src/plugins/contracts/plugin-sdk-subpaths.test.ts", ], + releaseNote: + "The deprecated `openclaw/plugin-sdk/messaging-targets` subpath was removed; import target helpers from `openclaw/plugin-sdk/channel-targets`.", }, { code: "bundled-plugin-allowlist", @@ -453,12 +435,9 @@ export const PLUGIN_COMPAT_RECORDS = [ }, { code: "embedded-pi-agent-sdk-aliases", - status: "deprecated", + status: "removed", owner: "agent-runtime", introduced: "2026-05-21", - deprecated: "2026-05-21", - warningStarts: "2026-05-21", - removeAfter: "2026-08-21", replacement: "`runEmbeddedAgent` and `EmbeddedAgent*` SDK/runtime names", docsPath: "/plugins/sdk-runtime", surfaces: [ @@ -472,16 +451,13 @@ export const PLUGIN_COMPAT_RECORDS = [ "src/plugins/contracts/plugin-sdk-subpaths.test.ts", ], releaseNote: - "Legacy `runEmbeddedPiAgent` and `EmbeddedPi*` plugin aliases remain as deprecated SDK compatibility only.", + "The legacy `runEmbeddedPiAgent` and `EmbeddedPi*` plugin aliases were removed; plugins must use the neutral embedded-agent names.", }, { code: "plugin-sdk-shipped-channel-setup-exports", - status: "deprecated", + status: "removed", owner: "channel", introduced: "2026-07-23", - deprecated: "2026-07-23", - warningStarts: "2026-07-23", - removeAfter: "2026-08-30", replacement: "plugin-owned config schemas plus generic `openclaw/plugin-sdk/channel-config-schema` and `openclaw/plugin-sdk/setup-runtime` primitives", docsPath: "/plugins/sdk-migration#published-channel-setup-compatibility", @@ -493,12 +469,10 @@ export const PLUGIN_COMPAT_RECORDS = [ "openclaw/plugin-sdk/setup-runtime createLegacyCompatChannelDmPolicy", "openclaw/plugin-sdk/setup-runtime promptLegacyChannelAllowFromForAccount", ], - diagnostics: [ - "repository deprecated API usage guard for core and bundled plugins; no external runtime import warning", - ], - tests: ["src/plugin-sdk/shipped-channel-compat.test.ts", "src/plugins/compat/registry.test.ts"], + diagnostics: ["plugin compatibility registry and migration guide"], + tests: ["src/plugins/compat/registry.test.ts"], releaseNote: - "Published OpenClaw channel packages through 2026.7.1 remain loadable while they migrate to plugin-owned config and setup helpers.", + "The shipped channel setup compatibility schemas and helpers were removed; channel plugins must own their config schemas and setup policy.", }, { code: "generated-bundled-channel-config-fallback", diff --git a/src/plugins/compat/registry.test.ts b/src/plugins/compat/registry.test.ts index 71d0b64dfd84..7185192ad007 100644 --- a/src/plugins/compat/registry.test.ts +++ b/src/plugins/compat/registry.test.ts @@ -1,26 +1,9 @@ // Plugin compatibility registry tests cover compatibility metadata loading and validation. import fs from "node:fs"; -import { beforeAll, describe, expect, it } from "vitest"; -import { listGitTrackedFiles } from "../../test-utils/repo-files.js"; +import { describe, expect, it } from "vitest"; import { listPluginCompatRecords, type PluginCompatCode } from "./registry.js"; const datePattern = /^\d{4}-\d{2}-\d{2}$/u; -const sourceRootsForDeprecatedCallGuard = [ - "src", - "extensions", - "packages", - "test", - "scripts", -] as const; -const deprecatedTargetParserCallPattern = - /\.parseExplicitTarget\?\.\s*\(|parseExplicitTargetFor(?:Channel|LoadedChannel)\s*\(|resolveRouteTargetFor(?:Channel|LoadedChannel)\s*\(/u; -const deprecatedTargetParserCompatFiles = new Set([ - "src/auto-reply/reply/group-id.ts", - "src/channels/plugins/target-parsing-loaded.ts", - "src/infra/outbound/outbound-session.ts", - "src/infra/outbound/outbound-session.test-helpers.ts", - "src/plugins/compat/registry.test.ts", -]); const removalDatePendingCompatCodes = new Set([ "plugin-sdk-tool-plugin-public-demotion", "agent-harness-sdk-alias", @@ -66,23 +49,7 @@ function expectNonEmptyStringList(values: readonly string[], label: string) { } } -function listTrackedSourceFiles(): string[] { - const files = listGitTrackedFiles({ pathspecs: sourceRootsForDeprecatedCallGuard }); - if (!files) { - throw new Error("unable to list tracked source files for the deprecated-call guard"); - } - return files.filter((file) => /\.(?:ts|tsx|mts|cts)$/u.test(file)); -} - describe("plugin compatibility registry", () => { - let deprecatedTargetParserOffenders: string[] = []; - - beforeAll(() => { - deprecatedTargetParserOffenders = listTrackedSourceFiles() - .filter((file) => !deprecatedTargetParserCompatFiles.has(file)) - .filter((file) => deprecatedTargetParserCallPattern.test(fs.readFileSync(file, "utf8"))); - }); - it("keeps every record actionable", () => { for (const record of listPluginCompatRecords()) { expect(record.introduced, record.code).toMatch(datePattern); @@ -223,7 +190,83 @@ describe("plugin compatibility registry", () => { expect(record?.removeAfter).toBeUndefined(); }); - it("keeps deprecated explicit target parser calls inside compatibility shims", () => { - expect(deprecatedTargetParserOffenders).toEqual([]); + it("keeps the removed subagent spawning hook as a migration tombstone", () => { + const record = listPluginCompatRecords().find( + (candidate) => candidate.code === "legacy-subagent-spawning-hook", + ); + + expect(record).toMatchObject({ + status: "removed", + replacement: + "`subagent_spawned` for post-launch observation; core session-binding adapters for thread routing", + }); + expect(record?.removeAfter).toBeUndefined(); + }); + + it("keeps the removed embedded Pi aliases as a migration tombstone", () => { + const record = listPluginCompatRecords().find( + (candidate) => candidate.code === "embedded-pi-agent-sdk-aliases", + ); + + expect(record).toMatchObject({ + status: "removed", + replacement: "`runEmbeddedAgent` and `EmbeddedAgent*` SDK/runtime names", + }); + expect(record?.removeAfter).toBeUndefined(); + }); + + it("keeps removed shipped channel setup exports as a migration tombstone", () => { + const record = listPluginCompatRecords().find( + (candidate) => candidate.code === "plugin-sdk-shipped-channel-setup-exports", + ); + + expect(record).toMatchObject({ + status: "removed", + replacement: + "plugin-owned config schemas plus generic `openclaw/plugin-sdk/channel-config-schema` and `openclaw/plugin-sdk/setup-runtime` primitives", + }); + expect(record?.removeAfter).toBeUndefined(); + }); + + it("keeps the removed memory embedding registrar as a migration tombstone", () => { + const record = listPluginCompatRecords().find( + (candidate) => candidate.code === "deprecated-memory-embedding-provider-api", + ); + + expect(record).toMatchObject({ + status: "removed", + replacement: "`api.registerEmbeddingProvider(...)` and `contracts.embeddingProviders`", + }); + expect(record?.removeAfter).toBeUndefined(); + }); + + it("keeps removed WhatsApp inbound aliases as migration tombstones", () => { + const records = new Map(listPluginCompatRecords().map((record) => [record.code, record])); + + for (const code of [ + "whatsapp-web-inbound-flat-message-aliases", + "whatsapp-web-inbound-admission-top-level-fields", + ] as const) { + expect(records.get(code)).toMatchObject({ + status: "removed", + releaseNote: expect.stringMatching(/\S/u), + }); + expect(records.get(code)?.removeAfter).toBeUndefined(); + } + }); + + it("keeps removed channel target compatibility as migration tombstones", () => { + const records = new Map(listPluginCompatRecords().map((record) => [record.code, record])); + + for (const code of [ + "channel-explicit-target-parser", + "channel-messaging-targets-subpath", + ] as const) { + expect(records.get(code)).toMatchObject({ + status: "removed", + releaseNote: expect.stringMatching(/\S/u), + }); + expect(records.get(code)?.removeAfter).toBeUndefined(); + } }); }); diff --git a/src/plugins/compat/registry.ts b/src/plugins/compat/registry.ts index fb971a4b3fc2..1754196078c0 100644 --- a/src/plugins/compat/registry.ts +++ b/src/plugins/compat/registry.ts @@ -5,18 +5,6 @@ import type { PluginCompatRecord } from "./types.js"; export type PluginCompatCode = (typeof PLUGIN_COMPAT_RECORDS)[number]["code"]; type KnownPluginCompatRecord = PluginCompatRecord; -const pluginCompatRecordByCode = new Map( - PLUGIN_COMPAT_RECORDS.map((record) => [record.code, record]), -); - export function listPluginCompatRecords(): readonly KnownPluginCompatRecord[] { return PLUGIN_COMPAT_RECORDS; } - -export function getPluginCompatRecord(code: PluginCompatCode): KnownPluginCompatRecord { - const record = pluginCompatRecordByCode.get(code); - if (!record) { - throw new Error(`Unknown plugin compatibility code: ${code}`); - } - return record; -} diff --git a/src/plugins/contracts/config-footprint-guardrails.test.ts b/src/plugins/contracts/config-footprint-guardrails.test.ts index 75511e08e629..c90492a17f89 100644 --- a/src/plugins/contracts/config-footprint-guardrails.test.ts +++ b/src/plugins/contracts/config-footprint-guardrails.test.ts @@ -188,7 +188,7 @@ describe("config footprint guardrails", () => { ); }); - it("keeps current channel schemas plugin-owned with a narrow shipped compatibility tier", () => { + it("keeps current channel schemas plugin-owned", () => { const source = readSource("src/plugin-sdk/channel-config-schema.ts"); const bundledSource = readSource("src/plugin-sdk/bundled-channel-config-schema.ts"); const bundledSection = bundledSource.slice( @@ -215,12 +215,8 @@ describe("config footprint guardrails", () => { ].toSorted((left, right) => left.localeCompare(right)); expect(exportedSchemaNames).toEqual([ - "DiscordConfigSchema", "GoogleChatConfigSchema", "IMessageConfigSchema", - "MSTeamsConfigSchema", - "SignalConfigSchema", - "SlackConfigSchema", "TelegramConfigSchema", "WhatsAppConfigSchema", ]); @@ -251,7 +247,6 @@ describe("config footprint guardrails", () => { const allowedShellImporters = new Set([ // The facade's focused regression tests are its only internal consumers. "src/plugin-sdk/bundled-channel-config-schema.test.ts", - "src/plugin-sdk/shipped-channel-compat.test.ts", // This guardrail file embeds facade specifiers in shell-shape assertions. "src/plugins/contracts/config-footprint-guardrails.test.ts", ]); diff --git a/src/plugins/contracts/core-extension-facade-boundary.test.ts b/src/plugins/contracts/core-extension-facade-boundary.test.ts index 5c28389d1718..dcec6a01f0d7 100644 --- a/src/plugins/contracts/core-extension-facade-boundary.test.ts +++ b/src/plugins/contracts/core-extension-facade-boundary.test.ts @@ -14,7 +14,7 @@ const forbiddenOllamaFacadeFiles = [ ] as const; const genericCoreFixtureFiles = [ "src/commands/auth-choice.apply.plugin-provider.test.ts", - "src/plugins/contracts/memory-embedding-provider.contract.test.ts", + "src/plugins/contracts/embedding-provider.contract.test.ts", "src/plugins/discovery.test.ts", "src/plugins/contracts/tts-contract-suites.ts", ] as const; diff --git a/src/plugins/contracts/memory-embedding-provider.contract.test.ts b/src/plugins/contracts/memory-embedding-provider.contract.test.ts deleted file mode 100644 index d3b3ffdb665c..000000000000 --- a/src/plugins/contracts/memory-embedding-provider.contract.test.ts +++ /dev/null @@ -1,162 +0,0 @@ -// Memory embedding provider contract tests cover memory plugin embedding provider behavior. -import { - createPluginRegistryFixture, - registerVirtualTestPlugin, -} from "openclaw/plugin-sdk/plugin-test-contracts"; -import { describe, expect, it } from "vitest"; -import type { MemoryEmbeddingBatchOptions } from "../memory-embedding-providers.js"; -import { createPluginRecord } from "../status.test-fixtures.js"; - -describe("memory embedding provider registration", () => { - it("rejects non-memory plugins that did not declare the capability contract", () => { - const { config, registry } = createPluginRegistryFixture(); - - registerVirtualTestPlugin({ - registry, - config, - id: "not-memory", - name: "Not Memory", - register(api) { - api.registerMemoryEmbeddingProvider({ - id: "forbidden", - create: async () => ({ provider: null }), - }); - }, - }); - expect(registry.registry.memoryEmbeddingProviders).toStrictEqual([]); - const diagnostic = registry.registry.diagnostics.find( - (entry) => entry.pluginId === "not-memory", - ); - expect(diagnostic?.message).toBe( - "plugin must own memory slot or declare contracts.memoryEmbeddingProviders for adapter: forbidden", - ); - }); - - it("allows non-memory plugins that declare the capability contract", () => { - const { config, registry } = createPluginRegistryFixture(); - - registerVirtualTestPlugin({ - registry, - config, - id: "external-vector", - name: "External Vector", - contracts: { - memoryEmbeddingProviders: ["external-vector"], - }, - register(api) { - api.registerMemoryEmbeddingProvider({ - id: "external-vector", - create: async () => ({ provider: null }), - }); - }, - }); - const provider = registry.registry.memoryEmbeddingProviders[0]; - expect(provider?.provider.id).toBe("external-vector"); - expect(provider?.pluginId).toBe("external-vector"); - }); - - it("records the owning memory plugin id for registered adapters", () => { - const { config, registry } = createPluginRegistryFixture(); - - registerVirtualTestPlugin({ - registry, - config, - id: "memory-core", - name: "Memory Core", - kind: "memory", - register(api) { - api.registerMemoryEmbeddingProvider({ - id: "demo-embedding", - create: async () => ({ provider: null }), - }); - }, - }); - const provider = registry.registry.memoryEmbeddingProviders[0]; - expect(provider?.provider.id).toBe("demo-embedding"); - expect(provider?.pluginId).toBe("memory-core"); - }); - - it("keeps source-wide batch embedding behind an explicit runtime opt-in", async () => { - const { config, registry } = createPluginRegistryFixture(); - - registerVirtualTestPlugin({ - registry, - config, - id: "source-wide-memory", - name: "Source Wide Memory", - contracts: { - memoryEmbeddingProviders: ["source-wide-memory"], - }, - register(api) { - api.registerMemoryEmbeddingProvider({ - id: "source-wide-memory", - create: async () => ({ - provider: { - id: "source-wide-memory", - model: "test-embedding", - embedQuery: async (text: string) => [text.length], - embedBatch: async (texts: string[]) => texts.map((text) => [text.length]), - }, - runtime: { - id: "source-wide-memory", - sourceWideBatchEmbed: true, - batchEmbed: async (batch: MemoryEmbeddingBatchOptions) => - batch.chunks.map((chunk, index) => [index, chunk.text.length]), - }, - }), - }); - }, - }); - const adapter = registry.registry.memoryEmbeddingProviders[0]?.provider; - const result = await adapter?.create({ config, model: "test-embedding" }); - - expect(result?.runtime?.sourceWideBatchEmbed).toBe(true); - await expect( - result?.runtime?.batchEmbed?.({ - agentId: "main", - chunks: [{ text: "alpha" }, { text: "beta" }], - wait: true, - concurrency: 1, - pollIntervalMs: 1000, - timeoutMs: 60_000, - debug: () => {}, - }), - ).resolves.toEqual([ - [0, 5], - [1, 4], - ]); - }); - - it("keeps companion embedding providers available during tool discovery", () => { - const { config, registry } = createPluginRegistryFixture(); - const record = createPluginRecord({ - id: "tool-discovery-memory", - name: "Tool Discovery Memory", - kind: "memory", - contracts: { tools: ["memory_recall"] }, - }); - registry.registry.plugins.push(record); - const api = registry.createApi(record, { - config, - registrationMode: "tool-discovery", - }); - - api.registerMemoryEmbeddingProvider({ - id: "tool-discovery-embedding", - create: async () => ({ provider: null }), - }); - api.registerTool({ - name: "memory_recall", - label: "Memory Recall", - description: "Recall memory", - parameters: {}, - execute: async () => ({ content: [], details: {} }), - }); - const provider = registry.registry.memoryEmbeddingProviders[0]; - expect(provider?.provider.id).toBe("tool-discovery-embedding"); - expect(provider?.pluginId).toBe("tool-discovery-memory"); - expect(registry.registry.tools).toHaveLength(1); - expect(registry.registry.tools[0]?.pluginId).toBe("tool-discovery-memory"); - expect(registry.registry.tools[0]?.names).toEqual(["memory_recall"]); - }); -}); diff --git a/src/plugins/contracts/plugin-sdk-package-contract-guardrails.test.ts b/src/plugins/contracts/plugin-sdk-package-contract-guardrails.test.ts index ffedf804dfc7..092ec32f9830 100644 --- a/src/plugins/contracts/plugin-sdk-package-contract-guardrails.test.ts +++ b/src/plugins/contracts/plugin-sdk-package-contract-guardrails.test.ts @@ -50,28 +50,6 @@ const DEPRECATED_TEST_ALIAS_ALLOWED_REFERENCE_FILES = new Set([ "src/plugins/compat/registry.ts", "src/plugins/contracts/plugin-sdk-package-contract-guardrails.test.ts", ]); -const LEGACY_MEMORY_EMBEDDING_PROVIDER_API_FILES = new Set([ - "extensions/amazon-bedrock/register.sync.runtime.ts", - "extensions/github-copilot/index.ts", - "extensions/google/index.ts", - "extensions/lmstudio/index.ts", - "extensions/memory-core/src/memory/provider-adapters.ts", - "extensions/mistral/index.ts", - "extensions/ollama/index.ts", - "extensions/openai/index.ts", - "extensions/voyage/index.ts", -]); -const LEGACY_MEMORY_EMBEDDING_PROVIDER_MANIFEST_FILES = new Set([ - "extensions/amazon-bedrock/openclaw.plugin.json", - "extensions/github-copilot/openclaw.plugin.json", - "extensions/google/openclaw.plugin.json", - "extensions/lmstudio/openclaw.plugin.json", - "extensions/memory-core/openclaw.plugin.json", - "extensions/mistral/openclaw.plugin.json", - "extensions/ollama/openclaw.plugin.json", - "extensions/openai/openclaw.plugin.json", - "extensions/voyage/openclaw.plugin.json", -]); const MATRIX_RUNTIME_DEPS = [ "@matrix-org/matrix-sdk-crypto-wasm", "@matrix-org/matrix-sdk-crypto-nodejs", @@ -438,45 +416,6 @@ function collectDeprecatedExtensionSdkImports(): Array<{ file: string; specifier return leaks; } -function collectNewDeprecatedMemoryEmbeddingProviderApiFiles(): string[] { - const files: string[] = []; - for (const file of collectExtensionFiles(resolve(REPO_ROOT, "extensions"))) { - const repoRelativePath = toRepoRelativePath(file); - if (isExtensionTestOrSupportPath(repoRelativePath)) { - continue; - } - const source = fs.readFileSync(file, "utf8"); - if ( - /\b(?:[A-Za-z_$][\w$]*\.)?registerMemoryEmbeddingProvider\s*\(/u.test(source) && - !LEGACY_MEMORY_EMBEDDING_PROVIDER_API_FILES.has(repoRelativePath) - ) { - files.push(repoRelativePath); - } - } - return files.toSorted(); -} - -function collectNewDeprecatedMemoryEmbeddingProviderManifestFiles(): string[] { - const files: string[] = []; - const manifestFiles = listGitTrackedFiles({ - repoRoot: REPO_ROOT, - pathspecs: "extensions/**/openclaw.plugin.json", - }); - if (!manifestFiles) { - throw new Error("unable to list plugin manifests for the deprecated manifest guard"); - } - for (const repoRelativePath of manifestFiles) { - const source = fs.readFileSync(resolve(REPO_ROOT, repoRelativePath), "utf8"); - if ( - /"memoryEmbeddingProviders"\s*:/u.test(source) && - !LEGACY_MEMORY_EMBEDDING_PROVIDER_MANIFEST_FILES.has(repoRelativePath) - ) { - files.push(repoRelativePath); - } - } - return files.toSorted(); -} - function collectCodeFiles(dir: string): string[] { const trackedFiles = listTrackedCodeFiles(dir); if (trackedFiles) { @@ -805,16 +744,6 @@ describe("plugin-sdk package contract guardrails", () => { expect(collectDeprecatedExtensionSdkImports()).toStrictEqual([]); }); - it("keeps new bundled plugins off deprecated memory embedding provider registration", () => { - expect({ - apiFiles: collectNewDeprecatedMemoryEmbeddingProviderApiFiles(), - manifestFiles: collectNewDeprecatedMemoryEmbeddingProviderManifestFiles(), - }).toStrictEqual({ - apiFiles: [], - manifestFiles: [], - }); - }); - it("keeps real tests off the deprecated plugin-sdk test-utils alias", () => { expect(deprecatedTestAliasImports).toStrictEqual([]); }); diff --git a/src/plugins/contracts/plugin-sdk-runtime-api-guardrails.test.ts b/src/plugins/contracts/plugin-sdk-runtime-api-guardrails.test.ts index 997e4887da87..8124edb80f4c 100644 --- a/src/plugins/contracts/plugin-sdk-runtime-api-guardrails.test.ts +++ b/src/plugins/contracts/plugin-sdk-runtime-api-guardrails.test.ts @@ -263,7 +263,7 @@ const RUNTIME_API_EXPORT_GUARDS: Record = { 'export { createWhatsAppLoginTool } from "./src/agent-tools-login.js";', 'export { formatWhatsAppWebAuthStatusState, getWebAuthAgeMs, hasWebCredsSync, logWebSelfId, logoutWeb, pickWebChannel, readCredsJsonRaw, readWebAuthExistsBestEffort, readWebAuthExistsForDecision, readWebAuthSnapshot, readWebAuthSnapshotBestEffort, readWebAuthState, readWebSelfId, readWebSelfIdentity, readWebSelfIdentityForDecision, resolveDefaultWebAuthDir, resolveWebCredsBackupPath, resolveWebCredsPath, restoreCredsFromBackupIfNeeded, webAuthExists, WA_WEB_AUTH_DIR, WHATSAPP_AUTH_UNSTABLE_CODE, WhatsAppAuthUnstableError, type WhatsAppWebAuthState } from "./src/auth-store.js";', 'export { DEFAULT_WEB_MEDIA_BYTES, HEARTBEAT_PROMPT, HEARTBEAT_TOKEN, monitorWebChannel, SILENT_REPLY_TOKEN, stripHeartbeatToken, type WebChannelStatus, type WebMonitorTuning } from "./src/auto-reply.js";', - 'export { extractContactContext, extractLocationData, extractText, monitorWebInbox, resetWebInboundDedupe, type LegacyFlatWebInboundMessage, type WebInboundCallbackMessage, type WebInboundMessage, type WebInboundMessageInput, type WebListenerCloseReason, type WhatsAppInboundAdmission } from "./src/inbound.js";', + 'export { extractContactContext, extractLocationData, extractText, monitorWebInbox, resetWebInboundDedupe, type WebInboundCallbackMessage, type WebInboundMessage, type WebListenerCloseReason, type WhatsAppInboundAdmission } from "./src/inbound.js";', 'export { loginWeb } from "./src/login.js";', 'export { getDefaultLocalRoots, loadWebMedia, loadWebMediaRaw, LocalMediaAccessError, optimizeImageToJpeg, optimizeImageToPng, type LocalMediaAccessErrorCode, type WebMediaResult } from "./src/media.js";', 'export { sendMessageWhatsApp, sendPollWhatsApp, sendReactionWhatsApp, sendTypingWhatsApp } from "./src/send.js";', diff --git a/src/plugins/contracts/plugin-sdk-subpaths.test.ts b/src/plugins/contracts/plugin-sdk-subpaths.test.ts index 7b66ace52a5e..18761d72cde3 100644 --- a/src/plugins/contracts/plugin-sdk-subpaths.test.ts +++ b/src/plugins/contracts/plugin-sdk-subpaths.test.ts @@ -1255,7 +1255,7 @@ describe("plugin-sdk subpath exports", () => { expectSourceMentions("setup-tools", ["formatCliCommand", "detectBinary", "formatDocsLink"]); expectSourceMentions("lazy-runtime", ["createLazyRuntimeSurface", "createLazyRuntimeModule"]); expectSourceMentions("agent-harness", ["./agent-harness-runtime.js"]); - expectSourceMentions("agent-harness-runtime", ["AgentHarness", "EmbeddedPiCompactResult"]); + expectSourceMentions("agent-harness-runtime", ["AgentHarness", "EmbeddedAgentCompactResult"]); expectSourceOmitsSnippet("agent-runtime", "./sglang.js"); expectSourceOmitsSnippet("agent-runtime", "./vllm.js"); expectSourceOmitsSnippet("agent-runtime", "../../extensions/"); diff --git a/src/plugins/embedding-provider-config.ts b/src/plugins/embedding-provider-config.ts index e95845d826bb..8c0626717b47 100644 --- a/src/plugins/embedding-provider-config.ts +++ b/src/plugins/embedding-provider-config.ts @@ -25,7 +25,7 @@ function resolveConfiguredProviderConfig( } /** Reads a configured provider's backing API id when runtime lookup should follow an alias. */ -export function readConfiguredProviderApiId(params: { +function readConfiguredProviderApiId(params: { providerId: string; cfg?: OpenClawConfig; resolveApiProviderId?: (normalizedApiId: string) => string | undefined; diff --git a/src/plugins/embedding-provider-runtime-shared.ts b/src/plugins/embedding-provider-runtime-shared.ts index 37f124c0280b..2b751e5f08d4 100644 --- a/src/plugins/embedding-provider-runtime-shared.ts +++ b/src/plugins/embedding-provider-runtime-shared.ts @@ -7,7 +7,7 @@ import { type CapabilityProviderFor, } from "./capability-provider-runtime.js"; -type EmbeddingProviderCapabilityKey = "embeddingProviders" | "memoryEmbeddingProviders"; +type EmbeddingProviderCapabilityKey = "embeddingProviders"; type RegisteredAdapterEntry = { adapter: TAdapter; }; diff --git a/src/plugins/embedding-providers.test.ts b/src/plugins/embedding-providers.test.ts index 2f8b000dd71c..5fb4290f4233 100644 --- a/src/plugins/embedding-providers.test.ts +++ b/src/plugins/embedding-providers.test.ts @@ -100,16 +100,14 @@ describe("collectRegisteredEmbeddingProviderIds", () => { // Boot-equivalence: the shared helper unions the same three sources the gateway // startup "configured but unregistered" warning uses, so the /status drift line and // the boot warning agree on what counts as "registered". - it("unions registry memory + general embedding providers with the global registry", () => { + it("unions registry embedding providers with the global registry", () => { registerEmbeddingProvider(createAdapter("global-embed"), { ownerPluginId: "p" }); const registry = { - memoryEmbeddingProviders: [{ provider: { id: "mem-embed" } }], embeddingProviders: [{ provider: { id: "gen-embed" } }], } as never; const ids = collectRegisteredEmbeddingProviderIds(registry); - expect(ids.has("mem-embed")).toBe(true); expect(ids.has("gen-embed")).toBe(true); expect(ids.has("global-embed")).toBe(true); // Every globally registered provider (core + plugin-registered) is always included. diff --git a/src/plugins/gateway-startup-plugin-activation.ts b/src/plugins/gateway-startup-plugin-activation.ts index 1c64dfc2858c..3f7c6a5a7c4e 100644 --- a/src/plugins/gateway-startup-plugin-activation.ts +++ b/src/plugins/gateway-startup-plugin-activation.ts @@ -57,7 +57,6 @@ type StartupActivationPolicy = type StartupContractKey = | keyof ConfiguredGenerationProviderIds | keyof ConfiguredVoiceProviderIds - | "memoryEmbeddingProviders" | "embeddingProviders"; export function addRequiredAgentHarnessPluginIds( @@ -246,11 +245,6 @@ const GATEWAY_STARTUP_ACTIVATION_POLICIES: readonly { { policy: "implicit-external", matches: ({ manifest, configuredMemoryEmbeddingProviderIds }) => - manifestOwnsConfiguredContract( - manifest, - "memoryEmbeddingProviders", - configuredMemoryEmbeddingProviderIds, - ) || manifestOwnsConfiguredContract( manifest, "embeddingProviders", diff --git a/src/plugins/gateway-startup-plugin-providers.ts b/src/plugins/gateway-startup-plugin-providers.ts index 1f618cf3d5e0..73c365c89ed4 100644 --- a/src/plugins/gateway-startup-plugin-providers.ts +++ b/src/plugins/gateway-startup-plugin-providers.ts @@ -406,17 +406,13 @@ export function collectUnregisteredConfiguredMemoryEmbeddingProviders(params: { } // Registered embedding provider ids the loaded runtime can actually serve: the live -// registry's memory + general embedding providers plus the global/core embedding -// registry. Shared by gateway boot (the startup "configured but unregistered" warning) -// and the `/status plugins` drift line so both agree on what counts as "registered" and -// never diverge. The `{ provider: entry.adapter }` wrap makes the core registry entries -// match the registration shape so the id projection stays uniform across all three sources. +// registry's embedding providers plus the global/core embedding registry. Shared by +// gateway boot and `/status plugins` so both agree on what counts as registered. export function collectRegisteredEmbeddingProviderIds( - registry: Partial>, + registry: Partial>, ): Set { return new Set( [ - ...(registry.memoryEmbeddingProviders ?? []), ...(registry.embeddingProviders ?? []), ...listRegisteredEmbeddingProviders().map((entry) => ({ provider: entry.adapter })), ].map((entry) => entry.provider.id), diff --git a/src/plugins/hook-types.ts b/src/plugins/hook-types.ts index ea7d8b8ff24e..3634f5167da8 100644 --- a/src/plugins/hook-types.ts +++ b/src/plugins/hook-types.ts @@ -119,12 +119,6 @@ export type PluginHookName = | "before_message_write" | "session_start" | "session_end" - /** - * @deprecated Core prepares thread-bound subagent bindings through channel - * session-binding adapters before `subagent_spawned` fires. Use - * `subagent_spawned` for post-launch observation in new plugins. - */ - | "subagent_spawning" | "subagent_delivery_target" | "subagent_spawned" | "subagent_progress" @@ -169,7 +163,6 @@ const PLUGIN_HOOK_NAMES = [ "before_message_write", "session_start", "session_end", - "subagent_spawning", "subagent_delivery_target", "subagent_spawned", "subagent_progress", @@ -194,14 +187,6 @@ type AssertAllPluginHookNamesListed = MissingPluginHookNames extends never ? tru const assertAllPluginHookNamesListed: AssertAllPluginHookNamesListed = true; void assertAllPluginHookNamesListed; -type DeprecatedPluginHookName = "subagent_spawning"; - -type PluginHookDeprecation = { - replacement: string; - reason: string; - removeAfter?: string; -}; - type PluginHookChannelPairingRequestedEvent = { /** Channel that created the pending pairing request. */ channel: string; @@ -221,25 +206,6 @@ type PluginHookChannelPairingContext = { senderId: string; }; -export const DEPRECATED_PLUGIN_HOOKS = { - subagent_spawning: { - replacement: "`subagent_spawned` for observation; core session bindings for routing", - reason: - "Core prepares thread-bound subagent bindings through channel session-binding adapters before `subagent_spawned` fires.", - removeAfter: "2026-08-30", - }, -} as const satisfies Record; - -const DEPRECATED_PLUGIN_HOOK_NAMES = Object.keys( - DEPRECATED_PLUGIN_HOOKS, -) as DeprecatedPluginHookName[]; - -const deprecatedPluginHookNameSet = new Set(DEPRECATED_PLUGIN_HOOK_NAMES); - -export const isDeprecatedPluginHookName = ( - hookName: PluginHookName, -): hookName is DeprecatedPluginHookName => deprecatedPluginHookNameSet.has(hookName); - const pluginHookNameSet = new Set(PLUGIN_HOOK_NAMES); export const isPluginHookName = (hookName: unknown): hookName is PluginHookName => @@ -839,44 +805,6 @@ type PluginHookSubagentSpawnBase = { threadRequested: boolean; }; -/** - * @deprecated Core prepares thread-bound subagent bindings through channel - * session-binding adapters before `subagent_spawned` fires. Use - * `subagent_spawned` for post-launch observation in new plugins. - */ -export type PluginHookSubagentSpawningEvent = PluginHookSubagentSpawnBase; - -/** - * @deprecated Core prepares thread-bound subagent bindings through channel - * session-binding adapters before `subagent_spawned` fires. Returning routing - * data from `subagent_spawning` is retained only for older runtimes. - */ -export type PluginHookSubagentSpawningResult = - | { - status: "ok"; - /** - * @deprecated Core now resolves thread-bound spawn routing from session - * bindings and channel route projection. Keep returning this only for - * compatibility with older OpenClaw runtimes. - */ - threadBindingReady?: boolean; - /** - * @deprecated Use channel `resolveDeliveryTarget` plus core - * `SessionBindingRecord` projection instead of returning an ad hoc - * delivery route from this hook. - */ - deliveryOrigin?: { - channel?: string; - accountId?: string; - to?: string; - threadId?: string | number; - }; - } - | { - status: "error"; - error: string; - }; - export type PluginHookSubagentDeliveryTargetEvent = { childSessionKey: string; requesterSessionKey: string; @@ -1339,15 +1267,6 @@ export type PluginHookHandlerMap = { event: PluginHookSessionEndEvent, ctx: PluginHookSessionContext, ) => Promise | void; - /** - * @deprecated Core prepares thread-bound subagent bindings through channel - * session-binding adapters before `subagent_spawned` fires. Use - * `subagent_spawned` for post-launch observation in new plugins. - */ - subagent_spawning: ( - event: PluginHookSubagentSpawningEvent, - ctx: PluginHookSubagentContext, - ) => Promise | PluginHookSubagentSpawningResult | void; subagent_delivery_target: ( event: PluginHookSubagentDeliveryTargetEvent, ctx: PluginHookSubagentContext, diff --git a/src/plugins/hooks.ts b/src/plugins/hooks.ts index 6705c6907bac..ede16de0af57 100644 --- a/src/plugins/hooks.ts +++ b/src/plugins/hooks.ts @@ -87,8 +87,6 @@ import type { PluginHookSubagentContext, PluginHookSubagentDeliveryTargetEvent, PluginHookSubagentDeliveryTargetResult, - PluginHookSubagentSpawningEvent, - PluginHookSubagentSpawningResult, PluginHookSubagentEndedEvent, PluginHookSubagentProgressEvent, PluginHookSubagentSpawnedEvent, @@ -566,24 +564,6 @@ export function createHookRunner( return next.action === "continue" ? { action: "continue", reason: next.reason } : (acc ?? next); }; - const mergeSubagentSpawningResult = ( - acc: PluginHookSubagentSpawningResult | undefined, - next: PluginHookSubagentSpawningResult, - ): PluginHookSubagentSpawningResult => { - if (acc?.status === "error") { - return acc; - } - if (next.status === "error") { - return next; - } - const deliveryOrigin = acc?.deliveryOrigin ?? next.deliveryOrigin; - return { - status: "ok", - threadBindingReady: Boolean(acc?.threadBindingReady || next.threadBindingReady), - ...(deliveryOrigin ? { deliveryOrigin } : {}), - }; - }; - const mergeSubagentDeliveryTargetResult = ( acc: PluginHookSubagentDeliveryTargetResult | undefined, next: PluginHookSubagentDeliveryTargetResult, @@ -1431,23 +1411,6 @@ export function createHookRunner( // Session Hooks // ========================================================================= - /** - * @deprecated Core prepares thread-bound subagent bindings through channel - * session-binding adapters before subagent_spawned fires. This remains only - * for older plugins that call the hook runner directly. - */ - async function runSubagentSpawning( - event: PluginHookSubagentSpawningEvent, - ctx: PluginHookSubagentContext, - ): Promise { - return runModifyingHook<"subagent_spawning", PluginHookSubagentSpawningResult>( - "subagent_spawning", - event, - ctx, - { mergeResults: mergeSubagentSpawningResult }, - ); - } - /** * Run subagent_delivery_target hook. * Runs sequentially so channel plugins can deterministically resolve routing. @@ -1684,7 +1647,6 @@ export function createHookRunner( event: PluginHookSessionEndEvent, ctx: PluginHookSessionContext, ): Promise => runVoidHook("session_end", event, ctx), - runSubagentSpawning, runSubagentDeliveryTarget, runSubagentSpawned: async ( event: PluginHookSubagentSpawnedEvent, @@ -1733,10 +1695,6 @@ export type HookRunner = ReturnType; export type SubagentLifecycleHookRunner = Pick< HookRunner, - | "hasHooks" - | "runSubagentSpawning" - | "runSubagentSpawned" - | "runSubagentProgress" - | "runSubagentEnded" + "hasHooks" | "runSubagentSpawned" | "runSubagentProgress" | "runSubagentEnded" >; /* oxlint-disable max-lines -- TODO: split this grandfathered oversized file. */ diff --git a/src/plugins/installed-plugin-index-scope-lookup.ts b/src/plugins/installed-plugin-index-scope-lookup.ts index e58373ece43e..1ec2f7e36127 100644 --- a/src/plugins/installed-plugin-index-scope-lookup.ts +++ b/src/plugins/installed-plugin-index-scope-lookup.ts @@ -8,7 +8,6 @@ import type { InstalledPluginIndex, InstalledPluginIndexRecord } from "./install const PROVIDER_CONTRIBUTION_CONTRACTS = [ "externalAuthProviders", "embeddingProviders", - "memoryEmbeddingProviders", "speechProviders", "realtimeTranscriptionProviders", "realtimeVoiceProviders", diff --git a/src/plugins/loader-records.test.ts b/src/plugins/loader-records.test.ts index a9478418cb32..c64ba56e37e7 100644 --- a/src/plugins/loader-records.test.ts +++ b/src/plugins/loader-records.test.ts @@ -73,7 +73,6 @@ describe("plugin loader records", () => { webFetchProviders: ["kitchen-sink-web-fetch-provider"], webSearchProviders: ["kitchen-sink-web-search-provider"], migrationProviders: ["kitchen-sink-migration-provider"], - memoryEmbeddingProviders: ["kitchen-sink-memory-provider"], }, configSchema: false, }); @@ -91,7 +90,6 @@ describe("plugin loader records", () => { expect(record.webFetchProviderIds).toEqual(["kitchen-sink-web-fetch-provider"]); expect(record.webSearchProviderIds).toEqual(["kitchen-sink-web-search-provider"]); expect(record.migrationProviderIds).toEqual(["kitchen-sink-migration-provider"]); - expect(record.memoryEmbeddingProviderIds).toEqual(["kitchen-sink-memory-provider"]); }); it.each([ diff --git a/src/plugins/loader-records.ts b/src/plugins/loader-records.ts index 09e2f0bade01..e0e2fe2177fa 100644 --- a/src/plugins/loader-records.ts +++ b/src/plugins/loader-records.ts @@ -87,7 +87,6 @@ export function createPluginRecord(params: { webSearchProviderIds: [...(params.contracts?.webSearchProviders ?? [])], migrationProviderIds: [...(params.contracts?.migrationProviders ?? [])], contextEngineIds: [], - memoryEmbeddingProviderIds: [...(params.contracts?.memoryEmbeddingProviders ?? [])], agentHarnessIds: [], cliCommands: [], services: [], diff --git a/src/plugins/loader.hooks-and-runtime.test-utils.ts b/src/plugins/loader.hooks-and-runtime.test-utils.ts index 899d4ce660f4..754be9a0a548 100644 --- a/src/plugins/loader.hooks-and-runtime.test-utils.ts +++ b/src/plugins/loader.hooks-and-runtime.test-utils.ts @@ -1463,36 +1463,5 @@ ${channelPluginSource({ undefined, ]); }); - - it("warns when plugins register deprecated subagent_spawning typed hooks", () => { - useNoBundledPlugins(); - const plugin = writePlugin({ - id: "legacy-subagent-spawning-hook", - filename: "legacy-subagent-spawning-hook.cjs", - body: `module.exports = { id: "legacy-subagent-spawning-hook", register(api) { - api.on("subagent_spawning", () => ({ status: "ok" })); - } };`, - }); - - const registry = loadRegistryFromSinglePlugin({ - plugin, - pluginConfig: { - allow: ["legacy-subagent-spawning-hook"], - }, - }); - - expect( - registry.plugins.find((entry) => entry.id === "legacy-subagent-spawning-hook")?.status, - ).toBe("loaded"); - expect(registry.typedHooks.map((entry) => entry.hookName)).toEqual(["subagent_spawning"]); - expect( - registry.diagnostics.some( - (diag) => - diag.pluginId === "legacy-subagent-spawning-hook" && - diag.message === - 'typed hook "subagent_spawning" is deprecated (legacy-subagent-spawning-hook); Core prepares thread-bound subagent bindings through channel session-binding adapters before `subagent_spawned` fires. Use `subagent_spawned` for observation; core session bindings for routing. This compatibility hook will be removed after 2026-08-30.', - ), - ).toBe(true); - }); }); /* oxlint-disable max-lines -- TODO: split this grandfathered oversized file. */ diff --git a/src/plugins/loader.registration.test-utils.ts b/src/plugins/loader.registration.test-utils.ts index 6508894b4436..e24306bca699 100644 --- a/src/plugins/loader.registration.test-utils.ts +++ b/src/plugins/loader.registration.test-utils.ts @@ -49,8 +49,6 @@ import { globalAfterEach0, globalAfterAll1, } from "./loader.test-harness.js"; -import { listRegisteredMemoryEmbeddingProviderAdapters } from "./memory-embedding-provider-runtime.js"; -import { registerMemoryEmbeddingProvider } from "./memory-embedding-providers.js"; import { buildMemoryPromptSection, getMemoryCapabilityRegistration, @@ -738,10 +736,6 @@ describe("loadOpenClawPlugins", () => { it("does not replace active memory plugin registries during non-activating loads", () => { useNoBundledPlugins(); - registerMemoryEmbeddingProvider({ - id: "active", - create: async () => ({ provider: null }), - }); registerMemoryCorpusSupplement("memory-wiki", { search: async () => [], get: async () => null, @@ -775,10 +769,6 @@ describe("loadOpenClawPlugins", () => { id: "snapshot-memory", kind: "memory", register(api) { - api.registerMemoryEmbeddingProvider({ - id: "snapshot", - create: async () => ({ provider: null }), - }); api.registerMemoryCapability({ promptBuilder: () => ["snapshot memory section"], flushPlanResolver: () => ({ @@ -824,9 +814,6 @@ describe("loadOpenClawPlugins", () => { expect(listMemoryCorpusSupplements()).toHaveLength(1); expect(resolveMemoryFlushPlan({})?.relativePath).toBe("memory/active.md"); expect(getMemoryRuntime()).toBe(activeRuntime); - expect(listRegisteredMemoryEmbeddingProviderAdapters().map((adapter) => adapter.id)).toEqual([ - "active", - ]); expect(listMemoryPromptPreparations()).toHaveLength(1); }); @@ -973,10 +960,6 @@ describe("loadOpenClawPlugins", () => { id: "failing-memory", kind: "memory", register(api) { - api.registerMemoryEmbeddingProvider({ - id: "failed", - create: async () => ({ provider: null }), - }); api.registerMemoryCapability({ promptBuilder: () => ["stale failure section"], flushPlanResolver: () => ({ @@ -1026,7 +1009,6 @@ describe("loadOpenClawPlugins", () => { expect(listMemoryPromptPreparations()).toStrictEqual([]); expect(resolveMemoryFlushPlan({})).toBeNull(); expect(getMemoryRuntime()).toBeUndefined(); - expect(listRegisteredMemoryEmbeddingProviderAdapters()).toStrictEqual([]); }); it("does not replace the active detached task runtime during non-activating loads", () => { diff --git a/src/plugins/loader.runtime-registry.test.ts b/src/plugins/loader.runtime-registry.test.ts index 678d4280a685..311a942af05d 100644 --- a/src/plugins/loader.runtime-registry.test.ts +++ b/src/plugins/loader.runtime-registry.test.ts @@ -4,6 +4,10 @@ import { createPluginMetadataSnapshot } from "../config/plugin-auto-enable.test- import type { OpenClawConfig } from "../config/types.openclaw.js"; import type { PluginInstallRecord } from "../config/types.plugins.js"; import { setCurrentPluginMetadataSnapshot } from "./current-plugin-metadata-snapshot.js"; +import { + getRegisteredEmbeddingProvider, + registerEmbeddingProvider, +} from "./embedding-providers.js"; import { loadInstalledPluginIndexInstallRecordsSync, writePersistedInstalledPluginIndexInstallRecordsSync, @@ -20,10 +24,6 @@ import { makePluginLoaderTempDir, resetPluginLoaderTestStateForTest, } from "./loader.test-fixtures.js"; -import { - getRegisteredMemoryEmbeddingProvider, - registerMemoryEmbeddingProvider, -} from "./memory-embedding-providers.js"; import { buildMemoryPromptSection, registerMemoryCapability } from "./memory-state.js"; import { clearPluginMetadataLifecycleCaches } from "./plugin-metadata-lifecycle.js"; import { createEmptyPluginRegistry } from "./registry.js"; @@ -43,7 +43,7 @@ it("keeps an empty scoped handle load from replacing the root registry", () => { }); function requireMemoryEmbeddingProvider(providerId: string) { - const provider = getRegisteredMemoryEmbeddingProvider(providerId)?.adapter; + const provider = getRegisteredEmbeddingProvider(providerId)?.adapter; if (!provider) { throw new Error(`expected ${providerId} memory embedding provider`); } @@ -316,7 +316,7 @@ describe("resolveRuntimePluginRegistry", () => { describe("clearPluginRegistryLoadCache", () => { it("preserves plugin-owned runtime registries while invalidating load snapshots", () => { - registerMemoryEmbeddingProvider({ + registerEmbeddingProvider({ id: "still-live", create: async () => ({ provider: null }), }); diff --git a/src/plugins/manifest-capability-normalizers.ts b/src/plugins/manifest-capability-normalizers.ts index 3b33f15984ee..78346c3549bc 100644 --- a/src/plugins/manifest-capability-normalizers.ts +++ b/src/plugins/manifest-capability-normalizers.ts @@ -346,7 +346,6 @@ const MANIFEST_CONTRACT_KEYS = [ "trustedToolPolicies", "externalAuthProviders", "embeddingProviders", - "memoryEmbeddingProviders", "speechProviders", "realtimeTranscriptionProviders", "realtimeVoiceProviders", diff --git a/src/plugins/manifest-registry.ts b/src/plugins/manifest-registry.ts index 078d2f714b73..59681d080f44 100644 --- a/src/plugins/manifest-registry.ts +++ b/src/plugins/manifest-registry.ts @@ -194,7 +194,6 @@ export type PluginManifestContractListKey = | "imageGenerationProviders" | "videoGenerationProviders" | "musicGenerationProviders" - | "memoryEmbeddingProviders" | "webContentExtractors" | "webFetchProviders" | "webSearchProviders" @@ -420,7 +419,6 @@ function mergeManifestContracts( "trustedToolPolicies", "externalAuthProviders", "embeddingProviders", - "memoryEmbeddingProviders", "speechProviders", "realtimeTranscriptionProviders", "realtimeVoiceProviders", diff --git a/src/plugins/manifest-types.ts b/src/plugins/manifest-types.ts index 26db8ec31b93..b3e1fb79d93e 100644 --- a/src/plugins/manifest-types.ts +++ b/src/plugins/manifest-types.ts @@ -450,7 +450,6 @@ export type PluginManifestContracts = { */ externalAuthProviders?: string[]; embeddingProviders?: string[]; - memoryEmbeddingProviders?: string[]; speechProviders?: string[]; realtimeTranscriptionProviders?: string[]; realtimeVoiceProviders?: string[]; diff --git a/src/plugins/memory-embedding-provider-runtime.test.ts b/src/plugins/memory-embedding-provider-runtime.test.ts index 7545428fa423..91fae453f8c5 100644 --- a/src/plugins/memory-embedding-provider-runtime.test.ts +++ b/src/plugins/memory-embedding-provider-runtime.test.ts @@ -1,9 +1,12 @@ // Covers memory embedding provider runtime hooks from plugins. import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { clearEmbeddingProviders, registerEmbeddingProvider } from "./embedding-providers.js"; import { - clearMemoryEmbeddingProviders, - registerMemoryEmbeddingProvider, + clearEmbeddingProviders, + registerEmbeddingProvider, + type EmbeddingProviderAdapter, +} from "./embedding-providers.js"; +import { + adaptMemoryEmbeddingProviderAdapter, type MemoryEmbeddingProviderAdapter, } from "./memory-embedding-providers.js"; @@ -23,7 +26,7 @@ vi.mock("./capability-provider-runtime.js", () => ({ let runtimeModule: typeof import("./memory-embedding-provider-runtime.js"); -function createCapabilityAdapter(id: string): MemoryEmbeddingProviderAdapter { +function createCapabilityAdapter(id: string): EmbeddingProviderAdapter { return { id, create: async () => ({ provider: null }), @@ -32,7 +35,6 @@ function createCapabilityAdapter(id: string): MemoryEmbeddingProviderAdapter { beforeEach(async () => { clearEmbeddingProviders(); - clearMemoryEmbeddingProviders(); mocks.resolvePluginCapabilityProviders.mockReset(); mocks.resolvePluginCapabilityProviders.mockReturnValue([]); mocks.resolvePluginCapabilityProvider.mockReset(); @@ -42,18 +44,18 @@ beforeEach(async () => { afterEach(() => { clearEmbeddingProviders(); - clearMemoryEmbeddingProviders(); }); describe("memory embedding provider runtime resolution", () => { it("merges registered and declared capability fallback adapters", () => { - registerMemoryEmbeddingProvider({ + registerEmbeddingProvider({ id: "registered", create: async () => ({ provider: null }), }); mocks.resolvePluginCapabilityProviders.mockReturnValue([createCapabilityAdapter("capability")]); expect(runtimeModule.listMemoryEmbeddingProviders().map((adapter) => adapter.id)).toEqual([ + "openai-compatible", "registered", "capability", ]); @@ -66,12 +68,13 @@ describe("memory embedding provider runtime resolution", () => { mocks.resolvePluginCapabilityProvider.mockReturnValue(createCapabilityAdapter("ollama")); expect(runtimeModule.listMemoryEmbeddingProviders().map((adapter) => adapter.id)).toEqual([ + "openai-compatible", "ollama", ]); expect(runtimeModule.getMemoryEmbeddingProvider("ollama")?.id).toBe("ollama"); expect(mocks.resolvePluginCapabilityProviders).toHaveBeenCalledTimes(1); expect(mocks.resolvePluginCapabilityProvider).toHaveBeenCalledWith({ - key: "memoryEmbeddingProviders", + key: "embeddingProviders", providerId: "ollama", cfg: undefined, }); @@ -94,16 +97,16 @@ describe("memory embedding provider runtime resolution", () => { providerId === "ollama" ? ollamaAdapter : undefined, ); - expect(runtimeModule.getMemoryEmbeddingProvider("ollama-5080", config as never)).toBe( - ollamaAdapter, + expect(runtimeModule.getMemoryEmbeddingProvider("ollama-5080", config as never)?.id).toBe( + ollamaAdapter.id, ); expect(mocks.resolvePluginCapabilityProvider).toHaveBeenCalledWith({ - key: "memoryEmbeddingProviders", + key: "embeddingProviders", providerId: "ollama-5080", cfg: config, }); expect(mocks.resolvePluginCapabilityProvider).toHaveBeenCalledWith({ - key: "memoryEmbeddingProviders", + key: "embeddingProviders", providerId: "ollama", cfg: config, }); @@ -111,7 +114,7 @@ describe("memory embedding provider runtime resolution", () => { it("uses registered adapters through a configured provider api", () => { const ollamaAdapter = createCapabilityAdapter("ollama"); - registerMemoryEmbeddingProvider(ollamaAdapter); + registerEmbeddingProvider(ollamaAdapter); const config = { models: { providers: { @@ -124,10 +127,12 @@ describe("memory embedding provider runtime resolution", () => { }, } as never; - expect(runtimeModule.getMemoryEmbeddingProvider("ollama-gpu1", config)).toBe(ollamaAdapter); + expect(runtimeModule.getMemoryEmbeddingProvider("ollama-gpu1", config)?.id).toBe( + ollamaAdapter.id, + ); expect(mocks.resolvePluginCapabilityProvider).toHaveBeenCalledTimes(1); expect(mocks.resolvePluginCapabilityProvider).toHaveBeenCalledWith({ - key: "memoryEmbeddingProviders", + key: "embeddingProviders", providerId: "ollama-gpu1", cfg: config, }); @@ -137,20 +142,21 @@ describe("memory embedding provider runtime resolution", () => { const registered = { id: "openai", create: async () => ({ provider: null }), - } satisfies MemoryEmbeddingProviderAdapter; - registerMemoryEmbeddingProvider({ + } satisfies EmbeddingProviderAdapter; + registerEmbeddingProvider({ ...registered, }); mocks.resolvePluginCapabilityProviders.mockReturnValue([createCapabilityAdapter("openai")]); - expect(runtimeModule.getMemoryEmbeddingProvider("openai")).toStrictEqual(registered); + expect(runtimeModule.getMemoryEmbeddingProvider("openai")?.id).toBe(registered.id); expect(runtimeModule.listMemoryEmbeddingProviders().map((adapter) => adapter.id)).toEqual([ + "openai-compatible", "openai", ]); expect(mocks.resolvePluginCapabilityProviders).toHaveBeenCalledTimes(1); }); - it("adapts generic providers once without adding them to memory auto-selection", async () => { + it("adapts generic providers for memory queries, batches, and identity", async () => { const close = vi.fn(); const embed = vi.fn(async () => [1, 2]); const embedBatch = vi.fn(async (inputs: unknown[]) => inputs.map(() => [3, 4])); @@ -184,7 +190,9 @@ describe("memory embedding provider runtime resolution", () => { defaultModel: "generic-default", transport: "local", }); - expect(runtimeModule.listMemoryEmbeddingProviders()).toEqual([]); + expect(runtimeModule.listMemoryEmbeddingProviders().map((entry) => entry.id)).toContain( + "generic", + ); const options = { config: {}, model: "generic-model", outputDimensionality: 7 }; expect(adapter?.resolveIndexIdentity?.(options)).toEqual({ model: "generic-model", @@ -208,11 +216,48 @@ describe("memory embedding provider runtime resolution", () => { expect(close).toHaveBeenCalledOnce(); }); - it("keeps memory-specific adapters authoritative during dual registration", () => { - const memoryAdapter = createCapabilityAdapter("dual"); - registerMemoryEmbeddingProvider(memoryAdapter); - registerEmbeddingProvider({ id: "dual", create: async () => ({ provider: null }) }); + it("preserves memory policy and batching after generic registration", async () => { + const embedQuery = vi.fn(async () => [1, 2]); + const embedBatch = vi.fn(async (texts: string[]) => texts.map(() => [3, 4])); + const acquireLocalService = vi.fn(); + const create = vi.fn(async (options: unknown) => { + expect(options).toMatchObject({ acquireLocalService }); + return { + provider: { + id: "migrated-memory", + model: "migrated-model", + embedQuery, + embedBatch, + }, + runtime: { + id: "migrated-memory", + sourceWideBatchEmbed: true, + }, + }; + }); + const memoryAdapter = { + id: "migrated-memory", + autoSelectPriority: 20, + allowExplicitWhenConfiguredAuto: true, + create, + } satisfies MemoryEmbeddingProviderAdapter; + registerEmbeddingProvider(adaptMemoryEmbeddingProviderAdapter(memoryAdapter)); - expect(runtimeModule.getMemoryEmbeddingProvider("dual")).toBe(memoryAdapter); + const adapter = runtimeModule.getMemoryEmbeddingProvider("migrated-memory"); + expect(adapter).toMatchObject({ + id: "migrated-memory", + autoSelectPriority: 20, + allowExplicitWhenConfiguredAuto: true, + }); + const result = await adapter?.create({ + config: {}, + model: "migrated-model", + acquireLocalService, + } as never); + await expect(result?.provider?.embedQuery("query")).resolves.toEqual([1, 2]); + await expect(result?.provider?.embedBatch(["document"])).resolves.toEqual([[3, 4]]); + expect(result?.runtime?.sourceWideBatchEmbed).toBe(true); + expect(embedQuery).toHaveBeenCalledWith("query", { inputType: "query" }); + expect(embedBatch).toHaveBeenCalledWith(["document"], { inputType: "document" }); }); }); diff --git a/src/plugins/memory-embedding-provider-runtime.ts b/src/plugins/memory-embedding-provider-runtime.ts index 13fcd36e3d6a..c79101a523d2 100644 --- a/src/plugins/memory-embedding-provider-runtime.ts +++ b/src/plugins/memory-embedding-provider-runtime.ts @@ -1,58 +1,32 @@ // Runtime bridge for plugin-provided memory embedding providers. import type { OpenClawConfig } from "../config/types.openclaw.js"; -import { readConfiguredProviderApiId } from "./embedding-provider-config.js"; -import { - getRuntimeEmbeddingProviderAdapter, - listRuntimeEmbeddingProviderAdapters, - resolveRuntimeEmbeddingProviderLookupIds, -} from "./embedding-provider-runtime-shared.js"; -import { getEmbeddingProvider } from "./embedding-provider-runtime.js"; +import { getEmbeddingProvider, listEmbeddingProviders } from "./embedding-provider-runtime.js"; import type { EmbeddingProvider, EmbeddingProviderAdapter, EmbeddingProviderCreateOptions, } from "./embedding-provider-types.js"; -import { - getRegisteredMemoryEmbeddingProvider, - listRegisteredMemoryEmbeddingProviders, - type MemoryEmbeddingProvider, - type MemoryEmbeddingProviderAdapter, - type MemoryEmbeddingProviderCreateOptions, +import { listRegisteredEmbeddingProviders } from "./embedding-providers.js"; +import type { + MemoryEmbeddingProvider, + MemoryEmbeddingProviderAdapter, + MemoryEmbeddingProviderCreateOptions, } from "./memory-embedding-providers.js"; const LOCAL_EMBEDDING_RUNTIME_FACTS = Symbol.for("openclaw.localEmbeddingRuntimeFacts"); -export { listRegisteredMemoryEmbeddingProviders }; - /** Lists registered memory embedding provider adapters without registry metadata. */ export function listRegisteredMemoryEmbeddingProviderAdapters(): MemoryEmbeddingProviderAdapter[] { - return listRegisteredMemoryEmbeddingProviders().map((entry) => entry.adapter); + return listRegisteredEmbeddingProviders().map((entry) => + adaptEmbeddingProviderAdapter(entry.adapter), + ); } /** Lists memory embedding providers from runtime config and registered adapters. */ export function listMemoryEmbeddingProviders( cfg?: OpenClawConfig, ): MemoryEmbeddingProviderAdapter[] { - return listRuntimeEmbeddingProviderAdapters({ - key: "memoryEmbeddingProviders", - cfg, - registered: listRegisteredMemoryEmbeddingProviderAdapters(), - }); -} - -function resolveConfiguredMemoryEmbeddingProviderId( - providerId: string, - cfg?: OpenClawConfig, -): string | undefined { - return readConfiguredProviderApiId({ providerId, cfg }); -} - -function resolveMemoryEmbeddingProviderLookupIds(id: string, cfg?: OpenClawConfig): string[] { - return resolveRuntimeEmbeddingProviderLookupIds({ - id, - cfg, - resolveConfiguredProviderId: resolveConfiguredMemoryEmbeddingProviderId, - }); + return listEmbeddingProviders(cfg).map(adaptEmbeddingProviderAdapter); } function adaptEmbeddingProvider(provider: EmbeddingProvider): MemoryEmbeddingProvider { @@ -110,18 +84,6 @@ export function getMemoryEmbeddingProvider( id: string, cfg?: OpenClawConfig, ): MemoryEmbeddingProviderAdapter | undefined { - const memoryAdapter = getRuntimeEmbeddingProviderAdapter({ - key: "memoryEmbeddingProviders", - cfg, - lookupIds: resolveMemoryEmbeddingProviderLookupIds(id, cfg), - getRegisteredProvider: getRegisteredMemoryEmbeddingProvider, - }); - if (memoryAdapter) { - return memoryAdapter; - } - - // Resolve the shipped generic provider contract at its registry owner so all - // memory consumers share one query/document and multimodal adaptation path. const embeddingAdapter = getEmbeddingProvider(id, cfg); return embeddingAdapter ? adaptEmbeddingProviderAdapter(embeddingAdapter) : undefined; } diff --git a/src/plugins/memory-embedding-providers.test.ts b/src/plugins/memory-embedding-providers.test.ts deleted file mode 100644 index 8476e39edbde..000000000000 --- a/src/plugins/memory-embedding-providers.test.ts +++ /dev/null @@ -1,180 +0,0 @@ -// Covers plugin-provided memory embedding provider registration. -import { afterEach, beforeEach, describe, expect, it } from "vitest"; -import { - clearMemoryEmbeddingProviders, - getRegisteredMemoryEmbeddingProvider, - listRegisteredMemoryEmbeddingProviders, - registerMemoryEmbeddingProvider, - restoreRegisteredMemoryEmbeddingProviders, - type MemoryEmbeddingProviderAdapter, -} from "./memory-embedding-providers.js"; -import { createEmptyPluginRegistry } from "./registry-empty.js"; -import { withPluginRegistrationContext } from "./runtime.js"; - -const INITIAL_REGISTERED_MEMORY_EMBEDDING_PROVIDERS = listRegisteredMemoryEmbeddingProviders(); - -function createAdapter(id: string): MemoryEmbeddingProviderAdapter { - return { - id, - create: async () => ({ provider: null }), - }; -} - -function expectRegisteredProviderEntry( - id: string, - entry: { - adapter: MemoryEmbeddingProviderAdapter; - ownerPluginId?: string; - }, -) { - expect(getRegisteredMemoryEmbeddingProvider(id)).toEqual(entry); -} - -function createOwnedAdapterEntry(id: string) { - return { - adapter: createAdapter(id), - ownerPluginId: "memory-core", - }; -} - -function expectRegisteredProviderState(params: { - entry: { - adapter: MemoryEmbeddingProviderAdapter; - ownerPluginId?: string; - }; - expectedList?: Array<{ - adapter: MemoryEmbeddingProviderAdapter; - ownerPluginId?: string; - }>; -}) { - expectRegisteredProviderEntry(params.entry.adapter.id, params.entry); - if (params.expectedList) { - expect(listRegisteredMemoryEmbeddingProviders()).toEqual(params.expectedList); - } -} - -function expectMemoryEmbeddingProviderIds(expectedIds: readonly string[]) { - expect(listRegisteredMemoryEmbeddingProviders().map((entry) => entry.adapter.id)).toEqual([ - ...expectedIds, - ]); -} - -function expectCurrentMemoryEmbeddingProvider( - id: string, - adapter: MemoryEmbeddingProviderAdapter | undefined, -) { - expect(getRegisteredMemoryEmbeddingProvider(id)?.adapter).toBe(adapter); -} - -function expectMemoryEmbeddingProviderState(params: { - expectedIds: readonly string[]; - expectedCurrent?: { id: string; adapter: MemoryEmbeddingProviderAdapter }; -}) { - if (params.expectedCurrent) { - expectCurrentMemoryEmbeddingProvider(params.expectedCurrent.id, params.expectedCurrent.adapter); - } - expectMemoryEmbeddingProviderIds(params.expectedIds); -} - -function expectRegisteredProviderSnapshotCase(params: { - entry: { - adapter: MemoryEmbeddingProviderAdapter; - ownerPluginId?: string; - }; - setup: (entry: { adapter: MemoryEmbeddingProviderAdapter; ownerPluginId?: string }) => void; - expectedList?: Array<{ - adapter: MemoryEmbeddingProviderAdapter; - ownerPluginId?: string; - }>; -}) { - params.setup(params.entry); - expectRegisteredProviderState({ - entry: params.entry, - ...(params.expectedList ? { expectedList: params.expectedList } : {}), - }); -} - -beforeEach(() => { - clearMemoryEmbeddingProviders(); -}); - -afterEach(() => { - restoreRegisteredMemoryEmbeddingProviders(INITIAL_REGISTERED_MEMORY_EMBEDDING_PROVIDERS); -}); - -describe("memory embedding provider registry", () => { - it("registers and lists adapters in insertion order", () => { - const alpha = createAdapter("alpha"); - const beta = createAdapter("beta"); - registerMemoryEmbeddingProvider(alpha); - registerMemoryEmbeddingProvider(beta); - - expectMemoryEmbeddingProviderState({ - expectedIds: ["alpha", "beta"], - expectedCurrent: { id: "alpha", adapter: alpha }, - }); - }); - - it.each([ - { - name: "tracks owner plugin ids in registered snapshots", - entry: createOwnedAdapterEntry("alpha"), - setup: (entry: { adapter: MemoryEmbeddingProviderAdapter; ownerPluginId?: string }) => - registerMemoryEmbeddingProvider(entry.adapter, { ownerPluginId: entry.ownerPluginId }), - expectList: true, - }, - { - name: "restores registered snapshots with owner metadata", - entry: createOwnedAdapterEntry("beta"), - setup: (entry: { adapter: MemoryEmbeddingProviderAdapter; ownerPluginId?: string }) => - restoreRegisteredMemoryEmbeddingProviders([entry]), - expectList: false, - }, - ] as const)("$name", ({ entry, setup, expectList }) => { - expectRegisteredProviderSnapshotCase({ - entry, - setup, - ...(expectList ? { expectedList: [entry] } : {}), - }); - }); - - it("clears the registry", () => { - registerMemoryEmbeddingProvider(createAdapter("alpha")); - - clearMemoryEmbeddingProviders(); - - expectMemoryEmbeddingProviderIds([]); - }); - - it("stores adapters in the active registry", () => { - const alpha = createAdapter("alpha"); - registerMemoryEmbeddingProvider(alpha, { ownerPluginId: "memory-core" }); - - expect(getRegisteredMemoryEmbeddingProvider("alpha")).toEqual({ - adapter: alpha, - ownerPluginId: "memory-core", - }); - }); - - it("uses builder ownership without displacing another plugin's adapter", () => { - const building = createEmptyPluginRegistry(); - const original = createAdapter("shared"); - building.memoryEmbeddingProviders.push({ - pluginId: "first-plugin", - provider: original, - source: "runtime", - }); - - expect(() => - withPluginRegistrationContext(building, "failing-plugin", () => { - registerMemoryEmbeddingProvider(createAdapter("shared")); - }), - ).toThrow("memory embedding provider shared already registered by first-plugin"); - expect(building.memoryEmbeddingProviders[0]?.provider).toBe(original); - - withPluginRegistrationContext(building, "builder-plugin", () => { - registerMemoryEmbeddingProvider(createAdapter("owned")); - }); - expect(building.memoryEmbeddingProviders[1]?.pluginId).toBe("builder-plugin"); - }); -}); diff --git a/src/plugins/memory-embedding-providers.ts b/src/plugins/memory-embedding-providers.ts index 5953e90ac107..f88e8c2b6323 100644 --- a/src/plugins/memory-embedding-providers.ts +++ b/src/plugins/memory-embedding-providers.ts @@ -1,12 +1,17 @@ import type { + EmbeddingInput, + EmbeddingProvider, + EmbeddingProviderAdapter, + EmbeddingProviderCallOptions, + EmbeddingProviderCreateOptions, +} from "./embedding-provider-types.js"; +import type { + MemoryEmbeddingProvider, MemoryEmbeddingProviderAdapter, - RegisteredMemoryEmbeddingProvider, + MemoryEmbeddingProviderCreateOptions, } from "./registry-contribution-types.js"; -import { - assertDirectPluginRegistrationReplacement, - requireActivePluginRegistry, - resolveDirectPluginRegistrationOwner, -} from "./runtime.js"; + +const LOCAL_EMBEDDING_RUNTIME_FACTS = Symbol.for("openclaw.localEmbeddingRuntimeFacts"); export type { MemoryEmbeddingBatchChunk, @@ -18,64 +23,94 @@ export type { MemoryEmbeddingProviderCreateResult, MemoryEmbeddingProviderIndexIdentity, MemoryEmbeddingProviderRuntime, - RegisteredMemoryEmbeddingProvider, } from "./registry-contribution-types.js"; -function getMemoryEmbeddingProviders(): RegisteredMemoryEmbeddingProvider[] { - return requireActivePluginRegistry().memoryEmbeddingProviders.map((entry) => ({ - adapter: entry.provider, - ownerPluginId: entry.pluginId || undefined, - })); +function embeddingInputText(input: EmbeddingInput): string { + return typeof input === "string" ? input : input.text; } -export function registerMemoryEmbeddingProvider( - adapter: MemoryEmbeddingProviderAdapter, - options?: { ownerPluginId?: string }, -): void { - const registry = requireActivePluginRegistry(); - const pluginId = resolveDirectPluginRegistrationOwner(options?.ownerPluginId) ?? ""; - const entry = { - pluginId, - provider: adapter, - source: "runtime", +function memoryCreateOptions( + options: EmbeddingProviderCreateOptions, +): Parameters[0] { + // Runtime callers may carry memory-owner extensions such as acquireLocalService. + // Preserve them while translating the two fields renamed by the generic contract. + const { dimensions, taskType, ...base } = options; + return { + ...base, + fallback: "none", + ...(typeof dimensions === "number" ? { outputDimensionality: dimensions } : {}), + taskType: taskType as MemoryEmbeddingProviderCreateOptions["taskType"], }; - const index = registry.memoryEmbeddingProviders.findIndex( - (registration) => registration.provider.id === adapter.id, - ); - if (index !== -1) { - assertDirectPluginRegistrationReplacement( - registry.memoryEmbeddingProviders[index]?.pluginId || undefined, - `memory embedding provider ${adapter.id}`, - ); - } - if (index === -1) { - registry.memoryEmbeddingProviders.push(entry); - } else { - registry.memoryEmbeddingProviders.splice(index, 1, entry); - } } -export function getRegisteredMemoryEmbeddingProvider( - id: string, -): RegisteredMemoryEmbeddingProvider | undefined { - return getMemoryEmbeddingProviders().find((entry) => entry.adapter.id === id); +type MemoryEmbeddingInput = Parameters< + NonNullable +>[0][number]; + +function memoryEmbeddingInput(input: EmbeddingInput): MemoryEmbeddingInput { + return typeof input === "string" ? { text: input } : input; } -export function listRegisteredMemoryEmbeddingProviders(): RegisteredMemoryEmbeddingProvider[] { - return getMemoryEmbeddingProviders(); -} +function adaptMemoryEmbeddingProvider(provider: MemoryEmbeddingProvider): EmbeddingProvider { + const embedBatch = async ( + inputs: EmbeddingInput[], + options?: EmbeddingProviderCallOptions, + ): Promise => { + if (options?.inputType === "query") { + return await Promise.all( + inputs.map((input) => provider.embedQuery(embeddingInputText(input), options)), + ); + } + if (provider.embedBatchInputs) { + return await provider.embedBatchInputs(inputs.map(memoryEmbeddingInput), options); + } + return await provider.embedBatch(inputs.map(embeddingInputText), options); + }; -export function restoreRegisteredMemoryEmbeddingProviders( - entries: RegisteredMemoryEmbeddingProvider[], -): void { - clearMemoryEmbeddingProviders(); - for (const entry of entries) { - registerMemoryEmbeddingProvider(entry.adapter, { - ownerPluginId: entry.ownerPluginId, + const adapted: EmbeddingProvider = { + id: provider.id, + model: provider.model, + ...(typeof provider.maxInputTokens === "number" + ? { maxInputTokens: provider.maxInputTokens } + : {}), + embed: async (input, options) => { + if (options?.inputType === "query") { + return await provider.embedQuery(embeddingInputText(input), options); + } + return (await embedBatch([input], options))[0] ?? []; + }, + embedBatch, + ...(provider.close ? { close: async () => await provider.close?.() } : {}), + }; + const getRuntimeFacts = Reflect.get(provider, LOCAL_EMBEDDING_RUNTIME_FACTS); + if (typeof getRuntimeFacts === "function") { + Object.defineProperty(adapted, LOCAL_EMBEDDING_RUNTIME_FACTS, { + enumerable: false, + value: getRuntimeFacts, }); } + return adapted; } -export function clearMemoryEmbeddingProviders(): void { - requireActivePluginRegistry().memoryEmbeddingProviders.length = 0; +/** Adapt a memory-aware provider implementation to the generic registration contract. */ +export function adaptMemoryEmbeddingProviderAdapter( + adapter: MemoryEmbeddingProviderAdapter, +): EmbeddingProviderAdapter { + const { create: _create, resolveIndexIdentity, ...metadata } = adapter; + return { + ...metadata, + ...(resolveIndexIdentity + ? { + resolveIndexIdentity: (options: EmbeddingProviderCreateOptions) => + resolveIndexIdentity(memoryCreateOptions(options)), + } + : {}), + create: async (options) => { + const result = await adapter.create(memoryCreateOptions(options)); + return { + ...result, + provider: result.provider ? adaptMemoryEmbeddingProvider(result.provider) : null, + }; + }, + }; } diff --git a/src/plugins/official-external-plugin-catalog.test.ts b/src/plugins/official-external-plugin-catalog.test.ts index df0faf6d7d9c..1971a123ee03 100644 --- a/src/plugins/official-external-plugin-catalog.test.ts +++ b/src/plugins/official-external-plugin-catalog.test.ts @@ -313,7 +313,6 @@ describe("official external plugin catalog", () => { const contracts = getOfficialExternalPluginCatalogManifest(entry)?.contracts; expect(contracts?.embeddingProviders).toEqual(["deepinfra"]); - expect(contracts?.memoryEmbeddingProviders).toBeUndefined(); }); it("does not allow malformed feed wrappers to count as feed documents", () => { @@ -2084,7 +2083,7 @@ describe("official external plugin catalog", () => { ]); }); - it("lists Voyage as an official external memory embedding provider", () => { + it("lists Voyage as an official external embedding provider", () => { const voyage = expectCatalogEntry("voyage"); const manifest = getOfficialExternalPluginCatalogManifest(voyage); @@ -2095,7 +2094,7 @@ describe("official external plugin catalog", () => { defaultChoice: "npm", minHostVersion: ">=2026.7.2", }); - expect(manifest?.contracts?.memoryEmbeddingProviders).toEqual(["voyage"]); + expect(manifest?.contracts?.embeddingProviders).toEqual(["voyage"]); expect(manifest?.providers).toEqual([ expect.objectContaining({ id: "voyage", @@ -2249,7 +2248,7 @@ describe("official external plugin catalog", () => { }), ]); expect(manifest?.contracts).toMatchObject({ - memoryEmbeddingProviders: ["mistral"], + embeddingProviders: ["mistral"], mediaUnderstandingProviders: ["mistral"], realtimeTranscriptionProviders: ["mistral"], }); @@ -2365,7 +2364,7 @@ describe("official external plugin catalog", () => { ).toEqual(["groq", "moonshot", "zai"]); expect( resolveOfficialExternalProviderContractPluginIds({ - contract: "memoryEmbeddingProviders", + contract: "embeddingProviders", providerIds: new Set(["voyage"]), }), ).toEqual(["voyage"]); diff --git a/src/plugins/official-external-plugin-catalog.ts b/src/plugins/official-external-plugin-catalog.ts index d95f5691eb11..a14369349e4f 100644 --- a/src/plugins/official-external-plugin-catalog.ts +++ b/src/plugins/official-external-plugin-catalog.ts @@ -280,7 +280,6 @@ type FetchLike = (input: RequestInfo | URL, init?: RequestInit) => Promise void; /** Register an additive memory-adjacent search/read corpus supplement (non-exclusive). */ registerMemoryCorpusSupplement: (supplement: MemoryCorpusSupplement) => void; - /** - * Register a memory embedding provider adapter. Multiple adapters may coexist. - * @deprecated New embedding providers should use `registerEmbeddingProvider` - * and `contracts.embeddingProviders`. This memory-specific seam is retained - * while existing memory providers migrate. - */ - registerMemoryEmbeddingProvider: (adapter: MemoryEmbeddingProviderAdapter) => void; resolvePath: (input: string) => string; /** Register a lifecycle hook handler */ on: ( diff --git a/src/plugins/registry-api.ts b/src/plugins/registry-api.ts index 11bb2591b7c7..b5b0569ac6c7 100644 --- a/src/plugins/registry-api.ts +++ b/src/plugins/registry-api.ts @@ -104,7 +104,6 @@ export function createPluginApiFactory( registerMemoryPromptSupplement, registerMemoryPromptPreparation, registerMemoryCorpusSupplement, - registerMemoryEmbeddingProvider, registerCli, registerChannel, } = registrars; @@ -368,8 +367,6 @@ export function createPluginApiFactory( registerMemoryPromptPreparation(record, prepare), registerMemoryCorpusSupplement: (supplement) => registerMemoryCorpusSupplement(record, supplement), - registerMemoryEmbeddingProvider: (adapter) => - registerMemoryEmbeddingProvider(record, adapter), on: (hookName, handler, opts) => registerTypedHook(record, hookName, handler, opts, params.hookPolicy), } diff --git a/src/plugins/registry-contribution-types.ts b/src/plugins/registry-contribution-types.ts index a52332664b76..e177c65654f3 100644 --- a/src/plugins/registry-contribution-types.ts +++ b/src/plugins/registry-contribution-types.ts @@ -132,11 +132,6 @@ export type MemoryEmbeddingProviderAdapter = Omit< shouldContinueAutoSelection?: (err: unknown) => boolean; }; -export type RegisteredMemoryEmbeddingProvider = { - adapter: MemoryEmbeddingProviderAdapter; - ownerPluginId?: string; -}; - export type MemoryPromptSectionParams = { availableTools: Set; citationsMode?: MemoryCitationsMode; diff --git a/src/plugins/registry-empty.ts b/src/plugins/registry-empty.ts index 03ac792939c0..6e6aa1038248 100644 --- a/src/plugins/registry-empty.ts +++ b/src/plugins/registry-empty.ts @@ -30,7 +30,6 @@ export function createEmptyPluginRegistry(): PluginRegistry { codexAppServerExtensionFactories: [], agentToolResultMiddlewareOwners: [], agentToolResultMiddlewares: [], - memoryEmbeddingProviders: [], agentHarnesses: [], pluginRuntimeArtifacts: new Map(), compactionProviders: [], diff --git a/src/plugins/registry-registrars-memory.ts b/src/plugins/registry-registrars-memory.ts index 550f63521c07..64312db58ce7 100644 --- a/src/plugins/registry-registrars-memory.ts +++ b/src/plugins/registry-registrars-memory.ts @@ -79,50 +79,10 @@ export function createMemoryRegistrars(state: PluginRegistryState) { registry.memoryCorpusSupplements.push({ pluginId: record.id, supplement }); }; - const registerMemoryEmbeddingProvider = ( - record: PluginRecord, - adapter: Parameters[0], - ) => { - if (hasKind(record.kind, "memory")) { - if (!requireMemorySlot(record, "embedding provider")) { - return; - } - } else if (!(record.contracts?.memoryEmbeddingProviders ?? []).includes(adapter.id)) { - pushDiagnostic({ - level: "error", - pluginId: record.id, - source: record.source, - message: `plugin must own memory slot or declare contracts.memoryEmbeddingProviders for adapter: ${adapter.id}`, - }); - return; - } - const existing = registry.memoryEmbeddingProviders.find( - (entry) => entry.provider.id === adapter.id, - ); - if (existing) { - const ownerDetail = existing.pluginId ? ` (owner: ${existing.pluginId})` : ""; - pushDiagnostic({ - level: "error", - pluginId: record.id, - source: record.source, - message: `memory embedding provider already registered: ${adapter.id}${ownerDetail}`, - }); - return; - } - registry.memoryEmbeddingProviders.push({ - pluginId: record.id, - pluginName: record.name, - provider: adapter, - source: record.source, - rootDir: record.rootDir, - }); - }; - return { registerMemoryCapability, registerMemoryPromptSupplement, registerMemoryPromptPreparation, registerMemoryCorpusSupplement, - registerMemoryEmbeddingProvider, }; } diff --git a/src/plugins/registry-registrars-tools-hooks.ts b/src/plugins/registry-registrars-tools-hooks.ts index cbf27555ace7..88344d89b93d 100644 --- a/src/plugins/registry-registrars-tools-hooks.ts +++ b/src/plugins/registry-registrars-tools-hooks.ts @@ -16,7 +16,6 @@ import { } from "./agent-tool-result-middleware.js"; import { CODEX_APP_SERVER_EXTENSION_RUNTIME_ID } from "./codex-app-server-extension-factory.js"; import type { CodexAppServerExtensionFactory } from "./codex-app-server-extension-types.js"; -import { getPluginCompatRecord } from "./compat/registry.js"; import { resolveTypedHookTimeoutMs, type PluginRegistryState, @@ -33,9 +32,7 @@ import { } from "./tool-contracts.js"; import { normalizePluginToolMatcher } from "./tool-hook-matcher.js"; import { - DEPRECATED_PLUGIN_HOOKS, isConversationHookName, - isDeprecatedPluginHookName, isPluginHookAgentTrigger, isPluginHookName, isPromptInjectionHookName, @@ -52,8 +49,6 @@ import type { PluginHookRegistration as TypedPluginHookRegistration, } from "./types.js"; -const LEGACY_SUBAGENT_SPAWNING_HOOK_COMPAT = getPluginCompatRecord("legacy-subagent-spawning-hook"); - function normalizeEligibleTriggers(value: unknown) { if (!Array.isArray(value)) { return undefined; @@ -65,22 +60,6 @@ function normalizeEligibleTriggers(value: unknown) { return uniqueValues(triggers); } -function formatDeprecatedTypedHookDiagnostic(hookName: PluginHookName): string | undefined { - if (!isDeprecatedPluginHookName(hookName)) { - return undefined; - } - const deprecation = DEPRECATED_PLUGIN_HOOKS[hookName]; - const compat = - hookName === "subagent_spawning" ? LEGACY_SUBAGENT_SPAWNING_HOOK_COMPAT : undefined; - const removeAfter = compat?.removeAfter ?? deprecation.removeAfter ?? "a future breaking release"; - const code = compat?.code ?? "deprecated-plugin-hook"; - return ( - `typed hook "${hookName}" is deprecated (${code}); ` + - `${deprecation.reason} Use ${deprecation.replacement}. ` + - `This compatibility hook will be removed after ${removeAfter}.` - ); -} - function canRegisterInstalledTrustedHook(record: PluginRecord): boolean { return record.origin === "bundled" || (record.enabled && record.explicitlyEnabled === true); } @@ -415,15 +394,6 @@ export function createToolHookRegistrars(state: PluginRegistryState) { }); return; } - const diagnostic = formatDeprecatedTypedHookDiagnostic(hookName); - if (diagnostic) { - pushDiagnostic({ - level: "warn", - pluginId: record.id, - source: record.source, - message: diagnostic, - }); - } if (policy?.allowPromptInjection === false && isPromptInjectionHookName(hookName)) { pushDiagnostic({ level: "warn", diff --git a/src/plugins/registry-runtime.ts b/src/plugins/registry-runtime.ts index df53db5cd523..4e8c74416a14 100644 --- a/src/plugins/registry-runtime.ts +++ b/src/plugins/registry-runtime.ts @@ -971,11 +971,6 @@ export function createPluginRuntimeResolver(state: PluginRegistryState) { enumerable: true, value: runEmbeddedAgent, }, - runEmbeddedPiAgent: { - configurable: true, - enumerable: true, - value: runEmbeddedAgent, - }, session: { configurable: true, enumerable: true, diff --git a/src/plugins/registry-types.ts b/src/plugins/registry-types.ts index b024e158c7bb..da10e87ca006 100644 --- a/src/plugins/registry-types.ts +++ b/src/plugins/registry-types.ts @@ -43,7 +43,6 @@ import type { PluginKind } from "./plugin-kind.types.js"; import type { ContextEngineRegistration, MemoryCorpusSupplementRegistration, - MemoryEmbeddingProviderAdapter, MemoryPluginCapabilityRegistration, MemoryPromptPreparationRegistration, MemoryPromptSupplementRegistration, @@ -253,8 +252,6 @@ type PluginWebFetchProviderRegistration = PluginOwnedProviderRegistration; type PluginWorkerProviderRegistration = PluginOwnedProviderRegistration; type PluginMigrationProviderRegistration = PluginOwnedProviderRegistration; -type PluginMemoryEmbeddingProviderRegistration = - PluginOwnedProviderRegistration; type PluginCodexAppServerExtensionFactoryRegistration = { pluginId: string; pluginName?: string; @@ -494,7 +491,6 @@ export type PluginRecord = { webSearchProviderIds: string[]; migrationProviderIds: string[]; contextEngineIds?: string[]; - memoryEmbeddingProviderIds: string[]; agentHarnessIds: string[]; cliCommands: string[]; services: string[]; @@ -540,7 +536,6 @@ export type PluginRegistry = { codexAppServerExtensionFactories: PluginCodexAppServerExtensionFactoryRegistration[]; agentToolResultMiddlewareOwners: PluginAgentToolResultMiddlewareOwner[]; agentToolResultMiddlewares: PluginAgentToolResultMiddlewareRegistration[]; - memoryEmbeddingProviders: PluginMemoryEmbeddingProviderRegistration[]; agentHarnesses: PluginAgentHarnessRegistration[]; pluginRuntimeArtifacts: Map; compactionProviders: RegisteredCompactionProvider[]; diff --git a/src/plugins/registry.runtime-config.test.ts b/src/plugins/registry.runtime-config.test.ts index 4a1e758a3160..7390a07dfc74 100644 --- a/src/plugins/registry.runtime-config.test.ts +++ b/src/plugins/registry.runtime-config.test.ts @@ -569,7 +569,6 @@ describe("plugin registry runtime config scope", () => { ) as unknown as PluginRuntime["agent"]["runEmbeddedAgent"]; Object.defineProperties(runtime.agent, { runEmbeddedAgent: { configurable: true, value: runEmbeddedAgent }, - runEmbeddedPiAgent: { configurable: true, value: runEmbeddedAgent }, }); const gatewayRequest = vi.fn(async () => ({ ok: true })); runtime.gateway = { diff --git a/src/plugins/registry.test-fixtures.ts b/src/plugins/registry.test-fixtures.ts index 7a098730d0ef..58eaabd00b70 100644 --- a/src/plugins/registry.test-fixtures.ts +++ b/src/plugins/registry.test-fixtures.ts @@ -4,5 +4,3 @@ import type { PluginRegistry } from "./registry.js"; export * from "./registry.js"; export type PluginProviderRegistration = PluginRegistry["providers"][number]; -export type PluginMemoryEmbeddingProviderRegistration = - PluginRegistry["memoryEmbeddingProviders"][number]; diff --git a/src/plugins/runtime/index.test.ts b/src/plugins/runtime/index.test.ts index 9404308a928d..33980d806280 100644 --- a/src/plugins/runtime/index.test.ts +++ b/src/plugins/runtime/index.test.ts @@ -344,12 +344,10 @@ describe("plugin runtime command execution", () => { }); expectFunctionKeys(runtime.agent as Record, [ "runEmbeddedAgent", - "runEmbeddedPiAgent", "normalizeThinkingLevel", "resolveThinkingPolicy", "resolveAgentDir", ]); - expect(runtime.agent.runEmbeddedPiAgent).toBe(runtime.agent.runEmbeddedAgent); expectFunctionKeys(runtime.agent.session as Record, [ "createSessionEntry", "getSessionEntry", diff --git a/src/plugins/runtime/runtime-agent.ts b/src/plugins/runtime/runtime-agent.ts index e1938179d262..85fb8bdade22 100644 --- a/src/plugins/runtime/runtime-agent.ts +++ b/src/plugins/runtime/runtime-agent.ts @@ -616,17 +616,12 @@ export function createRuntimeAgent(): PluginRuntime["agent"] { resolveAgentTimeoutMs, resolveCliBackendDispatchEligibility: resolveEmbeddedCliBackendDispatchEligibility, ensureAgentWorkspace, - } satisfies Omit & - Partial>; + } satisfies Omit & + Partial>; defineCachedValue(agentRuntime, "runEmbeddedAgent", () => createLazyRuntimeMethod(loadEmbeddedAgentRuntime, (runtime) => runtime.runPluginEmbeddedAgent), ); - defineCachedValue( - agentRuntime, - "runEmbeddedPiAgent", - () => (agentRuntime as PluginRuntime["agent"]).runEmbeddedAgent, - ); defineCachedValue(agentRuntime, "session", () => ({ resolveStorePath: resolveSessionStorePathCore, createSessionEntry, diff --git a/src/plugins/runtime/runtime-registry-loader.test.ts b/src/plugins/runtime/runtime-registry-loader.test.ts index 67534d391a23..58211c759842 100644 --- a/src/plugins/runtime/runtime-registry-loader.test.ts +++ b/src/plugins/runtime/runtime-registry-loader.test.ts @@ -103,7 +103,7 @@ import { ensurePluginRegistryLoaded } from "./runtime-registry-loader.js"; function useMemoryProviderOwner(params: { adapterId: string; - contract: "embeddingProviders" | "memoryEmbeddingProviders"; + contract: "embeddingProviders"; pluginId: string; }): void { mocks.resolvePluginMetadataSnapshot.mockReturnValue({ @@ -218,7 +218,7 @@ describe("ensurePluginRegistryLoaded", () => { it.each([ { adapterId: "gemini", - contract: "memoryEmbeddingProviders" as const, + contract: "embeddingProviders" as const, pluginId: "google", }, { @@ -255,7 +255,7 @@ describe("ensurePluginRegistryLoaded", () => { mocks.collectConfiguredMemoryEmbeddingProviderIds.mockReturnValue(new Set(["gemini"])); useMemoryProviderOwner({ adapterId: "gemini", - contract: "memoryEmbeddingProviders", + contract: "embeddingProviders", pluginId: "google", }); diff --git a/src/plugins/runtime/types-core.ts b/src/plugins/runtime/types-core.ts index 17b5b4c7d6df..e6e2e094c0a2 100644 --- a/src/plugins/runtime/types-core.ts +++ b/src/plugins/runtime/types-core.ts @@ -347,8 +347,6 @@ export type PluginRuntimeCore = { params: PluginRuntimeThinkingPolicyRequest, ) => PluginRuntimeThinkingPolicy; runEmbeddedAgent: RuntimeRunEmbeddedAgent; - /** @deprecated Use runEmbeddedAgent. */ - runEmbeddedPiAgent: RuntimeRunEmbeddedAgent; resolveAgentTimeoutMs: typeof import("../../agents/timeout.js").resolveAgentTimeoutMs; /** * Shares the embedded runner's CLI-backend dispatch eligibility (route, diff --git a/src/plugins/status-snapshot.ts b/src/plugins/status-snapshot.ts index fcf26581e61e..9aff09628adc 100644 --- a/src/plugins/status-snapshot.ts +++ b/src/plugins/status-snapshot.ts @@ -79,7 +79,6 @@ function buildPluginRecordFromInstalledIndex( webFetchProviderIds: [...(manifest?.contracts?.webFetchProviders ?? [])], webSearchProviderIds: [...(manifest?.contracts?.webSearchProviders ?? [])], migrationProviderIds: [...(manifest?.contracts?.migrationProviders ?? [])], - memoryEmbeddingProviderIds: [...(manifest?.contracts?.memoryEmbeddingProviders ?? [])], agentHarnessIds: [], cliCommands: [], services: [], diff --git a/src/plugins/status.test-fixtures.ts b/src/plugins/status.test-fixtures.ts index 9121d2e62caf..8b59b99e9557 100644 --- a/src/plugins/status.test-fixtures.ts +++ b/src/plugins/status.test-fixtures.ts @@ -26,8 +26,6 @@ export function createInstalledPluginIndexSnapshot( export const HOOK_ONLY_MESSAGE = "is hook-only. This remains a supported compatibility path, but it has not migrated to explicit capability registration yet."; -export const DEPRECATED_MEMORY_EMBEDDING_PROVIDER_API_MESSAGE = - "uses deprecated memory-specific embedding provider API; use api.registerEmbeddingProvider and contracts.embeddingProviders for new embedding providers."; export const REMOVED_SESSION_TRANSCRIPT_FILE_API_MESSAGE = "references removed session/transcript file APIs; migrate to session identity, SessionTranscriptUpdate.target, and Gateway/runtime session helpers."; @@ -43,14 +41,6 @@ export function createCompatibilityNotice( severity: "info", message: HOOK_ONLY_MESSAGE, }; - case "deprecated-memory-embedding-provider-api": - return { - pluginId: params.pluginId, - code: params.code, - compatCode: "deprecated-memory-embedding-provider-api", - severity: "warn", - message: DEPRECATED_MEMORY_EMBEDDING_PROVIDER_API_MESSAGE, - }; case "removed-session-transcript-file-api": return { pluginId: params.pluginId, diff --git a/src/plugins/status.test-helpers.ts b/src/plugins/status.test-helpers.ts index baa5db0b4710..3c69a8cd6137 100644 --- a/src/plugins/status.test-helpers.ts +++ b/src/plugins/status.test-helpers.ts @@ -35,7 +35,6 @@ export function createPluginRecord( webSearchProviderIds: [], migrationProviderIds: [], contextEngineIds: [], - memoryEmbeddingProviderIds: [], agentHarnessIds: [], cliCommands: [], services: [], diff --git a/src/plugins/status.test.ts b/src/plugins/status.test.ts index 348017ce622e..fa3b0e0ae364 100644 --- a/src/plugins/status.test.ts +++ b/src/plugins/status.test.ts @@ -2,14 +2,12 @@ import { expectDefined } from "@openclaw/normalization-core"; import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; -import type { PluginMemoryEmbeddingProviderRegistration } from "./registry.test-fixtures.js"; import { createCompatibilityNotice, createCustomHook, createInstalledPluginIndexSnapshot, createPluginLoadResult, createPluginRecord, - DEPRECATED_MEMORY_EMBEDDING_PROVIDER_API_MESSAGE, HOOK_ONLY_MESSAGE, REMOVED_SESSION_TRANSCRIPT_FILE_API_MESSAGE, } from "./status.test-fixtures.js"; @@ -853,74 +851,6 @@ describe("plugin status reports", () => { expect(loadOpenClawPluginsMock).not.toHaveBeenCalled(); }); - it("warns external plugins off deprecated memory embedding provider registration", () => { - setSinglePluginLoadResult( - createPluginRecord({ - id: "legacy-memory-provider", - name: "Legacy Memory Provider", - memoryEmbeddingProviderIds: ["legacy-memory-provider"], - contracts: { memoryEmbeddingProviders: ["legacy-memory-provider"] }, - }), - ); - - expectCompatibilityOutput({ - notices: [ - createCompatibilityNotice({ - pluginId: "legacy-memory-provider", - code: "deprecated-memory-embedding-provider-api", - }), - ], - warnings: [`legacy-memory-provider ${DEPRECATED_MEMORY_EMBEDDING_PROVIDER_API_MESSAGE}`], - }); - }); - - it("warns when external plugins register memory embedding providers at runtime only", () => { - const runtimeProviderRegistration: PluginMemoryEmbeddingProviderRegistration = { - pluginId: "runtime-only-legacy-memory-provider", - pluginName: "Runtime Only Legacy Memory Provider", - provider: { - id: "runtime-only-legacy-memory-provider", - create: async () => ({ provider: null }), - }, - source: "/tmp/runtime-only-legacy-memory-provider/index.ts", - }; - setPluginLoadResult({ - plugins: [ - createPluginRecord({ - id: "runtime-only-legacy-memory-provider", - name: "Runtime Only Legacy Memory Provider", - }), - ], - memoryEmbeddingProviders: [runtimeProviderRegistration], - }); - - expectCompatibilityOutput({ - notices: [ - createCompatibilityNotice({ - pluginId: "runtime-only-legacy-memory-provider", - code: "deprecated-memory-embedding-provider-api", - }), - ], - warnings: [ - `runtime-only-legacy-memory-provider ${DEPRECATED_MEMORY_EMBEDDING_PROVIDER_API_MESSAGE}`, - ], - }); - }); - - it("does not surface bundled memory embedding migration debt as user warnings", () => { - setSinglePluginLoadResult( - createPluginRecord({ - id: "bundled-memory-provider", - name: "Bundled Memory Provider", - origin: "bundled", - memoryEmbeddingProviderIds: ["bundled-memory-provider"], - contracts: { memoryEmbeddingProviders: ["bundled-memory-provider"] }, - }), - ); - - expectNoCompatibilityWarnings(); - }); - it("warns external plugins when load diagnostics reference removed session file APIs", () => { setPluginLoadResult({ plugins: [ diff --git a/src/plugins/status.ts b/src/plugins/status.ts index 021915052abd..4d3996386f7d 100644 --- a/src/plugins/status.ts +++ b/src/plugins/status.ts @@ -59,10 +59,7 @@ export type { PluginCapabilityKind, PluginInspectShape } from "./inspect-shape.j export type PluginCompatibilityNotice = { pluginId: string; - code: - | "hook-only" - | "deprecated-memory-embedding-provider-api" - | "removed-session-transcript-file-api"; + code: "hook-only" | "removed-session-transcript-file-api"; compatCode: PluginCompatCode; severity: "warn" | "info"; message: string; @@ -124,7 +121,6 @@ export type PluginInspectReport = { function buildCompatibilityNoticesForInspect( inspect: Pick & { diagnostics: readonly PluginDiagnostic[]; - hasRuntimeMemoryEmbeddingProviderRegistration: boolean; }, ): PluginCompatibilityNotice[] { const warnings: PluginCompatibilityNotice[] = []; @@ -138,20 +134,6 @@ function buildCompatibilityNoticesForInspect( "is hook-only. This remains a supported compatibility path, but it has not migrated to explicit capability registration yet.", }); } - const usesMemoryEmbeddingProviderApi = - inspect.plugin.memoryEmbeddingProviderIds.length > 0 || - (inspect.plugin.contracts?.memoryEmbeddingProviders?.length ?? 0) > 0 || - inspect.hasRuntimeMemoryEmbeddingProviderRegistration; - if (usesMemoryEmbeddingProviderApi && inspect.plugin.origin !== "bundled") { - warnings.push({ - pluginId: inspect.plugin.id, - code: "deprecated-memory-embedding-provider-api", - compatCode: "deprecated-memory-embedding-provider-api", - severity: "warn", - message: - "uses deprecated memory-specific embedding provider API; use api.registerEmbeddingProvider and contracts.embeddingProviders for new embedding providers.", - }); - } if (usesRemovedSessionTranscriptFileApi(inspect)) { warnings.push({ pluginId: inspect.plugin.id, @@ -469,14 +451,10 @@ export function buildPluginInspectReport(params: { ]; } - const hasRuntimeMemoryEmbeddingProviderRegistration = report.memoryEmbeddingProviders.some( - (entry) => entry.pluginId === plugin.id, - ); const compatibility = buildCompatibilityNoticesForInspect({ plugin, shape, diagnostics, - hasRuntimeMemoryEmbeddingProviderRegistration, }); return { workspaceDir: report.workspaceDir, diff --git a/src/plugins/wired-hooks-subagent.test.ts b/src/plugins/wired-hooks-subagent.test.ts index bcf85e0fbb23..c62db0aeb7da 100644 --- a/src/plugins/wired-hooks-subagent.test.ts +++ b/src/plugins/wired-hooks-subagent.test.ts @@ -1,8 +1,8 @@ /** - * Test: subagent spawning, routing, progress, and terminal hook wiring. + * Test: subagent routing, progress, and terminal hook wiring. */ import { describe, expect, it, vi } from "vitest"; -import { addStaticTestHooks, createHookRunnerWithRegistry } from "./hooks.test-fixtures.js"; +import { createHookRunnerWithRegistry } from "./hooks.test-fixtures.js"; describe("subagent hook runner methods", () => { const baseRequester = { @@ -20,7 +20,6 @@ describe("subagent hook runner methods", () => { async function invokeSubagentHook(params: { hookName: - | "subagent_spawning" | "subagent_spawned" | "subagent_progress" | "subagent_delivery_target" @@ -35,40 +34,19 @@ describe("subagent hook runner methods", () => { } const { runner } = createHookRunnerWithRegistry([{ hookName: params.hookName, handler }]); const result = - params.hookName === "subagent_spawning" - ? await runner.runSubagentSpawning(params.event as never, params.ctx as never) - : params.hookName === "subagent_spawned" - ? await runner.runSubagentSpawned(params.event as never, params.ctx as never) - : params.hookName === "subagent_progress" - ? await runner.runSubagentProgress(params.event as never, params.ctx as never) - : params.hookName === "subagent_delivery_target" - ? await runner.runSubagentDeliveryTarget(params.event as never, params.ctx as never) - : await runner.runSubagentEnded(params.event as never, params.ctx as never); + params.hookName === "subagent_spawned" + ? await runner.runSubagentSpawned(params.event as never, params.ctx as never) + : params.hookName === "subagent_progress" + ? await runner.runSubagentProgress(params.event as never, params.ctx as never) + : params.hookName === "subagent_delivery_target" + ? await runner.runSubagentDeliveryTarget(params.event as never, params.ctx as never) + : await runner.runSubagentEnded(params.event as never, params.ctx as never); expect(handler).toHaveBeenCalledWith(params.event, params.ctx); return result; } it.each([ - { - name: "runSubagentSpawning invokes registered subagent_spawning hooks", - hookName: "subagent_spawning" as const, - methodName: "runSubagentSpawning" as const, - event: { - childSessionKey: "agent:main:subagent:child", - agentId: "main", - label: "research", - mode: "session" as const, - requester: baseRequester, - threadRequested: true, - }, - ctx: { - childSessionKey: "agent:main:subagent:child", - requesterSessionKey: "agent:main:main", - }, - handlerResult: { status: "ok", threadBindingReady: true as const }, - expectedResult: { status: "ok", threadBindingReady: true }, - }, { name: "runSubagentSpawned invokes registered subagent_spawned hooks", hookName: "subagent_spawned" as const, @@ -168,76 +146,12 @@ describe("subagent hook runner methods", () => { it("hasHooks returns true for registered subagent hooks", () => { const { runner } = createHookRunnerWithRegistry([ - { hookName: "subagent_spawning", handler: vi.fn() }, { hookName: "subagent_delivery_target", handler: vi.fn() }, ]); - expect(runner.hasHooks("subagent_spawning")).toBe(true); expect(runner.hasHooks("subagent_delivery_target")).toBe(true); expect(runner.hasHooks("subagent_spawned")).toBe(false); expect(runner.hasHooks("subagent_progress")).toBe(false); expect(runner.hasHooks("subagent_ended")).toBe(false); }); - - it("runSubagentSpawning preserves higher-priority delivery origins", async () => { - const { registry, runner } = createHookRunnerWithRegistry([]); - addStaticTestHooks(registry, { - hookName: "subagent_spawning", - hooks: [ - { - pluginId: "high", - priority: 100, - result: { - status: "ok", - threadBindingReady: true, - deliveryOrigin: { - channel: "matrix", - accountId: "ops", - to: "room:!high:example", - threadId: "$high", - }, - }, - }, - { - pluginId: "low", - priority: 10, - result: { - status: "ok", - threadBindingReady: true, - deliveryOrigin: { - channel: "matrix", - accountId: "ops", - to: "room:!low:example", - threadId: "$low", - }, - }, - }, - ], - }); - - const result = await runner.runSubagentSpawning( - { - childSessionKey: "agent:main:subagent:child", - agentId: "main", - mode: "session", - requester: baseRequester, - threadRequested: true, - }, - { - childSessionKey: "agent:main:subagent:child", - requesterSessionKey: "agent:main:main", - }, - ); - - expect(result).toEqual({ - status: "ok", - threadBindingReady: true, - deliveryOrigin: { - channel: "matrix", - accountId: "ops", - to: "room:!high:example", - threadId: "$high", - }, - }); - }); }); diff --git a/src/trajectory/metadata.test.ts b/src/trajectory/metadata.test.ts index 4932a58771fd..402b8a4d28b6 100644 --- a/src/trajectory/metadata.test.ts +++ b/src/trajectory/metadata.test.ts @@ -145,7 +145,6 @@ describe("trajectory metadata", () => { webFetchProviderIds: [], webSearchProviderIds: [], migrationProviderIds: [], - memoryEmbeddingProviderIds: [], agentHarnessIds: ["openclaw"], cliCommands: [], services: [], diff --git a/src/trajectory/metadata.ts b/src/trajectory/metadata.ts index b8603dc7dd3a..cd6233395dac 100644 --- a/src/trajectory/metadata.ts +++ b/src/trajectory/metadata.ts @@ -135,7 +135,6 @@ function buildPluginsFromActiveRegistry() { musicGenerationProviderIds: toSortedUniqueStrings(plugin.musicGenerationProviderIds), webFetchProviderIds: toSortedUniqueStrings(plugin.webFetchProviderIds), webSearchProviderIds: toSortedUniqueStrings(plugin.webSearchProviderIds), - memoryEmbeddingProviderIds: toSortedUniqueStrings(plugin.memoryEmbeddingProviderIds), agentHarnessIds: toSortedUniqueStrings(plugin.agentHarnessIds), })) .toSorted((left, right) => left.id.localeCompare(right.id)),