Skip to content

[Bug]: OpenCode cold-start workspace caches a builtins-only skill list; $ picker stays empty until the directory is rescanned #31042

Closed

Description

@smfelsher

[Bug]: OpenCode cold-start workspace caches a builtins-only skill list; $ picker stays empty until the directory is rescanned

Before submitting

Area

apps/server (OpenCode provider, native provider adapter), with the composer $ picker as the user-visible symptom.

Environment

  • T3 Code: 0.1.6 (Linux desktop build)
  • OpenCode: 2.2.5 (T3-managed opencode serve for the workspace)
  • Provider: OpenCode (native adapter, not ACP Registry)
  • Connection: local
  • Project: a normal local project checkout (not a worktree)
  • Skills: 31 skills under ~/.agents/skills; ~/.agents is a symlinked skills root
  • Built-in skills returned by OpenCode: opencode, report, browser. While cold, the picker showed only opencode and report.

Summary

On a fresh workspace, the first $ picker open shows only the two built-in OpenCode skills (opencode, report). None of the 31 skills in ~/.agents/skills appear.

The skills are present and OpenCode can serve them. The first skill.list read for a cold workspace directory returns zero skills, or only the built-ins, while OpenCode's per-directory scan runs in the background. T3 caches that partial result as the workspace snapshot and does not retry for the five-minute snapshot TTL. The next $ open still shows the builtins-only list.

The state heals immediately if something forces a second skill.list for the directory. In this report, querying the T3-managed OpenCode server directly warmed the scan, and the next $ open listed all 34 skills (31 from ~/.agents + 3 built-ins).

Steps to reproduce

  1. Start T3 Code with the OpenCode provider enabled.
  2. Open a local project at <workspace> (a normal project checkout, not a worktree).
  3. Create a new thread and open the composer.
  4. Type $.
  5. The picker lists only opencode and report; all 31 ~/.agents/skills entries are missing.
  6. Query the T3-managed OpenCode server for the same directory:
    # T3-managed OpenCode server for this workspace; Basic auth uses OPENCODE_PASSWORD
    # from the server process environment.
    GET /api/skill?location[directory]=<workspace>
  7. The first call returns zero skills. The second call returns all 34.
  8. Reopen $; the full list now appears.

Expected behavior

The $ picker lists all registered skills for the workspace as soon as the workspace is opened, without requiring an external read or a provider restart.

A partial or empty first read should not be cached as a complete workspace snapshot.

Actual behavior

The picker lists only the two built-ins for the whole snapshot TTL.

T3's persisted OpenCode cache shows the partial result was saved:

~/.t3/caches/opencode.json
  checkedAt: 2026-11-04T17:39:29.570Z
  skills: []
  workspaceSnapshots: 0

That leaves both fallbacks empty in the composer:

workspaceSnapshots[cwd]?.skills ?? provider.skills
→ [] ?? []

Evidence

Real workspace

First skill.list for the cold project directory:

{"location":{"directory":"<workspace>"},"data":[]}

Second skill.list, immediately after:

{
  "location": {"directory": "<workspace>"},
  "data": [
    {"id": "opencode"}, {"id": "report"}, {"id": "browser"},
    {"id": "ask-matt"}, {"id": "chief-of-staff"}, {"id": "code-review"},
    {"id": "codebase-design"}, {"id": "diagnosing-bugs"},
    {"id": "domain-modeling"}, {"id": "find-skills"}, {"id": "grill-me"},
    {"id": "grill-with-docs"}, {"id": "grilling"}, {"id": "handoff"},
    {"id": "implement-spec"}, {"id": "implement"},
    {"id": "improve-codebase-architecture"}, {"id": "loop-me"}, {"id": "pr"},
    {"id": "prototype"}, {"id": "research"}, {"id": "retro"},
    {"id": "setup-matt-pocock-skills"}, {"id": "tdd"}, {"id": "teach"},
    {"id": "to-questionnaire"}, {"id": "to-spec"}, {"id": "to-tickets"},
    {"id": "triage"}, {"id": "unslop"}, {"id": "wait-what"},
    {"id": "wayfinder"}, {"id": "wizard"}, {"id": "writing-for-agents"}
  ]
}

The builtins-only intermediate state the picker showed matches this synthetic cold-directory test:

# fresh directory with .agents/skills/foo/SKILL.md
GET /api/command?location[directory]=<fresh>  → []
GET /api/skill?location[directory]=<fresh>    → ["opencode", "report"]
GET /api/skill?location[directory]=<fresh>    → ["opencode", "report", "browser", ...all ~/.agents skills..., "foo"]

So OpenCode's /api/skill is eventually consistent per location, and the builtins-only list is a real intermediate state, not a T3-only rendering artifact.

Likely cause

loadOpenCode2Workspace in the OpenCode provider status path:

const first = yield* read;
if (first.commands.length > 0) return first;
yield* scanned.pipe(timeoutOption("10 seconds"));
return yield* read;

Two ways this can cache the partial state:

  1. If the first concurrent read returns built-in commands while skills is still empty/partial, first.commands.length > 0 returns early with the partial skill list.
  2. If the command.updated / skill.updated wait is missed or times out, the reread can still land in the builtins-only window.

The registry then stamps the result with a fresh checkedAt, and PROVIDER_WORKSPACE_SNAPSHOT_TTL_MS = 300_000 suppresses another scan for five minutes. The composer's refresh path also treats an existing/complete-looking snapshot as current, so it does not recover on its own.

This is the same family as:

Workaround

  • Force a second skill.list for the workspace directory (for example, query the T3-managed OpenCode server directly), then reopen $.
  • Or restart the OpenCode provider / T3 Code.
  • Or wait for the five-minute snapshot TTL to expire.

Proposed fix

  • Do not treat a first read with an empty/partial skills array as complete, even when commands is non-empty.
  • Wait for both command.updated and skill.updated, or use a short retry/barrier before publishing the workspace snapshot.
  • In the registry/composer, keep a failed or empty skill scan retryable instead of stamping it with a fresh scan time and honoring the full TTL.
  • Optionally, make OpenCode's skill.list block until the per-location scan completes, or return a pending marker.
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