Skip to content

core: project skills missing from session when instruction baseline is captured before config skill plugin loads #50147

Closed as not planned

Description

@kirin-xiao

Project .opencode/skills/* are sometimes missing from a session's <available_skills> for the whole session, even though they load fine by ID and GET /api/skill lists them for the same location.

Cause: skill discovery is async (the location-scoped ConfigSkillPlugin in packages/core/src/config/plugin/skill.ts scans and registers after boot), but SkillInstructions.load (packages/core/src/skill/instructions.ts) snapshots Skill.list() during SessionContext.select without waiting for that scan. A session started before it completes freezes an incomplete baseline in instruction_state.initial_values that is never corrected.

OpenCode version: v2.0.10

Plugins: none

Steps to reproduce

  1. In a project containing .opencode/skills/foo/SKILL.md, start a session in a location that has not booted yet.
  2. The session's <available_skills> omits foo and stays that way.
  3. Later in the same location, GET /api/skill?location[directory]=<dir> lists foo.

Direct evidence of the race — same fresh directory, repeated calls:

call 1 -> []
call 2 -> ['opencode','report']
call 3 -> ['browser','opencode','openwebui-backend-testing','openwebui-fork-deploy']

Affected sessions have instruction_state.initial_values containing only built-in skills (or built-ins plus global skills), with through_seq frozen at the first step and no instruction_entry rows. Intermittent: some sessions in the same project captured the full list.

Operating System: Linux 6.18.33.2-microsoft-standard-WSL2 (WSL2)

Terminal: xterm-256color

Activity

  1. argszero commented on Sep 20, 2026

    @argszero
    Contributor

    I'd like to work on this.

    I checked the two halves of your diagnosis against the current v2 tree and both hold — the ordering problem is visible in the boot sequence, not only in the timing:

    The scan is not awaited by anything the session can observe. The skill plugin is registered inside the forked boot batch:

    • packages/core/src/plugin/internal.ts:118-121 — add(ConfigSkillPlugin.Plugin) sits in the State.batch(...) that is run with Effect.forkScoped({ startImmediately: true }), so the directory scan it performs (packages/core/src/config/plugin/skill.ts, SkillDiscovery) starts concurrently with location readiness rather than gating it.
    • packages/core/src/session/context.ts:89 obtains SkillInstructions.Service and the baseline is captured from Skill.list() (packages/core/src/skill/instructions.ts:75 — Skill.available(yield* skills.list(), permissions)).
    • The snapshot is then frozen as you described: packages/core/src/session/instruction-state.ts:103-104 writes through_seq + initial_values, and rendering reads initial_values back (:172-173). Nothing re-reads Skill.list() afterwards, so the incomplete list is permanent for that session.

    The mechanism for correcting it already exists and is unused on this path. instructions.ts's update() is written to emit exactly the delta your report would need — "New skills are available in addition to those previously listed:" for additions, and a removal notice — i.e. the design anticipates the available set changing after the first capture. So the defect is not a missing renderer; it is that the change is never detected, because the initial capture happens before the scan that would have produced it.

    Why your "call 1 → [] / call 3 → full list" evidence fits: the first caller in a fresh location captures the empty list while the scan is still running, and every later caller sees the completed one.

    Two directions, and I'd like your call before writing code:

    1. Gate the baseline on plugin readiness — have the location's instruction capture wait for the skill plugin's first scan to settle (mirroring how other location-scoped plugins are sequenced), so initial_values is never written from a partially-populated registry.
    2. Let the existing delta path do the work — after the scan settles, re-evaluate SkillInstructions.load and let instruction-state diff it into a "new skills are available" entry. This reuses update() rather than adding a wait, and it also covers skills that appear later (a SKILL.md added mid-session, which the file watcher already subscribes to).

    My default is (2), because the delta machinery is already there and it fixes the class rather than the window — but (1) is the smaller change if you consider "a session must be complete at creation" the contract. Either way the regression test is the same shape as your repro: create a session in a location whose skill scan has not completed and assert the skill appears in the session's skill guidance (and that a session created after the scan is unaffected).

    Happy to have the issue assigned if either direction looks right.

  2. caferpgit commented on Oct 19, 2026

    @caferpgit

    Reproduced on the desktop app (Windows 11), and it is per-workspace, not a fixed folder set.

    reading value
    opencode desktop 1.19.3 (Windows 11 Home)
    skills advertised in-session (this workspace) 28
    opencode debug skill (same folder, same machine) 32
    the 4 that differ global config-dir skills under ~/.config/opencode/skills/
    init count= logged for four workspaces in one app launch 36 · 32 · 29 · 28

    Three things I can add to the analysis above:

    1. Not CLI-vs-desktop only, and not a fixed set of folders. In a single desktop launch the same global-config directory is registered in one workspace (one logged 32) and not in another (this one logged 28).
    2. The logged init count itself is short, not only the session's instruction baseline — the shortfall is visible in the registry the app reports, not just in the frozen session list.
    3. skills.paths is not a reliable workaround either: adding ~/.config/opencode/skills to skills.paths did not restore the four in this workspace, which is consistent with a timing/race cause rather than a scan-pattern one.

    Raw evidence: <available_skills> count 28; app log message=init count=28 for this workspace; opencode debug skill listing 32 with the four present.

    No fix expectation here — just corroborating Windows/desktop data for the direction (1) vs (2) question above.

  3. opencodejr commented on Oct 28, 2026

    @opencodejr
    Contributor

    Thanks for the detailed report. I tried to reproduce this on v2.0.10 and on the latest dev (fa1260d), on native Linux, against a mock provider:

    • The skill list does fill in late. For a fresh directory, GET /api/skill returned [], then only the built-ins, and a few seconds later the project skill too, as in your evidence.
    • But sessions still got the project skill. I created a session in a location that had never been used and prompted it straight away. I also tried opencode run with --standalone, through the background service, and with --server, plus a project with 400 skills. Every time, the first model request already listed the project skill in <available_skills>.
    • Late skills were added on the next turn. When I added a skill after the first turn, the next turn included a <system-update> saying "New skills are available…", and instruction_state.through_seq moved forward. So in my tests the baseline did not stay frozen.

    These details would help narrow it down:

    1. Where does the project live under WSL2? Is it on the Linux filesystem or under /mnt/c/...? Discovery is much slower on /mnt/c.
    2. How do you start the sessions: the TUI (opencode), opencode run, the desktop or web app, ACP, or a script calling the API?
    3. Did the affected sessions have more than one turn? If a later turn still didn't list the skill, that is the key detail.
    4. If you can, server logs from a session where it happens (start the server with opencode serve --print-logs --log-level debug, or use --standalone --print-logs).
  4. opencodejr commented on Oct 31, 2026

    @opencodejr
    Contributor

    I'm closing this for now because there hasn't been a reply to the questions above. I couldn't reproduce it on v2.0.10 or on the latest dev (fa1260d). In my tests on native Linux, every new session had the project skill in <available_skills>, even when I prompted it in a location that had never been used. Skills that appeared after the first turn were added on the next turn through a <system-update>.

    If it still happens, please comment here and we can reopen it. These details would help most:

    • whether the project lives under /mnt/c in WSL2
    • how you start the sessions (TUI, opencode run, the desktop app, ACP or the API)
    • whether a later turn in an affected session still leaves the skill out
    • debug logs from a session where it happens

    Thanks for the detailed report.

  5. kirin-xiao commented on Nov 1, 2026

    @kirin-xiao
    Author

    Retest request: #51133 (b64cc80db3, Oct 24) makes the runner wait for plugin activation before each step and retry. It's in dev (fa1260d) but not v2.0.10, so the dev retest couldn't hit the original window.

    Note that a short GET /api/skill is not this bug: that handler doesn't wait for activation, so it can show [] → built-ins → full mid-activation.

    Please retest on a build with b64cc80db3 and reply with: WSL /mnt/c vs Linux FS, client, whether a later turn still omits the skill, and debug logs.

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions