Skip to content

Plugin agent tools changed by a plugin reload never reach running provider sessions (ACP/omp keeps the old schema) #5359

Description

@ComicBit

Workflow I could not complete

A path-installed plugin proxies a local MCP daemon (Aurora Operator) into BB threads with bb.agents.registerTool. omp under ACP loads MCP servers only from its ACP client, so BB's dynamic-tool bridge is the only way those threads reach the daemon. When the daemon gains an option (a new enum value in aurora_run), the plugin reloads and registers the new schema, but every omp thread that was already running keeps the old schema until its provider session is rebuilt. Agents in long-running threads report the tool as "outdated" and cannot pass the new option.

Restarting threads is not a workable answer for agents that run for hours or days, and omp already applies MCP tool-list changes mid-session: the same omp session mounted and unmounted tools from another MCP server when that server's list changed (xd:// device inventory changed notices at 10:52:50, 11:07:24 and 11:10:07 UTC in the session below).

Observed (bb 0.45.0, omp 18.8.7, macOS 26)

  • Thread thr_6xuag86y3q (acp-omp) started 2026-10-11 10:14 UTC. The plugin had last been loaded before the daemon gained aurora_run.gemma_phone values extraction-capacity, synthesis-capacity and embedding-public. The plugin was reloaded at ~11:00 UTC. The agent then reported: "the bridge's aurora_run options leave out extraction-capacity, synthesis-capacity and embedding-public".
  • New threads started after the reload see the new schema.

Steps to reproduce

  1. Create a path plugin whose server registers one tool and selects it for every provider:

    export default function plugin(bb) {
      bb.agents.registerTool({
        name: "live_probe",
        description: "Return its arguments.",
        parameters: { type: "object", properties: { a: { type: "string" } } },
        execute: async (input) => JSON.stringify(input),
      });
      bb.agents.configure(() => ({ tools: ["live_probe"], skills: [] }));
    }
  2. bb plugin install /path/to/plugin --yes

  3. Start an omp (ACP) thread and ask: "List the parameters of the live_probe tool."

  4. Add b: { type: "string" } to properties, then bb plugin reload <plugin-id>.

  5. In the same thread ask again.

Reproduced from source on main at 3912f4aaefb7dff55c7e0cb5003b26b5cf1457dc with pnpm dev (omp 18.8.7, thread thr_wz4xr6e763 on the dev server, one provider session 01a12b09-594e-7622-834f-4a732b824ea3 and one omp acp process throughout):

  • Step 3 answer a; the agent's read of the tool schema at 12:56:39Z returned type Args = { a?: string; };
  • Step 4: the server logged only {"msg":"plugin live-probe@0.1.0 loaded"}; the omp debug log (~/.omp/logs/omp.<date>.<pid>.log) received no notifications/tools/list_changed.
  • Step 5 answer still a; the read at 12:57:08Z returned type Args = { a?: string; };

Expected vs actual

  • Expected: after step 4 the running session's tool list contains b (the bridge MCP server emits notifications/tools/list_changed and serves the new schema).
  • Actual: the running session keeps { a } until the provider session is rebuilt.

Root cause (base commit 3912f4aaefb7dff55c7e0cb5003b26b5cf1457dc)

  • The server resolves the tool set only while building thread.start/turn.submit:
    const dynamicToolContributions = resolveDynamicTools(
    conditionalConfiguration.tools,
    );
    const dynamicTools = dynamicToolContributions.map(
    (contribution) => contribution.tool,
    );
    . A plugin reload only notifies the UI:
    function notifyPluginsChanged(): void {
    const changes: SystemChangeKind[] = ["plugins-changed"];
    const providerRegistrationRevision =
    deps.providerRegistry?.getRegistrationRevision();
    if (
    providerRegistrationRevision !== undefined &&
    providerRegistrationRevision !== lastNotifiedProviderRegistrationRevision
    ) {
    lastNotifiedProviderRegistrationRevision = providerRegistrationRevision;
    changes.push("provider-registrations-changed");
    }
    deps.hub.notifySystem(changes);
    }
  • On turn.submit to an already hosted thread the daemon returns before using resumeContext.dynamicTools:
    async function resumeThreadRuntimeIfMissing(
    args: ResumeThreadRuntimeIfMissingArgs,
    ): Promise<void> {
    const { command, entry, options } = args;
    const { resumeContext } = command;
    if (entry.runtime.hasThread(command.threadId)) {
    return;
    }
    if (!resumeContext.providerThreadId) {
    throw new CommandDispatchError(
    "unknown_thread_runtime",
    `No provider thread id available for thread ${command.threadId}`,
    );
    }
    const bridgeLaunch = await resolveRuntimeBridgeLaunch(
    command.resumeContext.bridgeLaunch ?? command.bridgeLaunch,
    options,
    );
    await entry.runtime.resumeThread({
    bridgeLaunch,
    skillRoots: entry.skillRoots,
    environmentId: command.environmentId,
    threadId: command.threadId,
    projectId: resumeContext.projectId,
    providerThreadId: resumeContext.providerThreadId,
    providerId: resumeContext.providerId,
    contributedEnv: resumeContext.contributedEnv,
    options: command.options,
    instructions: resumeContext.instructions,
    dynamicTools: resumeContext.dynamicTools,
    instructionMode: resumeContext.instructionMode,
    });
    }
    , and turn/start carries no tools:
    const turnInputFields = {
    threadId: z.string().min(1),
    providerThreadId: z.string().min(1),
    input: z.array(promptInputSchema),
    clientRequestId: clientTurnRequestIdSchema,
    options: bridgeExecutionOptionsSchema,
    };
    export const turnStartParamsSchema = z.object(turnInputFields).passthrough();
  • The ACP bridge freezes the list into the bb-bridge MCP child's env (BB_ACP_DYNAMIC_TOOLS) at session creation and advertises tools: {} without listChanged:
    async function buildSessionMcpServers(
    params: AcpSessionParams,
    ): Promise<AcpMcpServerConfig[]> {
    const dynamicTools = params.dynamicTools ?? [];
    if (dynamicTools.length === 0) {
    return [];
    }
    const bridge = await ensureDynamicToolBridge();
    const config = buildAcpMcpServerConfig({
    bridgeArgs: resolveBridgeProcessArgsForMcpServer(),
    command: process.execPath,
    dynamicTools,
    host: bridge.host,
    port: bridge.port,
    runtimeEnv: resolveBridgeProcessEnvForMcpServer(),
    threadId: params.threadId,
    token: bridge.token,
    });
    process.stderr.write(
    `acp bridge: built "${config.name}" session MCP config for thread "${params.threadId}" (${dynamicTools.length} tools)\n`,
    );
    return [config];
    }
    ,
    export function buildAcpMcpServerConfig(
    args: BuildAcpMcpServerConfigArgs,
    ): AcpMcpServerConfig {
    return {
    name: ACP_BRIDGE_MCP_SERVER_NAME,
    command: args.command,
    args: args.bridgeArgs,
    env: [
    ...args.runtimeEnv,
    { name: ENV_HOST, value: args.host },
    { name: ENV_PORT, value: String(args.port) },
    { name: ENV_TOKEN, value: args.token },
    { name: ENV_THREAD_ID, value: args.threadId },
    { name: ENV_TOOLS, value: JSON.stringify(args.dynamicTools) },
    ],
    };
    }
    ,
    case "initialize":
    writeResult(message.id, {
    protocolVersion:
    typeof objectParams(message.params).protocolVersion === "string"
    ? objectParams(message.params).protocolVersion
    : "2024-11-05",
    capabilities: { tools: {} },
    serverInfo: { name: ACP_BRIDGE_MCP_SERVER_NAME, version: "1.0.0" },
    });
    void callBridge(env, {
    kind: "initialized",
    toolCount: env.tools.length,
    }).catch((error) => {
    process.stderr.write(
    `bb-bridge MCP: failed to report initialize: ${
    error instanceof Error ? error.message : String(error)
    }\n`,
    );
    });
    return;
    case "tools/list":
    writeResult(message.id, {
    tools: env.tools.map((tool) => ({
    name: tool.name,
    description: tool.description,
    inputSchema: tool.inputSchema,
    })),
    });
    return;
  • The plugin docs describe this as intended:
    Resolution happens for `thread.start` and `turn.submit`. A selected tool set
    takes effect only when the provider session is next started/resumed; BB never
    hot-mutates a running provider session. Instructions follow the same rule: a
    live provider session keeps the instructions it was constructed with, and
    changed instructions apply when the session is next constructed.

Proposed behavior

When a plugin change (install, reload, enable/disable, settings change) alters a live thread's resolved dynamic tools, BB pushes the new set to providers that can apply it without rebuilding the session. Policy stays on the server; the daemon applies it to live sessions:

  • The server recomputes each live thread's tools through the same configure resolution used at start/submit and sends them to the daemon.
  • The daemon stores the set (so a rebuilt session uses it) and forwards it to bridges that declare a new optional handshake capability, threadToolsUpdate, through a new bridge request thread/tools/update.
  • The ACP bb-bridge MCP server advertises tools.listChanged and emits notifications/tools/list_changed exactly once when a name, description or schema changed; unchanged sets and presentation-only changes emit nothing. In-flight calls finish; calls to a removed tool fail with the existing Unknown tool error.
  • turn.submit to a live session also applies resumeContext.dynamicTools, covering pushes missed while a host was offline.
  • Providers that cannot apply tools live keep today's behavior (Codex app-server dynamicTools; Claude Code's in-process MCP server with fixed allowedTools). Tool instructions remain session-constructed.

No public Plugin SDK API changes; the plugin-authoring docs change from "never hot-mutates" to which providers update live.

Related

Duplicate search (gh search issues --repo get-bb/bb): "list_changed", "dynamic tools", "plugin tools", "tool schema", "bridge tools", "provider session restart", "tools/list_changed", "plugin tool reload running session", "hot-mutate provider session". None reports live sessions keeping a stale plugin tool schema.

Prototype

Fork branch feat/live-plugin-tool-updates: https://github.com/ComicBit/bb/tree/feat/live-plugin-tool-updates (commit 76d093dc62e6ff726e8a7f8430b3084258d43ab6, based on 3912f4aaefb7dff55c7e0cb5003b26b5cf1457dc)

  • HOST_DAEMON_PROTOCOL_VERSION 234 → 235 (new online commands provider.sessions.list and thread.tools.update; resumeContext.dynamicTools now applies to live sessions). Neither command wakes a host; an older daemon fails the RPC and the server logs it at debug level. The bridge protocol stays at version 2: the capability is optional and old bridges are never sent the request.
  • Tests: the real bb-bridge MCP child under an MCP SDK client (advertises listChanged; a changed schema emits list_changed once and tools/list returns it; same set or presentation-only change emits nothing; in-flight call completes; removed tool fails); ACP and ACP-next bridge thread/tools/update; agent-runtime no_session/unchanged/deferred; daemon dispatch; server test that installs a real path plugin, rewrites and reloads it, and checks the push. Typecheck, lint and tests pass for every touched package, including @bb/server (3439 tests).

Live check: pnpm dev from the branch, the same probe plugin and omp 18.8.7. Each thread kept one provider session and one omp acp process across the reloads.

  • thr_w7axgj2eyg: before the edit the agent answered a; after adding b and bb plugin reload live-probe it answered a,b in the same session (schema read at 12:54:11Z: type Args = { a?: string; b?: string; };).
  • Server log for that reload: {"status":"applied","threadId":"thr_w7axgj2eyg","toolNames":["update_environment_directory","live_probe"],"msg":"Updated the dynamic tools of a live provider session"}; omp debug log: {"message":"MCP notification received","path":"mcp:bb-bridge","method":"notifications/tools/list_changed"}.
  • A tool added by a reload also appeared live in a second thread (thr_c9iacs8jwz), which had started before the tool was selected.
  • Negative control: reloading without changes logged no update and no further list_changed.

Per CONTRIBUTING.md, I will not open a pull request until this is aligned. Once approved, I will open it from the branch above.

AGENT GENERATED

Activity

  1. added
    pluginsPlugin SDK, runtime, marketplace
    providersCross-provider bridges, models, login
    provider-acpBuilt-in plugin: provider-acp
    on Oct 11, 2026
  2. bb-slop-cop commented on Oct 11, 2026

    @bb-slop-cop
    Contributor

    🚨 SLOP COP 🚨 · new-issue-autopilot

    Applied and verified: Type: Feature · Priority: Medium · Effort: High. Labels: plugins, providers, provider-acp. All classifications were previously unset; no existing values were replaced.

    Trusted origin/main explicitly documents that selected tools take effect when a provider session is next constructed, not through live mutation (current contract). The daemon's existing-session path and ACP MCP catalog match that contract. Updating tools without reconstructing a session therefore adds behavior rather than fixes a regression against the current contract.

    Medium priority reflects the session-reconstruction workaround and limited scope; High effort reflects coordinated server, daemon, and provider-bridge changes. No bug report or pull request: this is a feature requiring a runtime-policy/protocol decision, outside the simple-fix gate.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    pluginsPlugin SDK, runtime, marketplaceprovider-acpBuilt-in plugin: provider-acpprovidersCross-provider bridges, models, login

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions