Skip to content

location: deleted-and-recreated directory keeps its stale loaded config/plugins #55168

Open

Description

@graphenn

Summary

The background service caches loaded locations by directory path. When a directory is
deleted and recreated at the same path, the cached location is never invalidated: a newly
connected client receives the previously loaded location, so configuration and plugins are
those of the old incarnation. New/changed plugin files in the recreated directory are never
scanned; reopening the window does not help, and the watchers that were subscribed to the
old files are gone with the deleted directory.

Environment

  • opencode version: 2.0.28
  • OS: Linux 7.0.0-38-generic (Ubuntu 24.04, x86_64)
  • Terminal: TERM_PROGRAM=herdr, TERM=xterm-256color, COLORTERM=truecolor
  • Shell: /bin/sh
  • Install/channel: latest (bun global install of @opencode/cli)
  • Active plugins: several project-local plugins under .opencode/plugin/, plus one
    file:// plugin from the global config

Reproduction

Setup: a checkout at $REPO with project-local plugins in $REPO/.opencode/plugin/*, plus
short-lived nested git worktrees at $REPO/sessions/<name> (linked worktrees of the same
repo, so each contains another copy of .opencode/plugin/* at the same relative paths).
The shared background service stays running the whole time.

  1. Open a client in $REPO/sessions/agent0. Its plugin set is loaded; confirm with
    GET /api/plugin?location[directory]=<dir> — e.g. the .session copy of the plugins is
    registered.
  2. Close the client. Delete the worktree directory entirely (git worktree remove + rm),
    then re-create it at the same path (git worktree add … sessions/agent0).
  3. Open a new client in $REPO/sessions/agent0.
  4. GET /api/debug/location still lists the directory, and
    GET /api/plugin?location[directory]=<dir> still returns the plugin set from the old
    incarnation — the new copy of the plugins is never registered.

Expected Behavior

The server should detect that the directory was removed and recreated (identity/instance
change, or simply that it is missing and came back) and rebuild the loaded location, so the
client gets config/plugins that match the on-disk state. At minimum, provide a supported
(non-debug) way to evict/refresh a single location.

Actual Behavior

The new client keeps the stale location. Plugin-provided tools/guards silently do not
exist because the plugin files of the recreated directory are never loaded. Reopening the
window does not rescan. Verified workaround on 2.0.28 (rebuilds the plugin set correctly):

DELETE /api/debug/location?location[directory]=<dir>
GET    /api/location?location[directory]=<dir>

Additional Context

Activity

  1. opencode-agent commented on Oct 13, 2026

    @opencode-agent
    Contributor

    Thanks for the detailed report. I reproduced this on 2.0.28, and it still happens on the latest dev (23e3493).

    What I did, using the background service:

    1. Created a repo with .opencode/plugin/alpha.ts and a linked worktree at sessions/agent0, then loaded that location. /api/plugin lists both copies of alpha.
    2. Ran git worktree remove + rm, waited a few seconds, then ran git worktree add at the same path. I also added a new beta.ts plugin and changed alpha.ts.
    3. Requested the location again. /api/debug/location still lists it, but /api/plugin only shows the plugin from the main checkout. The recreated worktree's plugins are never loaded.
    4. Your workaround (DELETE /api/debug/location, then GET /api/location) loads them correctly.

    The delay between deleting and recreating matters. When I recreated the worktree immediately, the server noticed the change and loaded the new plugins. With a gap of a few seconds it never does. The script below reproduces it.

    Related: #54827 (stale location never pruned after a worktree is removed) and #51198 (debug evict behaviour).

    Reproduction script (repro.sh)
    #!/usr/bin/env bash
    # Reproduce #55168: a deleted-and-recreated directory keeps its stale loaded location/plugins.
    # Usage: bash repro.sh                      (uses `opencode` from PATH)
    #        OC_CWD=/workspace/repo/packages/cli OC="bun --conditions=browser --preload=/workspace/repo/packages/tui/node_modules/@opentui/solid/scripts/preload.js /workspace/repo/packages/cli/src/index.ts" bash repro.sh   (from source)
    set -u
    OC=${OC:-opencode}
    mkdir -p /tmp/opencode
    ROOT=$(mktemp -d /tmp/opencode/r55168.XXXX)
    export HOME=$ROOT/home XDG_CONFIG_HOME=$ROOT/home/.config XDG_DATA_HOME=$ROOT/home/.local/share \
      XDG_STATE_HOME=$ROOT/home/.local/state XDG_CACHE_HOME=$ROOT/home/.cache
    mkdir -p "$HOME"
    REPO=$ROOT/repo
    mkdir -p "$REPO/.opencode/plugin" && cd "$REPO"
    git init -q && git config user.email a@b && git config user.name a
    echo 'export default { id: "alpha", setup: async () => {} }' > .opencode/plugin/alpha.ts
    git add -A && git commit -qm init
    git worktree add -q sessions/agent0 >/dev/null 2>&1
    DIR=$REPO/sessions/agent0
    Q="location%5Bdirectory%5D=$DIR"
    
    api() { (cd "${OC_CWD:-$REPO}" && $OC api "$@" 2>/dev/null); }
    plugins() { # user plugins of the location, waiting for the async plugin load
      for i in $(seq 1 20); do
        out=$(api GET "/api/plugin?$Q" | jq -r '.data[] | select(.source.type!="builtin") | "\(.id)  \(.source.path)  inode=\(.source.path)"' 2>/dev/null)
        [ -n "$out" ] && break; sleep 0.5
      done
      echo "$out" | awk '{print "   ", $1, $2}'
    }
    
    (cd "${OC_CWD:-$REPO}" && $OC service start) >/dev/null 2>&1; sleep 1
    echo "== 1. first incarnation of $DIR (plugin: alpha)"
    api GET "/api/location?$Q" >/dev/null
    plugins
    
    echo "== 2. git worktree remove + rm, then git worktree add at the same path; add plugin beta"
    git worktree remove --force sessions/agent0; rm -rf sessions/agent0
    sleep ${GAP:-5}
    git worktree add -q sessions/agent0 >/dev/null 2>&1
    echo 'export default { id: "beta", setup: async () => {} }' > "$DIR/.opencode/plugin/beta.ts"
    echo 'export default { id: "alpha_v2", setup: async () => {} }' > "$DIR/.opencode/plugin/alpha.ts"
    echo "   on disk: $(ls "$DIR/.opencode/plugin" | tr '\n' ' ')"
    sleep 3
    
    echo "== 3. new client on the recreated directory"
    api GET "/api/location?$Q" >/dev/null
    echo "   loaded locations: $(api GET /api/debug/location | jq -c '[.[].directory]')"
    echo "   plugins (expected alpha + beta):"
    plugins
    
    echo "== 4. workaround: DELETE /api/debug/location, then GET /api/location"
    api DELETE "/api/debug/location?$Q" >/dev/null
    api GET "/api/location?$Q" >/dev/null
    sleep 1
    plugins
    (cd "${OC_CWD:-$REPO}" && $OC service stop) >/dev/null 2>&1
    true

    Run it: bash repro.sh

  2. nakulmaurya commented on Oct 13, 2026

    @nakulmaurya

    I am investigating a focused fix on current dev (23e3493). I reproduced stale config/service reuse in the existing location-layer suite through get, contextEffect, and contextEffectOption. The candidate change compares local directory identity before reusing a cached graph and uses the existing invalidate/shutdown path when it changes. Workspace locations remain exempt from local probes, and optional lookups still do not create a graph.

    I will add deletion/recreation and cache-preservation coverage and run the package checks before opening a PR. This is separate from #54878, which skips missing locations during explicit reload. Please let me know if this is already being worked on or a different invalidation approach is preferred. AI assistance is being used; I am running the checks locally.

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