From 647e5578690b2e0c89fbe1e2b6a45a84a614ef5a Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sun, 26 Apr 2026 04:02:53 -0700 Subject: [PATCH 1/8] docs(agent-workspace): rewrite with AccordionGroup for file map, Steps and Tabs for git backup, Warning callouts for sandbox and secret risks --- docs/concepts/agent-workspace.md | 289 +++++++++++++++---------------- 1 file changed, 136 insertions(+), 153 deletions(-) diff --git a/docs/concepts/agent-workspace.md b/docs/concepts/agent-workspace.md index 42a6d46735c..aba67a7e708 100644 --- a/docs/concepts/agent-workspace.md +++ b/docs/concepts/agent-workspace.md @@ -4,26 +4,23 @@ read_when: - You need to explain the agent workspace or its file layout - You want to back up or migrate an agent workspace title: "Agent workspace" +sidebarTitle: "Agent workspace" --- -The workspace is the agent's home. It is the only working directory used for -file tools and for workspace context. Keep it private and treat it as memory. +The workspace is the agent's home. It is the only working directory used for file tools and for workspace context. Keep it private and treat it as memory. -This is separate from `~/.openclaw/`, which stores config, credentials, and -sessions. +This is separate from `~/.openclaw/`, which stores config, credentials, and sessions. -**Important:** the workspace is the **default cwd**, not a hard sandbox. Tools -resolve relative paths against the workspace, but absolute paths can still reach -elsewhere on the host unless sandboxing is enabled. If you need isolation, use -[`agents.defaults.sandbox`](/gateway/sandboxing) (and/or per‑agent sandbox config). -When sandboxing is enabled and `workspaceAccess` is not `"rw"`, tools operate -inside a sandbox workspace under `~/.openclaw/sandboxes`, not your host workspace. + +The workspace is the **default cwd**, not a hard sandbox. Tools resolve relative paths against the workspace, but absolute paths can still reach elsewhere on the host unless sandboxing is enabled. If you need isolation, use [`agents.defaults.sandbox`](/gateway/sandboxing) (and/or per-agent sandbox config). + +When sandboxing is enabled and `workspaceAccess` is not `"rw"`, tools operate inside a sandbox workspace under `~/.openclaw/sandboxes`, not your host workspace. + ## Default location - Default: `~/.openclaw/workspace` -- If `OPENCLAW_PROFILE` is set and not `"default"`, the default becomes - `~/.openclaw/workspace-`. +- If `OPENCLAW_PROFILE` is set and not `"default"`, the default becomes `~/.openclaw/workspace-`. - Override in `~/.openclaw/openclaw.json`: ```json5 @@ -36,13 +33,13 @@ inside a sandbox workspace under `~/.openclaw/sandboxes`, not your host workspac } ``` -`openclaw onboard`, `openclaw configure`, or `openclaw setup` will create the -workspace and seed the bootstrap files if they are missing. -Sandbox seed copies only accept regular in-workspace files; symlink/hardlink -aliases that resolve outside the source workspace are ignored. +`openclaw onboard`, `openclaw configure`, or `openclaw setup` will create the workspace and seed the bootstrap files if they are missing. -If you already manage the workspace files yourself, you can disable bootstrap -file creation: + +Sandbox seed copies only accept regular in-workspace files; symlink/hardlink aliases that resolve outside the source workspace are ignored. + + +If you already manage the workspace files yourself, you can disable bootstrap file creation: ```json5 { agents: { defaults: { skipBootstrap: true } } } @@ -50,80 +47,60 @@ file creation: ## Extra workspace folders -Older installs may have created `~/openclaw`. Keeping multiple workspace -directories around can cause confusing auth or state drift, because only one -workspace is active at a time. +Older installs may have created `~/openclaw`. Keeping multiple workspace directories around can cause confusing auth or state drift, because only one workspace is active at a time. -**Recommendation:** keep a single active workspace. If you no longer use the -extra folders, archive or move them to Trash (for example `trash ~/openclaw`). -If you intentionally keep multiple workspaces, make sure -`agents.defaults.workspace` points to the active one. + +**Recommendation:** keep a single active workspace. If you no longer use the extra folders, archive or move them to Trash (for example `trash ~/openclaw`). If you intentionally keep multiple workspaces, make sure `agents.defaults.workspace` points to the active one. `openclaw doctor` warns when it detects extra workspace directories. + -## Workspace file map (what each file means) +## Workspace file map These are the standard files OpenClaw expects inside the workspace: -- `AGENTS.md` - - Operating instructions for the agent and how it should use memory. - - Loaded at the start of every session. - - Good place for rules, priorities, and "how to behave" details. + + + Operating instructions for the agent and how it should use memory. Loaded at the start of every session. Good place for rules, priorities, and "how to behave" details. + + + Persona, tone, and boundaries. Loaded every session. Guide: [SOUL.md personality guide](/concepts/soul). + + + Who the user is and how to address them. Loaded every session. + + + The agent's name, vibe, and emoji. Created/updated during the bootstrap ritual. + + + Notes about your local tools and conventions. Does not control tool availability; it is only guidance. + + + Optional tiny checklist for heartbeat runs. Keep it short to avoid token burn. + + + Optional startup checklist run automatically on gateway restart (when [internal hooks](/automation/hooks) are enabled). Keep it short; use the message tool for outbound sends. + + + One-time first-run ritual. Only created for a brand-new workspace. Delete it after the ritual is complete. + + + Daily memory log (one file per day). Recommended to read today + yesterday on session start. + + + Curated long-term memory. Only load in the main, private session (not shared/group contexts). See [Memory](/concepts/memory) for the workflow and automatic memory flush. + + + Workspace-specific skills. Highest-precedence skill location for that workspace. Overrides project agent skills, personal agent skills, managed skills, bundled skills, and `skills.load.extraDirs` when names collide. + + + Canvas UI files for node displays (for example `canvas/index.html`). + + -- `SOUL.md` - - Persona, tone, and boundaries. - - Loaded every session. - - Guide: [SOUL.md Personality Guide](/concepts/soul) - -- `USER.md` - - Who the user is and how to address them. - - Loaded every session. - -- `IDENTITY.md` - - The agent's name, vibe, and emoji. - - Created/updated during the bootstrap ritual. - -- `TOOLS.md` - - Notes about your local tools and conventions. - - Does not control tool availability; it is only guidance. - -- `HEARTBEAT.md` - - Optional tiny checklist for heartbeat runs. - - Keep it short to avoid token burn. - -- `BOOT.md` - - Optional startup checklist run automatically on gateway restart (when [internal hooks](/automation/hooks) are enabled). - - Keep it short; use the message tool for outbound sends. - -- `BOOTSTRAP.md` - - One-time first-run ritual. - - Only created for a brand-new workspace. - - Delete it after the ritual is complete. - -- `memory/YYYY-MM-DD.md` - - Daily memory log (one file per day). - - Recommended to read today + yesterday on session start. - -- `MEMORY.md` (optional) - - Curated long-term memory. - - Only load in the main, private session (not shared/group contexts). - -See [Memory](/concepts/memory) for the workflow and automatic memory flush. - -- `skills/` (optional) - - Workspace-specific skills. - - Highest-precedence skill location for that workspace. - - Overrides project agent skills, personal agent skills, managed skills, bundled skills, and `skills.load.extraDirs` when names collide. - -- `canvas/` (optional) - - Canvas UI files for node displays (for example `canvas/index.html`). - -If any bootstrap file is missing, OpenClaw injects a "missing file" marker into -the session and continues. Large bootstrap files are truncated when injected; -adjust limits with `agents.defaults.bootstrapMaxChars` (default: 12000) and -`agents.defaults.bootstrapTotalMaxChars` (default: 60000). -`openclaw setup` can recreate missing defaults without overwriting existing -files. + +If any bootstrap file is missing, OpenClaw injects a "missing file" marker into the session and continues. Large bootstrap files are truncated when injected; adjust limits with `agents.defaults.bootstrapMaxChars` (default: 12000) and `agents.defaults.bootstrapTotalMaxChars` (default: 60000). `openclaw setup` can recreate missing defaults without overwriting existing files. + ## What is NOT in the workspace @@ -135,83 +112,82 @@ These live under `~/.openclaw/` and should NOT be committed to the workspace rep - `~/.openclaw/agents//sessions/` (session transcripts + metadata) - `~/.openclaw/skills/` (managed skills) -If you need to migrate sessions or config, copy them separately and keep them -out of version control. +If you need to migrate sessions or config, copy them separately and keep them out of version control. ## Git backup (recommended, private) -Treat the workspace as private memory. Put it in a **private** git repo so it is -backed up and recoverable. +Treat the workspace as private memory. Put it in a **private** git repo so it is backed up and recoverable. -Run these steps on the machine where the Gateway runs (that is where the -workspace lives). +Run these steps on the machine where the Gateway runs (that is where the workspace lives). -### 1) Initialize the repo + + + If git is installed, brand-new workspaces are initialized automatically. If this workspace is not already a repo, run: -If git is installed, brand-new workspaces are initialized automatically. If this -workspace is not already a repo, run: + ```bash + cd ~/.openclaw/workspace + git init + git add AGENTS.md SOUL.md TOOLS.md IDENTITY.md USER.md HEARTBEAT.md memory/ + git commit -m "Add agent workspace" + ``` -```bash -cd ~/.openclaw/workspace -git init -git add AGENTS.md SOUL.md TOOLS.md IDENTITY.md USER.md HEARTBEAT.md memory/ -git commit -m "Add agent workspace" -``` + + + + + 1. Create a new **private** repository on GitHub. + 2. Do not initialize with a README (avoids merge conflicts). + 3. Copy the HTTPS remote URL. + 4. Add the remote and push: -### 2) Add a private remote (beginner-friendly options) + ```bash + git branch -M main + git remote add origin + git push -u origin main + ``` + + + ```bash + gh auth login + gh repo create openclaw-workspace --private --source . --remote origin --push + ``` + + + 1. Create a new **private** repository on GitLab. + 2. Do not initialize with a README (avoids merge conflicts). + 3. Copy the HTTPS remote URL. + 4. Add the remote and push: -Option A: GitHub web UI + ```bash + git branch -M main + git remote add origin + git push -u origin main + ``` + + -1. Create a new **private** repository on GitHub. -2. Do not initialize with a README (avoids merge conflicts). -3. Copy the HTTPS remote URL. -4. Add the remote and push: - -```bash -git branch -M main -git remote add origin -git push -u origin main -``` - -Option B: GitHub CLI (`gh`) - -```bash -gh auth login -gh repo create openclaw-workspace --private --source . --remote origin --push -``` - -Option C: GitLab web UI - -1. Create a new **private** repository on GitLab. -2. Do not initialize with a README (avoids merge conflicts). -3. Copy the HTTPS remote URL. -4. Add the remote and push: - -```bash -git branch -M main -git remote add origin -git push -u origin main -``` - -### 3) Ongoing updates - -```bash -git status -git add . -git commit -m "Update memory" -git push -``` + + + ```bash + git status + git add . + git commit -m "Update memory" + git push + ``` + + ## Do not commit secrets + Even in a private repo, avoid storing secrets in the workspace: - API keys, OAuth tokens, passwords, or private credentials. - Anything under `~/.openclaw/`. - Raw dumps of chats or sensitive attachments. -If you must store sensitive references, use placeholders and keep the real -secret elsewhere (password manager, environment variables, or `~/.openclaw/`). +If you must store sensitive references, use placeholders and keep the real secret elsewhere (password manager, environment variables, or `~/.openclaw/`). + Suggested `.gitignore` starter: @@ -225,22 +201,29 @@ Suggested `.gitignore` starter: ## Moving the workspace to a new machine -1. Clone the repo to the desired path (default `~/.openclaw/workspace`). -2. Set `agents.defaults.workspace` to that path in `~/.openclaw/openclaw.json`. -3. Run `openclaw setup --workspace ` to seed any missing files. -4. If you need sessions, copy `~/.openclaw/agents//sessions/` from the - old machine separately. + + + Clone the repo to the desired path (default `~/.openclaw/workspace`). + + + Set `agents.defaults.workspace` to that path in `~/.openclaw/openclaw.json`. + + + Run `openclaw setup --workspace ` to seed any missing files. + + + If you need sessions, copy `~/.openclaw/agents//sessions/` from the old machine separately. + + ## Advanced notes -- Multi-agent routing can use different workspaces per agent. See - [Channel routing](/channels/channel-routing) for routing configuration. -- If `agents.defaults.sandbox` is enabled, non-main sessions can use per-session sandbox - workspaces under `agents.defaults.sandbox.workspaceRoot`. +- Multi-agent routing can use different workspaces per agent. See [Channel routing](/channels/channel-routing) for routing configuration. +- If `agents.defaults.sandbox` is enabled, non-main sessions can use per-session sandbox workspaces under `agents.defaults.sandbox.workspaceRoot`. ## Related -- [Standing Orders](/automation/standing-orders) — persistent instructions in workspace files - [Heartbeat](/gateway/heartbeat) — HEARTBEAT.md workspace file -- [Session](/concepts/session) — session storage paths - [Sandboxing](/gateway/sandboxing) — workspace access in sandboxed environments +- [Session](/concepts/session) — session storage paths +- [Standing orders](/automation/standing-orders) — persistent instructions in workspace files From a97ee5c1d322a31862a2c34d2e536b6269dd0812 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 26 Apr 2026 12:03:48 +0100 Subject: [PATCH 2/8] fix(google-meet): recover local chrome tabs --- docs/plugins/google-meet.md | 10 +- extensions/google-meet/index.test.ts | 72 +++++++ extensions/google-meet/index.ts | 15 +- extensions/google-meet/src/cli.test.ts | 1 + extensions/google-meet/src/cli.ts | 21 ++- extensions/google-meet/src/runtime.ts | 20 +- .../google-meet/src/transports/chrome.ts | 178 ++++++++++++++---- 7 files changed, 268 insertions(+), 49 deletions(-) diff --git a/docs/plugins/google-meet.md b/docs/plugins/google-meet.md index 00707f980bc..a2b4d1d5997 100644 --- a/docs/plugins/google-meet.md +++ b/docs/plugins/google-meet.md @@ -1238,10 +1238,12 @@ openclaw googlemeet recover-tab https://meet.google.com/abc-defg-hij ``` The equivalent tool action is `recover_current_tab`. It focuses and inspects an -existing Meet tab on the configured Chrome node. It does not open a new tab or -create a new session; it reports the current blocker, such as login, admission, -permissions, or audio-choice state. The CLI command talks to the configured -Gateway, so the Gateway must be running and the Chrome node must be connected. +existing Meet tab for the selected transport. With `chrome`, it uses local +browser control through the Gateway; with `chrome-node`, it uses the configured +Chrome node. It does not open a new tab or create a new session; it reports the +current blocker, such as login, admission, permissions, or audio-choice state. +The CLI command talks to the configured Gateway, so the Gateway must be running; +`chrome-node` also requires the Chrome node to be connected. ### Twilio setup checks fail diff --git a/extensions/google-meet/index.test.ts b/extensions/google-meet/index.test.ts index 678eefd5cca..c83211300fc 100644 --- a/extensions/google-meet/index.test.ts +++ b/extensions/google-meet/index.test.ts @@ -25,6 +25,7 @@ import { startNodeRealtimeAudioBridge } from "./src/realtime-node.js"; import { startCommandRealtimeAudioBridge } from "./src/realtime.js"; import { normalizeMeetUrl } from "./src/runtime.js"; import { noopLogger, setupGoogleMeetPlugin } from "./src/test-support/plugin-harness.js"; +import { __testing as chromeTransportTesting } from "./src/transports/chrome.js"; import { buildMeetDtmfSequence, normalizeDialInNumber } from "./src/transports/twilio.js"; const voiceCallMocks = vi.hoisted(() => ({ @@ -224,6 +225,7 @@ describe("google-meet plugin", () => { afterEach(() => { vi.unstubAllGlobals(); + chromeTransportTesting.setDepsForTest(null); }); it("defaults to chrome realtime with safe read-only tools", () => { @@ -1607,6 +1609,76 @@ describe("google-meet plugin", () => { ); }); + it("recovers and inspects an existing local Chrome Meet tab", async () => { + const callGatewayFromCli = vi.fn( + async ( + _method: string, + _opts: unknown, + params?: unknown, + _extra?: unknown, + ): Promise> => { + const request = params as { path?: string; body?: { targetId?: string } }; + if (request.path === "/tabs") { + return { + tabs: [ + { + targetId: "local-meet-tab", + title: "Meet", + url: "https://meet.google.com/abc-defg-hij?authuser=me@example.com", + }, + ], + }; + } + if (request.path === "/tabs/focus") { + return { ok: true }; + } + if (request.path === "/act") { + return { + result: JSON.stringify({ + inCall: false, + manualActionRequired: true, + manualActionReason: "meet-admission-required", + manualActionMessage: "Admit the OpenClaw browser participant in Google Meet.", + title: "Meet", + url: "https://meet.google.com/abc-defg-hij?authuser=me@example.com", + }), + }; + } + throw new Error(`unexpected browser request path ${request.path}`); + }, + ); + chromeTransportTesting.setDepsForTest({ callGatewayFromCli }); + const { tools, nodesInvoke } = setup({ defaultTransport: "chrome" }); + const tool = tools[0] as { + execute: ( + id: string, + params: unknown, + ) => Promise<{ details: { transport?: string; found?: boolean; browser?: unknown } }>; + }; + + const result = await tool.execute("id", { + action: "recover_current_tab", + url: "https://meet.google.com/abc-defg-hij", + }); + + expect(result.details).toMatchObject({ + transport: "chrome", + found: true, + targetId: "local-meet-tab", + browser: { + manualActionRequired: true, + manualActionReason: "meet-admission-required", + }, + }); + expect(callGatewayFromCli).toHaveBeenCalledWith( + "browser.request", + expect.any(Object), + expect.objectContaining({ method: "POST", path: "/tabs/focus" }), + { progress: false }, + ); + expect(nodesInvoke).not.toHaveBeenCalled(); + }); + it("exposes a test-speech action that joins the requested meeting", async () => { const { tools, nodesInvoke } = setup( { diff --git a/extensions/google-meet/index.ts b/extensions/google-meet/index.ts index 077736e1b08..aeeacda25c6 100644 --- a/extensions/google-meet/index.ts +++ b/extensions/google-meet/index.ts @@ -557,7 +557,13 @@ export default definePluginEntry({ async ({ params, respond }: GatewayRequestHandlerOptions) => { try { const rt = await ensureRuntime(); - respond(true, await rt.recoverCurrentTab({ url: normalizeOptionalString(params?.url) })); + respond( + true, + await rt.recoverCurrentTab({ + url: normalizeOptionalString(params?.url), + transport: normalizeTransport(params?.transport), + }), + ); } catch (err) { sendError(respond, err); } @@ -793,7 +799,12 @@ export default definePluginEntry({ } case "recover_current_tab": { const rt = await ensureRuntime(); - return json(await rt.recoverCurrentTab({ url: normalizeOptionalString(raw.url) })); + return json( + await rt.recoverCurrentTab({ + url: normalizeOptionalString(raw.url), + transport: normalizeTransport(raw.transport), + }), + ); } case "setup_status": { const rt = await ensureRuntime(); diff --git a/extensions/google-meet/src/cli.test.ts b/extensions/google-meet/src/cli.test.ts index 673bf1bed40..446a60ffe66 100644 --- a/extensions/google-meet/src/cli.test.ts +++ b/extensions/google-meet/src/cli.test.ts @@ -793,6 +793,7 @@ describe("google-meet CLI", () => { config: { defaultTransport: "chrome-node" }, runtime: { recoverCurrentTab: async () => ({ + transport: "chrome-node", nodeId: "node-1", found: true, targetId: "tab-1", diff --git a/extensions/google-meet/src/cli.ts b/extensions/google-meet/src/cli.ts index db68bf9e6a7..4bde834a7a3 100644 --- a/extensions/google-meet/src/cli.ts +++ b/extensions/google-meet/src/cli.ts @@ -148,6 +148,10 @@ type JsonOptions = { json?: boolean; }; +type RecoverTabOptions = JsonOptions & { + transport?: GoogleMeetTransport; +}; + type CreateOptions = { accessToken?: string; refreshToken?: string; @@ -431,7 +435,8 @@ function writeRecoverCurrentTabResult( result: Awaited>, ): void { writeStdoutLine("Google Meet current tab: %s", result.found ? "found" : "not found"); - writeStdoutLine("node: %s", result.nodeId); + writeStdoutLine("transport: %s", result.transport); + writeStdoutLine("node: %s", result.nodeId ?? "local/none"); if (result.targetId) { writeStdoutLine("target: %s", result.targetId); } @@ -445,12 +450,15 @@ function writeRecoverCurrentTabResult( session: { id: "current-tab", url: result.browser.browserUrl ?? result.tab?.url ?? "unknown", - transport: "chrome-node", + transport: result.transport, mode: "transcribe", state: "active", createdAt: "", updatedAt: "", - participantIdentity: "signed-in Google Chrome profile on a paired node", + participantIdentity: + result.transport === "chrome-node" + ? "signed-in Google Chrome profile on a paired node" + : "signed-in Google Chrome profile", realtime: { enabled: false, toolPolicy: "safe-read-only" }, chrome: { audioBackend: "blackhole-2ch", @@ -1960,12 +1968,13 @@ export function registerGoogleMeetCli(params: { root .command("recover-tab") - .description("Focus and inspect an existing Google Meet tab on the Chrome node") + .description("Focus and inspect an existing Google Meet tab") .argument("[url]", "Optional Meet URL to match") + .option("--transport ", "Transport to inspect: chrome or chrome-node") .option("--json", "Print JSON output", false) - .action(async (url: string | undefined, options: JsonOptions) => { + .action(async (url: string | undefined, options: RecoverTabOptions) => { const rt = await params.ensureRuntime(); - const result = await rt.recoverCurrentTab({ url }); + const result = await rt.recoverCurrentTab({ url, transport: options.transport }); if (options.json) { writeStdoutJson(result); return; diff --git a/extensions/google-meet/src/runtime.ts b/extensions/google-meet/src/runtime.ts index 503f715be2b..ce9491ff259 100644 --- a/extensions/google-meet/src/runtime.ts +++ b/extensions/google-meet/src/runtime.ts @@ -11,6 +11,7 @@ import { assertBlackHole2chAvailable, launchChromeMeet, launchChromeMeetOnNode, + recoverCurrentMeetTab, recoverCurrentMeetTabOnNode, } from "./transports/chrome.js"; import { buildMeetDtmfSequence, normalizeDialInNumber } from "./transports/twilio.js"; @@ -181,11 +182,22 @@ export class GoogleMeetRuntime { }); } - async recoverCurrentTab(request: { url?: string } = {}) { - return recoverCurrentMeetTabOnNode({ - runtime: this.params.runtime, + async recoverCurrentTab(request: { url?: string; transport?: GoogleMeetTransport } = {}) { + const transport = resolveTransport(request.transport, this.params.config); + if (transport === "twilio") { + throw new Error("recover_current_tab only supports chrome or chrome-node transports"); + } + const url = request.url ? normalizeMeetUrl(request.url) : undefined; + if (transport === "chrome-node") { + return recoverCurrentMeetTabOnNode({ + runtime: this.params.runtime, + config: this.params.config, + url, + }); + } + return recoverCurrentMeetTab({ config: this.params.config, - url: request.url ? normalizeMeetUrl(request.url) : undefined, + url, }); } diff --git a/extensions/google-meet/src/transports/chrome.ts b/extensions/google-meet/src/transports/chrome.ts index 63b7bf71cb4..abf7c434f62 100644 --- a/extensions/google-meet/src/transports/chrome.ts +++ b/extensions/google-meet/src/transports/chrome.ts @@ -1,3 +1,4 @@ +import { callGatewayFromCli } from "openclaw/plugin-sdk/browser-node-runtime"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime"; import type { PluginRuntime } from "openclaw/plugin-sdk/plugin-runtime"; import type { RuntimeLogger } from "openclaw/plugin-sdk/plugin-runtime"; @@ -23,6 +24,27 @@ import type { GoogleMeetChromeHealth } from "./types.js"; export const GOOGLE_MEET_SYSTEM_PROFILER_COMMAND = "/usr/sbin/system_profiler"; +type BrowserRequestParams = { + method: "GET" | "POST" | "DELETE"; + path: string; + body?: unknown; + timeoutMs: number; +}; + +type BrowserRequestCaller = (params: BrowserRequestParams) => Promise; + +const chromeTransportDeps: { + callGatewayFromCli: typeof callGatewayFromCli; +} = { + callGatewayFromCli, +}; + +export const __testing = { + setDepsForTest(deps: { callGatewayFromCli?: typeof callGatewayFromCli } | null) { + chromeTransportDeps.callGatewayFromCli = deps?.callGatewayFromCli ?? callGatewayFromCli; + }, +}; + export function outputMentionsBlackHole2ch(output: string): boolean { return /\bBlackHole\s+2ch\b/i.test(output); } @@ -215,6 +237,23 @@ function parseMeetBrowserStatus(result: unknown): GoogleMeetChromeHealth | undef }; } +async function callLocalBrowserRequest(params: BrowserRequestParams) { + return await chromeTransportDeps.callGatewayFromCli( + "browser.request", + { + json: true, + timeout: String(params.timeoutMs + 5_000), + }, + { + method: params.method, + path: params.path, + body: params.body, + timeoutMs: params.timeoutMs, + }, + { progress: false }, + ); +} + function meetStatusScript(params: { guestName: string; autoJoin: boolean }) { return `() => { const text = (node) => (node?.innerText || node?.textContent || "").trim(); @@ -412,11 +451,96 @@ function isRecoverableMeetTab(tab: BrowserTab, url?: string): boolean { ); } +async function inspectRecoverableMeetTab(params: { + callBrowser: BrowserRequestCaller; + config: GoogleMeetConfig; + timeoutMs: number; + tab: BrowserTab; + targetId: string; +}) { + await params.callBrowser({ + method: "POST", + path: "/tabs/focus", + body: { targetId: params.targetId }, + timeoutMs: Math.min(params.timeoutMs, 5_000), + }); + const evaluated = await params.callBrowser({ + method: "POST", + path: "/act", + body: { + kind: "evaluate", + targetId: params.targetId, + fn: meetStatusScript({ + guestName: params.config.chrome.guestName, + autoJoin: false, + }), + }, + timeoutMs: Math.min(params.timeoutMs, 10_000), + }); + const browser = parseMeetBrowserStatus(evaluated); + const manual = browser?.manualActionRequired + ? browser.manualActionMessage || browser.manualActionReason + : undefined; + return { + found: true, + targetId: params.targetId, + tab: params.tab, + browser, + message: + manual ?? (browser?.inCall ? "Existing Meet tab is in-call." : "Existing Meet tab focused."), + }; +} + +export async function recoverCurrentMeetTab(params: { + config: GoogleMeetConfig; + url?: string; +}): Promise<{ + transport: "chrome"; + nodeId?: undefined; + found: boolean; + targetId?: string; + tab?: BrowserTab; + browser?: GoogleMeetChromeHealth; + message: string; +}> { + const timeoutMs = Math.max(1_000, params.config.chrome.joinTimeoutMs); + const tabs = asBrowserTabs( + await callLocalBrowserRequest({ + method: "GET", + path: "/tabs", + timeoutMs: Math.min(timeoutMs, 5_000), + }), + ); + const tab = tabs.find((entry) => isRecoverableMeetTab(entry, params.url)); + const targetId = tab?.targetId; + if (!tab || !targetId) { + return { + transport: "chrome", + found: false, + tab, + message: params.url + ? `No existing Meet tab matched ${params.url}.` + : "No existing Meet tab found in local Chrome.", + }; + } + return { + transport: "chrome", + ...(await inspectRecoverableMeetTab({ + callBrowser: callLocalBrowserRequest, + config: params.config, + timeoutMs, + tab, + targetId, + })), + }; +} + export async function recoverCurrentMeetTabOnNode(params: { runtime: PluginRuntime; config: GoogleMeetConfig; url?: string; }): Promise<{ + transport: "chrome-node"; nodeId: string; found: boolean; targetId?: string; @@ -442,6 +566,7 @@ export async function recoverCurrentMeetTabOnNode(params: { const targetId = tab?.targetId; if (!tab || !targetId) { return { + transport: "chrome-node", nodeId, found: false, tab, @@ -450,44 +575,31 @@ export async function recoverCurrentMeetTabOnNode(params: { : "No existing Meet tab found on the selected Chrome node.", }; } - await callBrowserProxyOnNode({ - runtime: params.runtime, - nodeId, - method: "POST", - path: "/tabs/focus", - body: { targetId }, - timeoutMs: Math.min(timeoutMs, 5_000), - }); - const evaluated = await callBrowserProxyOnNode({ - runtime: params.runtime, - nodeId, - method: "POST", - path: "/act", - body: { - kind: "evaluate", - targetId, - fn: meetStatusScript({ - guestName: params.config.chrome.guestName, - autoJoin: false, - }), - }, - timeoutMs: Math.min(timeoutMs, 10_000), - }); - const browser = parseMeetBrowserStatus(evaluated); - const manual = browser?.manualActionRequired - ? browser.manualActionMessage || browser.manualActionReason - : undefined; return { + transport: "chrome-node", nodeId, - found: true, - targetId, - tab, - browser, - message: - manual ?? (browser?.inCall ? "Existing Meet tab is in-call." : "Existing Meet tab focused."), + ...(await inspectRecoverableMeetTab({ + callBrowser: async (request) => + await callBrowserProxyOnNode({ + runtime: params.runtime, + nodeId, + method: request.method, + path: request.path, + body: request.body, + timeoutMs: request.timeoutMs, + }), + config: params.config, + timeoutMs, + tab, + targetId, + })), }; } +export type GoogleMeetCurrentTabRecoveryResult = Awaited< + ReturnType +>; + export async function launchChromeMeetOnNode(params: { runtime: PluginRuntime; config: GoogleMeetConfig; From 2a5d3ad5b9931280ca3b70b5f926d502dbba6eb0 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sun, 26 Apr 2026 04:04:02 -0700 Subject: [PATCH 3/8] docs(dreaming): rewrite with AccordionGroup for phases and backfill, Tabs for quick start and CLI workflow, ParamField for dreaming defaults --- docs/concepts/dreaming.md | 223 +++++++++++++++++++------------------- 1 file changed, 111 insertions(+), 112 deletions(-) diff --git a/docs/concepts/dreaming.md b/docs/concepts/dreaming.md index 6f107b5c947..53fd5d5f516 100644 --- a/docs/concepts/dreaming.md +++ b/docs/concepts/dreaming.md @@ -1,17 +1,18 @@ --- summary: "Background memory consolidation with light, deep, and REM phases plus a Dream Diary" title: "Dreaming" +sidebarTitle: "Dreaming" read_when: - You want memory promotion to run automatically - You want to understand what each dreaming phase does - You want to tune consolidation without polluting MEMORY.md --- -Dreaming is the background memory consolidation system in `memory-core`. -It helps OpenClaw move strong short-term signals into durable memory while -keeping the process explainable and reviewable. +Dreaming is the background memory consolidation system in `memory-core`. It helps OpenClaw move strong short-term signals into durable memory while keeping the process explainable and reviewable. + Dreaming is **opt-in** and disabled by default. + ## What dreaming writes @@ -32,69 +33,63 @@ Dreaming uses three cooperative phases: | Deep | Score and promote durable candidates | Yes (`MEMORY.md`) | | REM | Reflect on themes and recurring ideas | No | -These phases are internal implementation details, not separate user-configured -"modes." +These phases are internal implementation details, not separate user-configured "modes." -### Light phase + + + Light phase ingests recent daily memory signals and recall traces, dedupes them, and stages candidate lines. -Light phase ingests recent daily memory signals and recall traces, dedupes them, -and stages candidate lines. + - Reads from short-term recall state, recent daily memory files, and redacted session transcripts when available. + - Writes a managed `## Light Sleep` block when storage includes inline output. + - Records reinforcement signals for later deep ranking. + - Never writes to `MEMORY.md`. -- Reads from short-term recall state, recent daily memory files, and redacted session transcripts when available. -- Writes a managed `## Light Sleep` block when storage includes inline output. -- Records reinforcement signals for later deep ranking. -- Never writes to `MEMORY.md`. + + + Deep phase decides what becomes long-term memory. -### Deep phase + - Ranks candidates using weighted scoring and threshold gates. + - Requires `minScore`, `minRecallCount`, and `minUniqueQueries` to pass. + - Rehydrates snippets from live daily files before writing, so stale/deleted snippets are skipped. + - Appends promoted entries to `MEMORY.md`. + - Writes a `## Deep Sleep` summary into `DREAMS.md` and optionally writes `memory/dreaming/deep/YYYY-MM-DD.md`. -Deep phase decides what becomes long-term memory. + + + REM phase extracts patterns and reflective signals. -- Ranks candidates using weighted scoring and threshold gates. -- Requires `minScore`, `minRecallCount`, and `minUniqueQueries` to pass. -- Rehydrates snippets from live daily files before writing, so stale/deleted snippets are skipped. -- Appends promoted entries to `MEMORY.md`. -- Writes a `## Deep Sleep` summary into `DREAMS.md` and optionally writes `memory/dreaming/deep/YYYY-MM-DD.md`. + - Builds theme and reflection summaries from recent short-term traces. + - Writes a managed `## REM Sleep` block when storage includes inline output. + - Records REM reinforcement signals used by deep ranking. + - Never writes to `MEMORY.md`. -### REM phase - -REM phase extracts patterns and reflective signals. - -- Builds theme and reflection summaries from recent short-term traces. -- Writes a managed `## REM Sleep` block when storage includes inline output. -- Records REM reinforcement signals used by deep ranking. -- Never writes to `MEMORY.md`. + + ## Session transcript ingestion -Dreaming can ingest redacted session transcripts into the dreaming corpus. When -transcripts are available, they are fed into the light phase alongside daily -memory signals and recall traces. Personal and sensitive content is redacted -before ingestion. +Dreaming can ingest redacted session transcripts into the dreaming corpus. When transcripts are available, they are fed into the light phase alongside daily memory signals and recall traces. Personal and sensitive content is redacted before ingestion. ## Dream Diary -Dreaming also keeps a narrative **Dream Diary** in `DREAMS.md`. -After each phase has enough material, `memory-core` runs a best-effort background -subagent turn (using the default runtime model) and appends a short diary entry. +Dreaming also keeps a narrative **Dream Diary** in `DREAMS.md`. After each phase has enough material, `memory-core` runs a best-effort background subagent turn (using the default runtime model) and appends a short diary entry. -This diary is for human reading in the Dreams UI, not a promotion source. -Dreaming-generated diary/report artifacts are excluded from short-term -promotion. Only grounded memory snippets are eligible to promote into -`MEMORY.md`. + +This diary is for human reading in the Dreams UI, not a promotion source. Dreaming-generated diary/report artifacts are excluded from short-term promotion. Only grounded memory snippets are eligible to promote into `MEMORY.md`. + There is also a grounded historical backfill lane for review and recovery work: -- `memory rem-harness --path ... --grounded` previews grounded diary output from historical `YYYY-MM-DD.md` notes. -- `memory rem-backfill --path ...` writes reversible grounded diary entries into `DREAMS.md`. -- `memory rem-backfill --path ... --stage-short-term` stages grounded durable candidates into the same short-term evidence store the normal deep phase already uses. -- `memory rem-backfill --rollback` and `--rollback-short-term` remove those staged backfill artifacts without touching ordinary diary entries or live short-term recall. + + + - `memory rem-harness --path ... --grounded` previews grounded diary output from historical `YYYY-MM-DD.md` notes. + - `memory rem-backfill --path ...` writes reversible grounded diary entries into `DREAMS.md`. + - `memory rem-backfill --path ... --stage-short-term` stages grounded durable candidates into the same short-term evidence store the normal deep phase already uses. + - `memory rem-backfill --rollback` and `--rollback-short-term` remove those staged backfill artifacts without touching ordinary diary entries or live short-term recall. + + -The Control UI exposes the same diary backfill/reset flow so you can inspect -results in the Dreams scene before deciding whether the grounded candidates -deserve promotion. The Scene also shows a distinct grounded lane so you can see -which staged short-term entries came from historical replay, which promoted -items were grounded-led, and clear only grounded-only staged entries without -touching ordinary live short-term state. +The Control UI exposes the same diary backfill/reset flow so you can inspect results in the Dreams scene before deciding whether the grounded candidates deserve promotion. The Scene also shows a distinct grounded lane so you can see which staged short-term entries came from historical replay, which promoted items were grounded-led, and clear only grounded-only staged entries without touching ordinary live short-term state. ## Deep ranking signals @@ -109,13 +104,11 @@ Deep ranking uses six weighted base signals plus phase reinforcement: | Consolidation | 0.10 | Multi-day recurrence strength | | Conceptual richness | 0.06 | Concept-tag density from snippet/path | -Light and REM phase hits add a small recency-decayed boost from -`memory/.dreams/phase-signals.json`. +Light and REM phase hits add a small recency-decayed boost from `memory/.dreams/phase-signals.json`. ## Scheduling -When enabled, `memory-core` auto-manages one cron job for a full dreaming -sweep. Each sweep runs phases in order: light -> REM -> deep. +When enabled, `memory-core` auto-manages one cron job for a full dreaming sweep. Each sweep runs phases in order: light → REM → deep. Default cadence behavior: @@ -125,43 +118,44 @@ Default cadence behavior: ## Quick start -Enable dreaming: - -```json -{ - "plugins": { - "entries": { - "memory-core": { - "config": { - "dreaming": { - "enabled": true + + + ```json + { + "plugins": { + "entries": { + "memory-core": { + "config": { + "dreaming": { + "enabled": true + } + } } } } } - } -} -``` - -Enable dreaming with a custom sweep cadence: - -```json -{ - "plugins": { - "entries": { - "memory-core": { - "config": { - "dreaming": { - "enabled": true, - "timezone": "America/Los_Angeles", - "frequency": "0 */6 * * *" + ``` + + + ```json + { + "plugins": { + "entries": { + "memory-core": { + "config": { + "dreaming": { + "enabled": true, + "timezone": "America/Los_Angeles", + "frequency": "0 */6 * * *" + } + } } } } } - } -} -``` + ``` + + ## Slash command @@ -174,47 +168,52 @@ Enable dreaming with a custom sweep cadence: ## CLI workflow -Use CLI promotion for preview or manual apply: + + + ```bash + openclaw memory promote + openclaw memory promote --apply + openclaw memory promote --limit 5 + openclaw memory status --deep + ``` -```bash -openclaw memory promote -openclaw memory promote --apply -openclaw memory promote --limit 5 -openclaw memory status --deep -``` + Manual `memory promote` uses deep-phase thresholds by default unless overridden with CLI flags. -Manual `memory promote` uses deep-phase thresholds by default unless overridden -with CLI flags. + + + Explain why a specific candidate would or would not promote: -Explain why a specific candidate would or would not promote: + ```bash + openclaw memory promote-explain "router vlan" + openclaw memory promote-explain "router vlan" --json + ``` -```bash -openclaw memory promote-explain "router vlan" -openclaw memory promote-explain "router vlan" --json -``` + + + Preview REM reflections, candidate truths, and deep promotion output without writing anything: -Preview REM reflections, candidate truths, and deep promotion output without -writing anything: + ```bash + openclaw memory rem-harness + openclaw memory rem-harness --json + ``` -```bash -openclaw memory rem-harness -openclaw memory rem-harness --json -``` + + ## Key defaults All settings live under `plugins.entries.memory-core.config.dreaming`. -| Key | Default | -| ----------- | ----------- | -| `enabled` | `false` | -| `frequency` | `0 3 * * *` | + + Enable or disable the dreaming sweep. + + + Cron cadence for the full dreaming sweep. + -Phase policy, thresholds, and storage behavior are internal implementation -details (not user-facing config). - -See [Memory configuration reference](/reference/memory-config#dreaming) -for the full key list. + +Phase policy, thresholds, and storage behavior are internal implementation details (not user-facing config). See [Memory configuration reference](/reference/memory-config#dreaming) for the full key list. + ## Dreams UI @@ -230,6 +229,6 @@ When enabled, the Gateway **Dreams** tab shows: ## Related - [Memory](/concepts/memory) -- [Memory Search](/concepts/memory-search) -- [memory CLI](/cli/memory) +- [Memory CLI](/cli/memory) - [Memory configuration reference](/reference/memory-config) +- [Memory search](/concepts/memory-search) From 3a54bbb6174a9f68dfa04d32b94269ded0947841 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sun, 26 Apr 2026 04:03:57 -0700 Subject: [PATCH 4/8] fix(plugins): persist synthetic auth refs in index --- src/plugins/installed-plugin-index-store.test.ts | 1 + src/plugins/installed-plugin-index-store.ts | 1 + src/plugins/installed-plugin-index.test.ts | 2 ++ src/plugins/installed-plugin-index.ts | 4 ++++ 4 files changed, 8 insertions(+) diff --git a/src/plugins/installed-plugin-index-store.test.ts b/src/plugins/installed-plugin-index-store.test.ts index 61af416c615..fb556707a74 100644 --- a/src/plugins/installed-plugin-index-store.test.ts +++ b/src/plugins/installed-plugin-index-store.test.ts @@ -39,6 +39,7 @@ function createIndex(overrides: Partial = {}): InstalledPl rootDir: "/plugins/demo", origin: "global", enabled: true, + syntheticAuthRefs: ["demo"], startup: { sidecar: false, memory: false, diff --git a/src/plugins/installed-plugin-index-store.ts b/src/plugins/installed-plugin-index-store.ts index 0827ca174c9..45edc0201d3 100644 --- a/src/plugins/installed-plugin-index-store.ts +++ b/src/plugins/installed-plugin-index-store.ts @@ -69,6 +69,7 @@ const InstalledPluginIndexRecordSchema = z.object({ origin: z.string(), enabled: z.boolean(), enabledByDefault: z.boolean().optional(), + syntheticAuthRefs: StringArraySchema.optional(), startup: InstalledPluginIndexStartupSchema, compat: z.array(z.string()), }); diff --git a/src/plugins/installed-plugin-index.test.ts b/src/plugins/installed-plugin-index.test.ts index 119bc8bea3c..9d2dff291e2 100644 --- a/src/plugins/installed-plugin-index.test.ts +++ b/src/plugins/installed-plugin-index.test.ts @@ -123,6 +123,7 @@ function createRichPluginFixture(params: { packageVersion?: string } = {}) { providerAuthEnvVars: { demo: ["DEMO_API_KEY"], }, + syntheticAuthRefs: ["demo", "demo-cli"], channelEnvVars: { "demo-chat": ["DEMO_CHAT_TOKEN"], }, @@ -182,6 +183,7 @@ describe("installed plugin index", () => { rootDir: fixture.rootDir, source: path.join(fixture.rootDir, "index.ts"), enabled: true, + syntheticAuthRefs: ["demo", "demo-cli"], packageInstall: { defaultChoice: "npm", npm: { diff --git a/src/plugins/installed-plugin-index.ts b/src/plugins/installed-plugin-index.ts index 4c280e8a9cb..e8f551fda15 100644 --- a/src/plugins/installed-plugin-index.ts +++ b/src/plugins/installed-plugin-index.ts @@ -103,6 +103,7 @@ export type InstalledPluginIndexRecord = { origin: PluginManifestRecord["origin"]; enabled: boolean; enabledByDefault?: boolean; + syntheticAuthRefs?: readonly string[]; startup: InstalledPluginStartupInfo; compat: readonly PluginCompatCode[]; }; @@ -591,6 +592,9 @@ function buildInstalledPluginIndex( startup: buildStartupInfo(record), compat: collectCompatCodes(record), }; + if (record.syntheticAuthRefs && record.syntheticAuthRefs.length > 0) { + indexRecord.syntheticAuthRefs = record.syntheticAuthRefs; + } if (record.format && record.format !== "openclaw") { indexRecord.format = record.format; } From 6dfb03ab2ef05a1351ade9e1f3e30503d693d485 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sun, 26 Apr 2026 03:25:10 -0700 Subject: [PATCH 5/8] fix(plugins): record crabpot compat deprecations --- docs/plugins/compatibility.md | 4 ++-- src/plugin-sdk/index.ts | 2 ++ src/plugins/compat/registry.ts | 14 ++++++++++++++ src/plugins/contracts/plugin-sdk-index.test.ts | 8 +++++++- 4 files changed, 25 insertions(+), 3 deletions(-) diff --git a/docs/plugins/compatibility.md b/docs/plugins/compatibility.md index 804216d7c1a..127e5ca0bb0 100644 --- a/docs/plugins/compatibility.md +++ b/docs/plugins/compatibility.md @@ -86,8 +86,8 @@ Current compatibility records include: `register(api)` - legacy SDK aliases such as `openclaw/extension-api`, `openclaw/plugin-sdk/channel-runtime`, `openclaw/plugin-sdk/command-auth` - status builders, `openclaw/plugin-sdk/test-utils`, and the `ClawdbotConfig` - type alias + status builders, `openclaw/plugin-sdk/test-utils`, and the `ClawdbotConfig` / + `OpenClawSchemaType` type aliases - bundled plugin allowlist and enablement behavior - legacy provider/channel env-var manifest metadata - legacy provider plugin hooks and type aliases while providers move to diff --git a/src/plugin-sdk/index.ts b/src/plugin-sdk/index.ts index 67bb19a9498..4077cf6fcd8 100644 --- a/src/plugin-sdk/index.ts +++ b/src/plugin-sdk/index.ts @@ -80,6 +80,8 @@ export type { export type { OpenClawConfig } from "../config/config.js"; /** @deprecated Use OpenClawConfig instead */ export type { OpenClawConfig as ClawdbotConfig } from "../config/config.js"; +/** @deprecated Use OpenClawConfig instead */ +export type { OpenClawConfig as OpenClawSchemaType } from "../config/config.js"; export type { MemoryPluginCapability, MemoryPluginPublicArtifact, diff --git a/src/plugins/compat/registry.ts b/src/plugins/compat/registry.ts index 0848f5ccfbe..ae7a905473a 100644 --- a/src/plugins/compat/registry.ts +++ b/src/plugins/compat/registry.ts @@ -367,6 +367,20 @@ export const PLUGIN_COMPAT_RECORDS = [ diagnostics: ["plugin SDK compatibility warning"], tests: ["src/plugins/contracts/plugin-sdk-index.test.ts"], }, + { + code: "openclaw-schema-type-alias", + status: "deprecated", + owner: "sdk", + introduced: "2026-04-26", + deprecated: "2026-04-26", + warningStarts: "2026-04-26", + removeAfter: "2026-07-26", + replacement: "`OpenClawConfig` from `openclaw/plugin-sdk/config-schema`", + docsPath: "/plugins/sdk-migration", + surfaces: ["openclaw/plugin-sdk `OpenClawSchemaType` type export"], + diagnostics: ["plugin SDK compatibility warning"], + tests: ["src/plugins/contracts/plugin-sdk-index.test.ts"], + }, { code: "legacy-extension-api-import", status: "deprecated", diff --git a/src/plugins/contracts/plugin-sdk-index.test.ts b/src/plugins/contracts/plugin-sdk-index.test.ts index 81f46fb7cf3..ba6c82e250e 100644 --- a/src/plugins/contracts/plugin-sdk-index.test.ts +++ b/src/plugins/contracts/plugin-sdk-index.test.ts @@ -1,8 +1,9 @@ import fs from "node:fs/promises"; import path from "node:path"; import { fileURLToPath } from "node:url"; -import { describe, expect, it } from "vitest"; +import { describe, expect, expectTypeOf, it } from "vitest"; import { buildPluginSdkPackageExports } from "../../plugin-sdk/entrypoints.js"; +import type { ClawdbotConfig, OpenClawConfig, OpenClawSchemaType } from "../../plugin-sdk/index.js"; const pluginSdkIndexPath = fileURLToPath(new URL("../../plugin-sdk/index.ts", import.meta.url)); @@ -104,6 +105,11 @@ describe("plugin-sdk exports", () => { ]); }); + it("keeps deprecated root config type aliases aligned", () => { + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + }); + it("keeps package.json plugin-sdk exports synced with the manifest", async () => { const packageJsonPath = path.join(process.cwd(), "package.json"); const packageJson = JSON.parse(await fs.readFile(packageJsonPath, "utf8")) as { From 4c40cf878374466b7dd2769f8b3c4101ec8b1910 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sun, 26 Apr 2026 03:34:03 -0700 Subject: [PATCH 6/8] chore(plugins): inventory doctor deprecation compat --- .../openclaw-release-maintainer/SKILL.md | 35 ++- CHANGELOG.md | 1 + docs/plugins/compatibility.md | 16 + .../doctor/shared/deprecation-compat.test.ts | 78 +++++ .../doctor/shared/deprecation-compat.ts | 274 ++++++++++++++++++ src/plugins/compat/registry.test.ts | 25 ++ src/plugins/compat/registry.ts | 76 +++++ 7 files changed, 493 insertions(+), 12 deletions(-) create mode 100644 src/commands/doctor/shared/deprecation-compat.test.ts create mode 100644 src/commands/doctor/shared/deprecation-compat.ts diff --git a/.agents/skills/openclaw-release-maintainer/SKILL.md b/.agents/skills/openclaw-release-maintainer/SKILL.md index dd004c28a35..3aa8fbb179b 100644 --- a/.agents/skills/openclaw-release-maintainer/SKILL.md +++ b/.agents/skills/openclaw-release-maintainer/SKILL.md @@ -25,12 +25,22 @@ Use this skill for release and publish-time workflow. Keep ordinary development - Before release branching, commit any dirty files in coherent groups, push, pull/rebase, then run `/changelog` on `main` and commit/push/pull that changelog rewrite immediately before creating the release branch. -- During release planning, inspect `src/plugins/compat/registry.ts` before - branching and again before final publish. For every deprecated or - removal-pending compatibility record whose `removeAfter` date is on or before - the release date, either remove the compatibility path where safe and - validate the affected tests, or write down why removal is blocked and get - explicit maintainer approval before shipping the expired compatibility path. +- During release planning, inspect both `src/plugins/compat/registry.ts` and + `src/commands/doctor/shared/deprecation-compat.ts` before branching and again + before final publish. For every deprecated or removal-pending compatibility + record whose `removeAfter` date is on or before the release date, either + remove the compatibility path where safe and validate the affected tests, or + write down why removal is blocked and get explicit maintainer approval before + shipping the expired compatibility path. +- When removing deprecated runtime/config compatibility, preserve any doctor + migration, repair, or hint that is still needed by supported upgrade paths. + Doctor-side compatibility should stay tracked in + `src/commands/doctor/shared/deprecation-compat.ts` until maintainers confirm + the repair is no longer needed. +- Revalidate compatibility replacement text during release planning. The + recommended replacement can shift as plugin ownership, externalization, and + config footprint move, so do not blindly copy stale replacement annotations + into release notes. - Do not delete or rewrite beta tags after they leave the machine. If a published or pushed beta needs a fix, commit the fix on the release branch and increment to the next `-beta.N`. @@ -123,12 +133,13 @@ Use this skill for release and publish-time workflow. Keep ordinary development `CHANGELOG.md` version section, not highlights or an excerpt. When creating or editing a release, extract from `## YYYY.M.D` through the line before the next level-2 heading and use that complete block as the release notes. -- When preparing release notes, scan `src/plugins/compat/registry.ts` for - plugin compatibility records with `warningStarts` or `removeAfter` within 7 - days after the release date. Add an `Upcoming deprecations` note to the - release notes when any exist, including the compatibility code, target date, - replacement, and a link to the record's `docsPath` or `/plugins/compatibility` - when no more specific deprecation page exists. +- When preparing release notes, scan `src/plugins/compat/registry.ts` and + `src/commands/doctor/shared/deprecation-compat.ts` for compatibility records + with `warningStarts` or `removeAfter` within 7 days after the release date. + Add an `Upcoming deprecations` note to the release notes when any exist, + including the compatibility code, target date, replacement, and a link to the + record's `docsPath` or `/plugins/compatibility` when no more specific + deprecation page exists. - When cutting a mac release with a beta GitHub prerelease: - tag `vYYYY.M.D-beta.N` from the release commit - create a prerelease titled `openclaw YYYY.M.D-beta.N` diff --git a/CHANGELOG.md b/CHANGELOG.md index 18b5e8c8406..f4fea0bacab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ Docs: https://docs.openclaw.ai - Plugins/startup: load the default `memory-core` slot during Gateway startup when permitted so active-memory recall can call `memory_search` and `memory_get` without requiring an explicit `plugins.slots.memory` entry, while preserving `plugins.slots.memory: "none"`. Thanks @codex. - Plugins/CLI: prefer native require for compiled bundled plugin JavaScript before jiti so read-only config, status, device, and node commands avoid unnecessary transform overhead on slow hosts. Fixes #62842. Thanks @Effet. +- Plugins/compat: inventory doctor-side deprecation migrations separately from runtime plugin compatibility so release sweeps preserve needed repairs while enforcing dated removal windows. Thanks @vincentkoc. - Plugins/compat: add missing dated compatibility records for legacy extension-api, memory registration, provider hook/type aliases, runtime aliases, channel SDK helpers, and approval/test utility shims. Thanks @vincentkoc. - Plugins/CLI: refresh the persisted registry after managed plugin files are removed so ClawHub uninstall cannot leave stale `plugins list` entries. Thanks @codex. - Plugins/CLI: make plugin install and uninstall config writes conflict-aware, clear stale denylist entries on explicit reinstall/removal, and delete managed plugin files only after config/index commit succeeds. Thanks @codex. diff --git a/docs/plugins/compatibility.md b/docs/plugins/compatibility.md index 127e5ca0bb0..579c4572cbc 100644 --- a/docs/plugins/compatibility.md +++ b/docs/plugins/compatibility.md @@ -31,6 +31,18 @@ The registry is the source for maintainer planning and future plugin inspector checks. If a plugin-facing behavior changes, add or update the compatibility record in the same change that adds the adapter. +Doctor repair and migration compatibility is tracked separately at +`src/commands/doctor/shared/deprecation-compat.ts`. Those records cover old +config shapes, install-ledger layouts, and repair shims that may need to stay +available after the runtime compatibility path is removed. + +Release sweeps should check both registries. Do not delete a doctor migration +just because the matching runtime or config compatibility record expired; first +verify there is no supported upgrade path that still needs the repair. Also +revalidate each replacement annotation during release planning because plugin +ownership and config footprint can change as providers and channels move out of +core. + ## Plugin inspector package The plugin inspector should live outside the core OpenClaw repo as a separate @@ -112,6 +124,10 @@ Current compatibility records include: - persisted plugin registry disable and install-migration env flags while repair flows migrate operators to `openclaw plugins registry --refresh` and `openclaw doctor --fix` +- legacy plugin-owned web search, web fetch, and x_search config paths while + doctor migrates them to `plugins.entries..config` +- legacy `plugins.installs` authored config and bundled plugin load-path + aliases while install metadata moves into the state-managed plugin ledger New plugin code should prefer the replacement listed in the registry and in the specific migration guide. Existing plugins can keep using a compatibility path diff --git a/src/commands/doctor/shared/deprecation-compat.test.ts b/src/commands/doctor/shared/deprecation-compat.test.ts new file mode 100644 index 00000000000..2f09ecbba55 --- /dev/null +++ b/src/commands/doctor/shared/deprecation-compat.test.ts @@ -0,0 +1,78 @@ +import fs from "node:fs"; +import { describe, expect, it } from "vitest"; +import { + getDoctorDeprecationCompatRecord, + isDoctorDeprecationCompatCode, + listDeprecatedDoctorDeprecationCompatRecords, + listDoctorDeprecationCompatRecords, +} from "./deprecation-compat.js"; + +const datePattern = /^\d{4}-\d{2}-\d{2}$/u; + +const requiredDoctorCompatCodes = [ + "doctor-agent-runtime-embedded-harness", + "doctor-plugin-install-config-ledger", + "doctor-bundled-plugin-load-paths", + "doctor-web-search-plugin-config", + "doctor-web-fetch-plugin-config", + "doctor-x-search-plugin-config", +] as const; + +function parseDate(date: string): Date { + return new Date(`${date}T00:00:00Z`); +} + +function addUtcMonths(date: Date, months: number): Date { + const next = new Date(date); + next.setUTCMonth(next.getUTCMonth() + months); + return next; +} + +describe("doctor deprecation compatibility inventory", () => { + it("keeps compatibility codes unique and lookup-safe", () => { + const records = listDoctorDeprecationCompatRecords(); + const codes = records.map((record) => record.code); + + expect(new Set(codes).size).toBe(codes.length); + expect(isDoctorDeprecationCompatCode("doctor-web-search-plugin-config")).toBe(true); + expect(isDoctorDeprecationCompatCode("missing-code")).toBe(false); + expect(getDoctorDeprecationCompatRecord("doctor-web-search-plugin-config").owner).toBe( + "provider", + ); + }); + + it("tracks the known doctor migrations that protect plugin/config rollout", () => { + for (const code of requiredDoctorCompatCodes) { + expect(isDoctorDeprecationCompatCode(code), code).toBe(true); + } + }); + + it("requires dated deprecation metadata with a three-month maximum window", () => { + for (const record of listDeprecatedDoctorDeprecationCompatRecords()) { + expect(record.deprecated, record.code).toMatch(datePattern); + expect(record.warningStarts, record.code).toMatch(datePattern); + expect(record.removeAfter, record.code).toMatch(datePattern); + if (!record.warningStarts || !record.removeAfter) { + throw new Error(`${record.code} is missing deprecation window dates`); + } + const maxRemoveAfter = addUtcMonths(parseDate(record.warningStarts), 3); + const removeAfter = parseDate(record.removeAfter); + expect(removeAfter <= maxRemoveAfter, record.code).toBe(true); + } + }); + + it("keeps every record actionable", () => { + for (const record of listDoctorDeprecationCompatRecords()) { + expect(record.introduced, record.code).toMatch(datePattern); + expect(record.source, record.code).toBeTruthy(); + expect(record.migration, record.code).toBeTruthy(); + expect(record.replacement, record.code).toBeTruthy(); + expect(record.docsPath, record.code).toMatch(/^\//u); + expect(fs.existsSync(record.migration), `${record.code}: ${record.migration}`).toBe(true); + expect(record.tests.length, record.code).toBeGreaterThan(0); + for (const testPath of record.tests) { + expect(fs.existsSync(testPath), `${record.code}: ${testPath}`).toBe(true); + } + } + }); +}); diff --git a/src/commands/doctor/shared/deprecation-compat.ts b/src/commands/doctor/shared/deprecation-compat.ts new file mode 100644 index 00000000000..8f473f6d817 --- /dev/null +++ b/src/commands/doctor/shared/deprecation-compat.ts @@ -0,0 +1,274 @@ +export type DoctorDeprecationCompatStatus = "active" | "deprecated" | "removal-pending" | "removed"; + +export type DoctorDeprecationCompatOwner = + | "agent-runtime" + | "audio" + | "browser" + | "channel" + | "config" + | "gateway" + | "plugin" + | "provider" + | "tools" + | "tts"; + +export type DoctorDeprecationCompatRecord = { + code: Code; + status: DoctorDeprecationCompatStatus; + owner: DoctorDeprecationCompatOwner; + introduced: string; + deprecated?: string; + warningStarts?: string; + removeAfter?: string; + source: string; + migration: string; + replacement: string; + docsPath: string; + tests: readonly string[]; + notes?: string; +}; + +const TODAY = "2026-04-26"; +const MAX_REMOVE_AFTER = "2026-07-26"; + +function deprecatedCompatRecord( + record: Omit< + DoctorDeprecationCompatRecord, + "deprecated" | "warningStarts" | "removeAfter" | "status" + > & + Partial< + Pick< + DoctorDeprecationCompatRecord, + "deprecated" | "removeAfter" | "status" | "warningStarts" + > + >, +): DoctorDeprecationCompatRecord { + return { + status: "deprecated", + deprecated: TODAY, + warningStarts: TODAY, + removeAfter: MAX_REMOVE_AFTER, + ...record, + }; +} + +// Doctor migrations and repair shims can outlive the runtime/config compatibility +// path they repair. Release removals must check this inventory before deleting +// doctor fixes, and replacement notes should be revalidated against the current +// architecture because ownership and config footprint can shift during rollout. +export const DOCTOR_DEPRECATION_COMPAT_RECORDS = [ + deprecatedCompatRecord({ + code: "doctor-agent-runtime-embedded-harness", + owner: "agent-runtime", + introduced: "2026-04-25", + source: "agents.defaults.embeddedHarness; agents.list[].embeddedHarness", + migration: "src/commands/doctor/shared/legacy-config-migrations.runtime.agents.ts", + replacement: "agents.defaults.agentRuntime and agents.list[].agentRuntime", + docsPath: "/plugins/sdk-agent-harness", + tests: ["src/commands/doctor/shared/legacy-config-migrate.test.ts"], + notes: + "Runtime-policy naming changed during the plugin architecture work; verify replacement wording against current agentRuntime docs before removal.", + }), + deprecatedCompatRecord({ + code: "doctor-agent-sandbox-persession", + owner: "agent-runtime", + introduced: "2026-04-26", + source: "agents.defaults.sandbox.perSession; agents.list[].sandbox.perSession", + migration: "src/commands/doctor/shared/legacy-config-migrations.runtime.agents.ts", + replacement: "agents.*.sandbox.scope", + docsPath: "/cli/doctor", + tests: ["src/commands/doctor/shared/legacy-config-migrate.test.ts"], + }), + deprecatedCompatRecord({ + code: "doctor-top-level-memory-search", + owner: "config", + introduced: "2026-04-26", + source: "memorySearch", + migration: "src/commands/doctor/shared/legacy-config-migrations.runtime.agents.ts", + replacement: "agents.defaults.memorySearch", + docsPath: "/cli/doctor", + tests: ["src/commands/doctor/shared/legacy-config-migrate.test.ts"], + }), + deprecatedCompatRecord({ + code: "doctor-top-level-heartbeat", + owner: "config", + introduced: "2026-04-26", + source: "heartbeat", + migration: "src/commands/doctor/shared/legacy-config-migrations.runtime.agents.ts", + replacement: "agents.defaults.heartbeat and channels.defaults.heartbeat", + docsPath: "/automation", + tests: ["src/commands/doctor/shared/legacy-config-migrate.test.ts"], + }), + deprecatedCompatRecord({ + code: "doctor-gateway-bind-host-aliases", + owner: "gateway", + introduced: "2026-04-26", + source: "gateway.bind host aliases such as 0.0.0.0 and localhost", + migration: "src/commands/doctor/shared/legacy-config-migrations.runtime.gateway.ts", + replacement: "gateway.bind.mode values such as lan, loopback, custom, tailnet, and auto", + docsPath: "/gateway/configuration", + tests: ["src/commands/doctor/shared/legacy-config-migrate.test.ts"], + }), + deprecatedCompatRecord({ + code: "doctor-audio-transcription-command", + owner: "audio", + introduced: "2026-04-26", + source: "audio.transcription", + migration: "src/commands/doctor/shared/legacy-config-migrations.audio.ts", + replacement: "tools.media.audio.models", + docsPath: "/tools/media-overview", + tests: ["src/commands/doctor/shared/legacy-config-migrate.test.ts"], + }), + deprecatedCompatRecord({ + code: "doctor-channel-thread-binding-ttl", + owner: "channel", + introduced: "2026-04-26", + source: "threadBindings.ttlHours", + migration: "src/commands/doctor/shared/legacy-config-migrations.channels.ts", + replacement: "threadBindings.idleHours", + docsPath: "/channels/channel-routing", + tests: ["src/commands/doctor/shared/legacy-config-migrate.test.ts"], + }), + deprecatedCompatRecord({ + code: "doctor-channel-dm-aliases", + owner: "channel", + introduced: "2026-04-26", + source: "channels..dm.policy and channels..dm.allowFrom", + migration: "src/config/channel-compat-normalization.ts", + replacement: "channels..dmPolicy and channels..allowFrom", + docsPath: "/channels/channel-routing", + tests: ["src/commands/doctor/shared/channel-legacy-config-migrate.test.ts"], + }), + deprecatedCompatRecord({ + code: "doctor-channel-streaming-aliases", + owner: "channel", + introduced: "2026-04-26", + source: "streamMode, scalar streaming, chunkMode, blockStreaming, draftChunk, nativeStreaming", + migration: "src/config/channel-compat-normalization.ts", + replacement: "channels..streaming.*", + docsPath: "/channels/channel-routing", + tests: ["src/commands/doctor/shared/channel-legacy-config-migrate.test.ts"], + }), + deprecatedCompatRecord({ + code: "doctor-tts-provider-aliases", + owner: "tts", + introduced: "2026-04-26", + source: "messages.tts.openai/elevenlabs/edge and plugins.entries.voice-call.config.tts aliases", + migration: "src/commands/doctor/shared/legacy-config-migrations.runtime.tts.ts", + replacement: "messages.tts.providers. and microsoft instead of edge", + docsPath: "/tools/tts", + tests: ["src/commands/doctor/shared/legacy-config-migrate.test.ts"], + }), + deprecatedCompatRecord({ + code: "doctor-plugin-install-config-ledger", + owner: "plugin", + introduced: "2026-04-25", + source: "plugins.installs in authored config", + migration: "src/config/plugin-install-config-migration.ts", + replacement: "state-managed plugins/installs.json install ledger", + docsPath: "/cli/plugins#registry", + tests: [ + "src/config/io.write-config.test.ts", + "src/commands/doctor/shared/plugin-registry-migration.test.ts", + ], + }), + deprecatedCompatRecord({ + code: "doctor-bundled-plugin-load-paths", + owner: "plugin", + introduced: "2026-04-25", + source: "plugins.load.paths entries that point at bundled plugin source/dist locations", + migration: "src/commands/doctor/shared/bundled-plugin-load-paths.ts", + replacement: "packaged bundled plugins and the persisted plugin registry", + docsPath: "/cli/plugins#registry", + tests: ["src/commands/doctor/shared/bundled-plugin-load-paths.test.ts"], + }), + deprecatedCompatRecord({ + code: "doctor-web-search-plugin-config", + owner: "provider", + introduced: "2026-04-26", + source: "tools.web.search.apiKey and tools.web.search.", + migration: "src/commands/doctor/shared/legacy-web-search-migrate.ts", + replacement: "plugins.entries..config.webSearch", + docsPath: "/tools/web", + tests: ["src/commands/doctor/shared/legacy-web-search-migrate.test.ts"], + notes: + "Provider/plugin ownership can move as bundled providers externalize; verify the current manifest owner before deleting migration support.", + }), + deprecatedCompatRecord({ + code: "doctor-web-fetch-plugin-config", + owner: "provider", + introduced: "2026-04-26", + source: "tools.web.fetch.firecrawl", + migration: "src/commands/doctor/shared/legacy-web-fetch-migrate.ts", + replacement: "plugins.entries.firecrawl.config.webFetch", + docsPath: "/tools/web-fetch", + tests: ["src/commands/doctor/shared/legacy-web-fetch-migrate.test.ts"], + }), + deprecatedCompatRecord({ + code: "doctor-x-search-plugin-config", + owner: "provider", + introduced: "2026-04-26", + source: "tools.web.x_search.apiKey", + migration: "src/commands/doctor/shared/legacy-x-search-migrate.ts", + replacement: "plugins.entries.xai.config.webSearch.apiKey", + docsPath: "/tools/grok-search", + tests: [ + "src/commands/doctor/shared/legacy-x-search-migrate.test.ts", + "src/commands/doctor/shared/legacy-config-migrate.test.ts", + ], + }), + deprecatedCompatRecord({ + code: "doctor-talk-provider-shape", + owner: "tts", + introduced: "2026-04-26", + source: "legacy talk provider scalar fields and provider/provider ids", + migration: "src/commands/doctor/shared/legacy-talk-config-normalizer.ts", + replacement: "talk.providers.", + docsPath: "/tools/tts", + tests: ["src/commands/doctor/shared/legacy-config-migrate.test.ts"], + }), + deprecatedCompatRecord({ + code: "doctor-legacy-tools-by-sender", + owner: "tools", + introduced: "2026-04-26", + source: "untyped toolsBySender keys", + migration: "src/commands/doctor/shared/legacy-tools-by-sender.ts", + replacement: "typed id:, e164:, username:, or name: sender keys", + docsPath: "/tools/exec-approvals", + tests: ["src/commands/doctor/shared/legacy-tools-by-sender.test.ts"], + }), +] as const satisfies readonly DoctorDeprecationCompatRecord[]; + +export type DoctorDeprecationCompatCode = + (typeof DOCTOR_DEPRECATION_COMPAT_RECORDS)[number]["code"]; +export type KnownDoctorDeprecationCompatRecord = + DoctorDeprecationCompatRecord; + +const doctorDeprecationCompatRecordByCode = new Map< + DoctorDeprecationCompatCode, + KnownDoctorDeprecationCompatRecord +>(DOCTOR_DEPRECATION_COMPAT_RECORDS.map((record) => [record.code, record])); + +export function listDoctorDeprecationCompatRecords(): readonly KnownDoctorDeprecationCompatRecord[] { + return DOCTOR_DEPRECATION_COMPAT_RECORDS; +} + +export function listDeprecatedDoctorDeprecationCompatRecords(): readonly KnownDoctorDeprecationCompatRecord[] { + return DOCTOR_DEPRECATION_COMPAT_RECORDS.filter((record) => + (["deprecated", "removal-pending"] as readonly string[]).includes(record.status), + ); +} + +export function isDoctorDeprecationCompatCode(code: string): code is DoctorDeprecationCompatCode { + return doctorDeprecationCompatRecordByCode.has(code as DoctorDeprecationCompatCode); +} + +export function getDoctorDeprecationCompatRecord( + code: DoctorDeprecationCompatCode, +): KnownDoctorDeprecationCompatRecord { + const record = doctorDeprecationCompatRecordByCode.get(code); + if (!record) { + throw new Error(`Unknown doctor deprecation compatibility code: ${code}`); + } + return record; +} diff --git a/src/plugins/compat/registry.test.ts b/src/plugins/compat/registry.test.ts index 2aa2aa95254..7897546e0f4 100644 --- a/src/plugins/compat/registry.test.ts +++ b/src/plugins/compat/registry.test.ts @@ -90,6 +90,31 @@ const knownDeprecatedSurfaceMarkers = [ file: "src/plugin-sdk/test-utils.ts", marker: "Deprecated compatibility alias", }, + { + code: "plugin-install-config-ledger", + file: "src/config/plugin-install-config-migration.ts", + marker: "stripShippedPluginInstallConfigRecords", + }, + { + code: "bundled-plugin-load-path-aliases", + file: "src/commands/doctor/shared/bundled-plugin-load-paths.ts", + marker: "plugins.load.paths", + }, + { + code: "plugin-owned-web-search-config", + file: "src/commands/doctor/shared/legacy-web-search-migrate.ts", + marker: "tools.web.search", + }, + { + code: "plugin-owned-web-fetch-config", + file: "src/commands/doctor/shared/legacy-web-fetch-migrate.ts", + marker: "tools.web.fetch.firecrawl", + }, + { + code: "plugin-owned-x-search-config", + file: "src/commands/doctor/shared/legacy-x-search-migrate.ts", + marker: "tools.web.x_search", + }, ] as const; function parseDate(date: string): Date { diff --git a/src/plugins/compat/registry.ts b/src/plugins/compat/registry.ts index ae7a905473a..49aee47b7fa 100644 --- a/src/plugins/compat/registry.ts +++ b/src/plugins/compat/registry.ts @@ -267,6 +267,82 @@ export const PLUGIN_COMPAT_RECORDS = [ diagnostics: ["postinstall migration skip", "postinstall migration force deprecation warning"], tests: ["src/commands/doctor/shared/plugin-registry-migration.test.ts"], }, + { + code: "plugin-install-config-ledger", + status: "deprecated", + owner: "config", + introduced: "2026-04-25", + deprecated: "2026-04-26", + warningStarts: "2026-04-26", + removeAfter: "2026-07-26", + replacement: "state-managed `plugins/installs.json` install ledger", + docsPath: "/cli/plugins#registry", + surfaces: ["plugins.installs authored config", "plugin install/update migration"], + diagnostics: ["config write migration warning", "doctor registry migration"], + tests: [ + "src/config/io.write-config.test.ts", + "src/commands/doctor/shared/plugin-registry-migration.test.ts", + ], + }, + { + code: "bundled-plugin-load-path-aliases", + status: "deprecated", + owner: "config", + introduced: "2026-04-25", + deprecated: "2026-04-26", + warningStarts: "2026-04-26", + removeAfter: "2026-07-26", + replacement: "packaged bundled plugins resolved through the persisted plugin registry", + docsPath: "/cli/plugins#registry", + surfaces: ["plugins.load.paths entries pointing at bundled plugin source/dist paths"], + diagnostics: ["doctor bundled plugin load-path warning"], + tests: ["src/commands/doctor/shared/bundled-plugin-load-paths.test.ts"], + }, + { + code: "plugin-owned-web-search-config", + status: "deprecated", + owner: "provider", + introduced: "2026-04-26", + deprecated: "2026-04-26", + warningStarts: "2026-04-26", + removeAfter: "2026-07-26", + replacement: "`plugins.entries..config.webSearch`", + docsPath: "/tools/web", + surfaces: ["tools.web.search.apiKey", "tools.web.search."], + diagnostics: ["doctor legacy web-search config migration"], + tests: ["src/commands/doctor/shared/legacy-web-search-migrate.test.ts"], + }, + { + code: "plugin-owned-web-fetch-config", + status: "deprecated", + owner: "provider", + introduced: "2026-04-26", + deprecated: "2026-04-26", + warningStarts: "2026-04-26", + removeAfter: "2026-07-26", + replacement: "`plugins.entries.firecrawl.config.webFetch`", + docsPath: "/tools/web-fetch", + surfaces: ["tools.web.fetch.firecrawl"], + diagnostics: ["doctor legacy web-fetch config migration"], + tests: ["src/commands/doctor/shared/legacy-web-fetch-migrate.test.ts"], + }, + { + code: "plugin-owned-x-search-config", + status: "deprecated", + owner: "provider", + introduced: "2026-04-26", + deprecated: "2026-04-26", + warningStarts: "2026-04-26", + removeAfter: "2026-07-26", + replacement: "`plugins.entries.xai.config.webSearch.apiKey`", + docsPath: "/tools/grok-search", + surfaces: ["tools.web.x_search.apiKey"], + diagnostics: ["doctor legacy x_search config migration"], + tests: [ + "src/commands/doctor/shared/legacy-x-search-migrate.test.ts", + LEGACY_CONFIG_MIGRATE_TEST_PATH, + ], + }, { code: "plugin-activate-entrypoint-alias", status: "deprecated", From efaa66f70d3871396315893d39eea8eb4d5b0871 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sun, 26 Apr 2026 03:45:23 -0700 Subject: [PATCH 7/8] fix(plugins): satisfy doctor compat lint --- src/commands/doctor/shared/deprecation-compat.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/commands/doctor/shared/deprecation-compat.ts b/src/commands/doctor/shared/deprecation-compat.ts index 8f473f6d817..a69dc88f547 100644 --- a/src/commands/doctor/shared/deprecation-compat.ts +++ b/src/commands/doctor/shared/deprecation-compat.ts @@ -241,8 +241,7 @@ export const DOCTOR_DEPRECATION_COMPAT_RECORDS = [ export type DoctorDeprecationCompatCode = (typeof DOCTOR_DEPRECATION_COMPAT_RECORDS)[number]["code"]; -export type KnownDoctorDeprecationCompatRecord = - DoctorDeprecationCompatRecord; +export type KnownDoctorDeprecationCompatRecord = DoctorDeprecationCompatRecord; const doctorDeprecationCompatRecordByCode = new Map< DoctorDeprecationCompatCode, @@ -260,7 +259,7 @@ export function listDeprecatedDoctorDeprecationCompatRecords(): readonly KnownDo } export function isDoctorDeprecationCompatCode(code: string): code is DoctorDeprecationCompatCode { - return doctorDeprecationCompatRecordByCode.has(code as DoctorDeprecationCompatCode); + return doctorDeprecationCompatRecordByCode.has(code); } export function getDoctorDeprecationCompatRecord( From ecf71da888af0f4be6fedecbda87ef16ee667b78 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 26 Apr 2026 12:05:31 +0100 Subject: [PATCH 8/8] fix(voice-call): avoid duplicate webhook logs --- extensions/voice-call/src/runtime.test.ts | 27 +++++++++++++++++++++++ extensions/voice-call/src/runtime.ts | 2 +- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/extensions/voice-call/src/runtime.test.ts b/extensions/voice-call/src/runtime.test.ts index b50864af5dd..c169b7b89c4 100644 --- a/extensions/voice-call/src/runtime.test.ts +++ b/extensions/voice-call/src/runtime.test.ts @@ -227,6 +227,33 @@ describe("createVoiceCallRuntime lifecycle", () => { await runtime.stop(); }); + it("does not log duplicate webhook and public URLs when they match", async () => { + const logger = { + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + }; + + const runtime = await createVoiceCallRuntime({ + config: createExternalProviderConfig({ + provider: "twilio", + publicUrl: "https://voice.example.com/voice/webhook", + }), + coreConfig: {} as CoreConfig, + agentRuntime: {} as never, + logger, + }); + + expect(logger.info).toHaveBeenCalledWith( + "[voice-call] Webhook URL: https://voice.example.com/voice/webhook", + ); + expect(logger.info).not.toHaveBeenCalledWith( + "[voice-call] Public URL: https://voice.example.com/voice/webhook", + ); + + await runtime.stop(); + }); + it("wires the shared realtime agent consult tool and handler", async () => { const config = createBaseConfig(); config.inboundPolicy = "allowlist"; diff --git a/extensions/voice-call/src/runtime.ts b/extensions/voice-call/src/runtime.ts index eb509a40d20..88cdf0f0151 100644 --- a/extensions/voice-call/src/runtime.ts +++ b/extensions/voice-call/src/runtime.ts @@ -464,7 +464,7 @@ export async function createVoiceCallRuntime(params: { log.info("[voice-call] Runtime initialized"); log.info(`[voice-call] Webhook URL: ${webhookUrl}`); - if (publicUrl) { + if (publicUrl && publicUrl !== webhookUrl) { log.info(`[voice-call] Public URL: ${publicUrl}`); }