Skip to content

app: an open web page boots every evicted location again within a second #58795

Closed

Description

@lsequeiraa

Description

While a web app page is open, an evicted location is booted again within a fraction of a second, so idle eviction never takes effect.

Evicting a location stops its MCP servers, and stopServer (packages/core/src/mcp/index.ts) publishes mcp.resources.changed and mcp.status.changed for that location, after location.shutdown. The client's event handler (packages/client/src/solid/data.ts, case "mcp.status.changed" / "mcp.resources.changed") invalidates and re-fetches that location's MCP servers and resources. It does this for any location, including ones the page isn't showing. That GET /api/mcp/resource?location[directory]=… boots the location again: config, plugins, file watchers and every MCP server. The boot then publishes provider.updated, model.updated, agent.updated, command.updated and so on, which the page re-fetches too.

On my desktop service, every location the server had loaded (17 of them) was evicted and booted again within 5 s every hour, all day. In the proxy's access log, the first request after each eviction is that /api/mcp/resource call from an open Firefox tab. A side effect: re-resolving the location un-archives its project. A project I had archived in the database came back at the next eviction.

Possible fixes: don't publish MCP change events while a location shuts down, or have the client skip mcp.*.changed for a location it has no data for (or that just sent location.shutdown).

Plugins

None needed (reproduced with only a stub stdio MCP server)

OpenCode version

2.1.4 (the handlers in data.ts and stopServer are unchanged on dev as of 3de8f3a)

Steps to reproduce

  1. Config with any local MCP server and "experimental": { "location_idle_timeout": "1 minute" }, then opencode serve.
  2. Boot a location: GET /api/location?location[directory]=<project>. Subscribe to /api/event.
  3. With no page open: after about a minute the log shows location services evicted, /api/event shows location.shutdown then mcp.resources.changed and mcp.status.changed for the location, and it stays evicted.
  4. Open the web app (/?auth_token=…) and boot the location again. At each eviction the log shows location services booted about 0.16 s later and the MCP server is launched again. Measured: evicted 20:17:39.985, booted 20:17:40.145; evicted 20:19:40.054, booted 20:19:40.216.
  5. Close the page: the next eviction (20:21:40) sticks.

Screenshot

No response

Operating System

Linux (WSL2 Ubuntu 24.04); web app in Firefox and headless Chrome on Windows

Terminal

N/A (web app)

Activity

  1. opencode-agent commented on Oct 24, 2026

    @opencode-agent
    Contributor

    Thanks for the detailed report. I reproduced this on 2.1.4, and it still happens on the latest dev (0093fe5).

    Setup: a stub stdio MCP server, experimental.location_idle_timeout: "1 minute", opencode serve, and one location booted through /api/location.

    • No page open: the location is evicted and stays evicted. The event stream shows location.shutdown, then mcp.resources.changed and mcp.status.changed.
    • Web app open (headless Chromium): about 50 ms after each location services evicted, the page sends GET /api/mcp/resource?location[directory]=…. The location and its MCP server are then booted again. On 2.1.4: evicted 20:33:01.344, booted 20:33:01.431.
    • dev: same result. Evicted 20:37:00.420, page request at .851, booted 20:37:01.342. In that run the page hadn't loaded that location's catalogs beforehand, so the refetch happens for any location, as you described.

    The mcp.status.changed / mcp.resources.changed handlers in packages/client/src/solid/data.ts still refetch with no check that the location is loaded, and the client doesn't handle location.shutdown. I didn't check the un-archiving side effect. The script below reproduces the eviction and re-boot.

    Reproduction script (repro.sh)
    #!/bin/bash
    # Reproduces #58795: with the web app open, an idle-evicted location is booted again immediately.
    # Needs: node, `npm i playwright && npx playwright install chromium` in this dir, and `opencode` on PATH
    # (or OC_DEV=/path/to/opencode checkout to run from source). Writes page.mjs (headless web app) next to it.
    # From source, build the web app first: (cd $OC_DEV/packages/app && bun run build).
    set -u
    D=$(cd "$(dirname "$0")" && pwd)
    mkdir -p "$D/proj" "$D/home/.config/opencode"
    cat > "$D/mcp-stub.mjs" <<'JS'
    import readline from "node:readline";
    const rl = readline.createInterface({ input: process.stdin });
    process.stderr.write(`stub mcp started at ${new Date().toISOString()}\n`);
    rl.on("line", (l) => { let m; try { m = JSON.parse(l) } catch { return }
      if (m.id === undefined) return; const r = { jsonrpc: "2.0", id: m.id };
      if (m.method === "initialize") r.result = { protocolVersion: m.params?.protocolVersion ?? "2024-11-05", capabilities: { tools: {}, resources: {}, prompts: {} }, serverInfo: { name: "stub", version: "1" } };
      else if (m.method === "tools/list") r.result = { tools: [] };
      else if (m.method === "resources/list") r.result = { resources: [] };
      else if (m.method === "resources/templates/list") r.result = { resourceTemplates: [] };
      else if (m.method === "prompts/list") r.result = { prompts: [] };
      else r.result = {};
      process.stdout.write(JSON.stringify(r) + "\n"); });
    JS
    cat > "$D/page.mjs" <<'JS'
    // Opens the OpenCode web app in headless Chromium, logs every /api/ request with a timestamp.
    // usage: node page.mjs <server password> <seconds>
    import { chromium } from "playwright";
    const [password, secs = "200"] = process.argv.slice(2);
    const token = Buffer.from(`opencode:${password}`).toString("base64");
    const browser = await chromium.launch();
    const page = await browser.newPage();
    page.on("request", (r) => {
      const u = new URL(r.url());
      if (u.pathname.startsWith("/api/") && u.pathname !== "/api/event")
        console.log(new Date().toISOString(), "REQ", r.method(), decodeURIComponent(u.pathname + u.search));
    });
    await page.goto(`http://127.0.0.1:4096/?auth_token=${token}`);
    console.log(new Date().toISOString(), "page loaded");
    await page.waitForTimeout(Number(secs) * 1000);
    await browser.close();
    JS
    CFG="{ \"mcp\": { \"stub\": { \"type\": \"local\", \"command\": [\"node\", \"$D/mcp-stub.mjs\"] } }, \"experimental\": { \"location_idle_timeout\": \"1 minute\" } }"
    echo "$CFG" > "$D/proj/opencode.json"; echo "$CFG" > "$D/home/.config/opencode/opencode.json"
    export PLAYWRIGHT_BROWSERS_PATH=${PLAYWRIGHT_BROWSERS_PATH:-$HOME/.cache/ms-playwright}
    export HOME=$D/home XDG_CONFIG_HOME=$D/home/.config XDG_DATA_HOME=$D/home/.local/share XDG_STATE_HOME=$D/home/.local/state XDG_CACHE_HOME=$D/home/.cache
    cd "$D/proj"
    if [ -n "${OC_DEV:-}" ]; then
      CMD=(bun --conditions=browser --preload="$OC_DEV/packages/tui/node_modules/@opentui/solid/scripts/preload.js" "$OC_DEV/packages/cli/src/index.ts")
    else CMD=(opencode); fi
    "${CMD[@]}" --version
    LOG=$D/serve.log
    "${CMD[@]}" serve --port 4096 --hostname 127.0.0.1 --print-logs --log-level info > "$LOG" 2>&1 &
    SRV=$!
    trap 'kill $SRV 2>/dev/null' EXIT
    for i in $(seq 60); do PW=$(grep -o 'server password [^ ]*' "$LOG" | awk '{print $3}'); [ -n "$PW" ] && break; sleep 1; done
    curl -s -u "opencode:$PW" "http://127.0.0.1:4096/api/location?location%5Bdirectory%5D=$D/proj" >/dev/null  # boot the location
    # Open the web app (it loads the project's location), keep it open through ~3 eviction sweeps (1/minute).
    node "$D/page.mjs" "$PW" 200 > "$D/page.log" 2>&1
    echo "--- server: boots/evictions"; grep -E "location services (booted|evicted)" "$LOG" | sed -E 's/.*timestamp=([^ ]*).*message="([^"]*)".*/\1 \2/'
    echo "--- page: MCP requests"; grep "/api/mcp" "$D/page.log"

    Run it: bash repro.sh

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions