an opencode agent could not look at a calendar all afternoon. i treated it as a
framing problem, then a regression, then a free-vs-paid model problem, and it was
none of those. the documentation says all four, plainly.
1. A SKILL'S `name` IS AN IDENTIFIER, not a label. it must match
^[a-z0-9]+(-[a-z0-9]+)*$ and equal its directory. every skill here carried a
human title there — "Calendar and contacts" — so every one was LISTED and none
could be LOADED. the agent found the skill, called
skill({name:"calendar-and-contacts"}), and got "Unable to load skill". `title`
is now a separate field and keeps the prose.
2. THERE IS NO `skills.paths` CONFIG KEY. it is in the binary's own config
example and not in the docs, and the difference is exactly this: skills
configured that way are offered to the agent and cannot be opened. skills load
from fixed discovery paths only, so each directory is now SYMLINKED into
~/.config/opencode/skills. links, not copies — one source of truth, and a skill
edited in its plugin is live immediately.
3. `prompt` IS A FILE REFERENCE — "{file:./path}". passing the text is parsed
without complaint and never applied. this is what made it look like a model
problem: the agent existed, the prompt was on its record, and the model had
never seen a word of it.
4. THE `skill` PERMISSION DEFAULTS TO ASKING, and a server session has nobody to
ask, so the call errors rather than waiting. {"skill":{"*":"allow"}}.
also: the platform cached opencode's model list for the lifetime of `officer`,
so signing into a provider and restarting the SIDECAR left the old catalogue in
the OTHER process — six free models, no paid ones, nothing saying why. the cache
is dropped when the sidecar reports a new port, which is the one event that
always accompanies a new serve.
verified: the prompt reaches the model — asked who it is, it answered "Officer,
running on your home server at officer-dev under your user account". 10 skills
discovered through the linked path with loadable names. NOT verified end to end:
a turn that opens the calendar skill and answers from it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043
41 lines
1.8 KiB
TypeScript
41 lines
1.8 KiB
TypeScript
import * as sidecar from '@@/sidecar-registry';
|
|
import { logger } from '../logger';
|
|
import { setOpenCodeSession } from './state';
|
|
import { invalidateModelCache } from '../list-models';
|
|
|
|
// The OpenCode sidecar (officer-opencode) starts its `opencode serve` on a random port and reports it
|
|
// here on connect. We remember it so the OpenCode harness always routes to the current server. It also
|
|
// reports the OpenCode `ses_…` id it created for each live turn, which we persist so the next turn can
|
|
// resume it (`opencode run --session …`).
|
|
|
|
let serverPort: number | null = null;
|
|
|
|
sidecar.on('opencode:server', (msg) => {
|
|
const port = (msg as { port?: number }).port;
|
|
if (typeof port !== 'number') return;
|
|
serverPort = port;
|
|
/*
|
|
A new serve means a possibly different catalogue, so the cached model list has to go.
|
|
|
|
`listOpenCodeModels` caches for the platform process's lifetime, which is right — the catalogue moves
|
|
about once a release. But it is the PLATFORM that holds it and the SIDECAR that changes, so signing
|
|
into a provider and restarting `officer-opencode` left the platform serving the list it read before
|
|
the login: six free models, no paid ones, and nothing anywhere saying why. Reported 2026-08-27, after
|
|
a successful login that appeared to do nothing.
|
|
|
|
The port report is the one event that always accompanies a new serve, which makes it the right place
|
|
to drop the cache.
|
|
*/
|
|
invalidateModelCache();
|
|
logger.info('OpenCode sidecar server registered', { port, url: getOpenCodeServerUrl() });
|
|
});
|
|
|
|
sidecar.onOpenCodeSession((sessionKey, sessionId) => {
|
|
setOpenCodeSession(sessionKey, sessionId);
|
|
});
|
|
|
|
/** The base URL of the sidecar's OpenCode server, or null if the sidecar hasn't reported in yet. */
|
|
export function getOpenCodeServerUrl(): string | null {
|
|
return serverPort ? `http://127.0.0.1:${serverPort}` : null;
|
|
}
|