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; }