Repository navigation
Description
[Bug]: OpenCode cold-start workspace caches a builtins-only skill list; $ picker stays empty until the directory is rescanned
Before submitting
- I searched existing issues and did not find an exact duplicate. Closest reports are [Bug]: OpenCode worktree snapshot caches a builtins-only /skill: list mid-thread and never refreshes (recovered by "Restart agent session") #16912 (closed, mid-thread rebuild) and [Bug]: ACP Registry provider skills are missing from the / menu in a new thread until the first message is sent #26460 (open, ACP Registry provider, not native OpenCode).
- I included enough detail to reproduce or investigate the problem.
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-managedopencode servefor 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;~/.agentsis a symlinked skills root - Built-in skills returned by OpenCode:
opencode,report,browser. While cold, the picker showed onlyopencodeandreport.
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
- Start T3 Code with the OpenCode provider enabled.
- Open a local project at
<workspace>(a normal project checkout, not a worktree). - Create a new thread and open the composer.
- Type
$. - The picker lists only
opencodeandreport; all 31~/.agents/skillsentries are missing. - 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>
- The first call returns zero skills. The second call returns all 34.
- 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:
- If the first concurrent read returns built-in commands while
skillsis still empty/partial,first.commands.length > 0returns early with the partial skill list. - If the
command.updated/skill.updatedwait 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:
- [Bug]: OpenCode worktree snapshot caches a builtins-only /skill: list mid-thread and never refreshes (recovered by "Restart agent session") #16912 — OpenCode snapshot caches a builtins-only
/skill:list mid-thread (closed; different trigger: registry rebuild, not cold start). - [Bug]: Workspace skill catalogs retain failed discovery and discard newer scan results #16866 — workspace catalogs retain failed discovery and discard newer scans (closed partially; composer retry half remains).
- [Bug]: ACP Registry provider skills are missing from the / menu in a new thread until the first message is sent #26460 — ACP Registry provider skills missing from
/and$in a new thread until the first message (open; same cold-start snapshot shape, different provider). - [Bug]: Workspace snapshot taken before the first Claude probe finishes leaves the / menu without provider commands for the whole session #11575 / [Bug]: Project skills that a new worktree receives after creation never appear in the $ picker #14801 — Claude workspace snapshot taken before the first probe / before worktree skills exist (closed; same snapshot-before-discovery pattern).
- [Bug]: OpenCode attachment is dropped on first send from a brand-new worktree (submit-time skill.list races OpenCode's async skill scan) #29513 / fix(server): OpenCode no longer drops a $skill read before its directory scan ends #29535 — OpenCode
$skilldropped at submit time; explicitly not the$picker path.
Workaround
- Force a second
skill.listfor 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
skillsarray as complete, even whencommandsis non-empty. - Wait for both
command.updatedandskill.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.listblock until the per-location scan completes, or return apendingmarker.
Activity