route OpenCode chat through the officer-opencode sidecar
Turns now run in the sidecar via `opencode run --dir <cwd> --format json
--dangerously-skip-permissions [-s <ses_>]` instead of the serve's
`POST /session/{id}/message` path. That path was unreliable at reporting
tool completion — tools finished but the turn stayed status=running,
wedging the UI at "Working…". `run` re-anchors tools to the chat cwd via
--dir, reports completion faithfully, and exits when done.
- runner.ts (new): spawn `run`, map its JSON events (text/tool_use/
step_finish) to ChatEvent, report the `ses_` id for resume, accumulate
cost; inactivity (120s) + hard-cap (10min) watchdogs kill a hung turn
and emit a clean error instead of hanging forever.
- protocol.ts: opencode:run-streaming/kill commands; opencode:spawned/
event/session events; OpenCodeRunParams.
- sidecar index.ts: wire run/kill; sweepStaleServes() on startup kills
only an `opencode serve` whose resolved /proc/<pid>/cwd == SERVE_CWD,
so an unclean prior exit can't leave two.
- sidecar-registry.ts: spawnOpenCodeStreaming/killOpenCode/onOpenCodeEvent/
onOpenCodeSession helpers.
- send-opencode.ts: rewritten to mirror send-claude-code (subscribe →
resolve resume id → spawn → kill handle).
- sidecar-server.ts: persist reported ses_ id into state for resume.
- list-models/server-manager: route to the sidecar's reported serve URL.
The serve stays up only for read-only calls that never hung (model
listing, session history).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import type { ModelInfo } from './types';
|
||||
import { OPENCODE_SERVER_URL } from './opencode/server-manager';
|
||||
import { getOpenCodeServerUrl } from './opencode/sidecar-server';
|
||||
|
||||
// The Claude harness runs the `claude` CLI, so its tiers are a fixed set.
|
||||
const CLAUDE_CODE_MODELS: ModelInfo[] = [
|
||||
@@ -24,8 +24,10 @@ type ProvidersResponse = {
|
||||
// defaults for now.
|
||||
async function listOpenCodeModels(): Promise<ModelInfo[]> {
|
||||
if (openCodeCache) return openCodeCache;
|
||||
const baseUrl = getOpenCodeServerUrl();
|
||||
if (!baseUrl) return []; // sidecar hasn't reported its server yet
|
||||
try {
|
||||
const res = await fetch(`${OPENCODE_SERVER_URL}/config/providers`, { signal: AbortSignal.timeout(5000) });
|
||||
const res = await fetch(`${baseUrl}/config/providers`, { signal: AbortSignal.timeout(5000) });
|
||||
if (!res.ok) return [];
|
||||
const data = (await res.json()) as ProvidersResponse;
|
||||
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
// The OpenCode server is a fixed, pm2-managed process (see officer-opencode in ecosystem.config.cjs)
|
||||
// listening on a known port — not spawned per-cwd by us. All OpenCode chat + session traffic goes to
|
||||
// this one server; sessions live in its project store. Override the URL with OPENCODE_SERVER_URL.
|
||||
import { getOpenCodeServerUrl } from './sidecar-server';
|
||||
|
||||
export const OPENCODE_SERVER_URL = process.env.OPENCODE_SERVER_URL || 'http://127.0.0.1:4096';
|
||||
// The OpenCode server is owned by the officer-opencode sidecar, which starts `opencode serve` on a
|
||||
// random port (cwd = DATA_PATH/opencode-sidecar) and reports it to the API (getOpenCodeServerUrl).
|
||||
// All OpenCode HTTP traffic routes to whatever port the sidecar last reported.
|
||||
|
||||
export async function isServerHealthy(): Promise<boolean> {
|
||||
const baseUrl = getOpenCodeServerUrl();
|
||||
if (!baseUrl) return false;
|
||||
try {
|
||||
const res = await fetch(`${OPENCODE_SERVER_URL}/api/health`, { signal: AbortSignal.timeout(3000) });
|
||||
const res = await fetch(`${baseUrl}/api/health`, { signal: AbortSignal.timeout(3000) });
|
||||
return res.ok;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** The base URL of the fixed OpenCode server. */
|
||||
/** The base URL of the OpenCode server the sidecar is running. Throws if it hasn't reported in yet. */
|
||||
export async function ensureServer(): Promise<{ baseUrl: string }> {
|
||||
return { baseUrl: OPENCODE_SERVER_URL };
|
||||
const baseUrl = getOpenCodeServerUrl();
|
||||
if (!baseUrl) throw new Error('OpenCode sidecar server not available yet');
|
||||
return { baseUrl };
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import * as sidecar from '@@/sidecar-registry';
|
||||
import { logger } from '../logger';
|
||||
import { setOpenCodeSession } from './state';
|
||||
|
||||
// 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.
|
||||
// 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;
|
||||
|
||||
@@ -13,6 +16,10 @@ sidecar.on('opencode:server', (msg) => {
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user