OpenCode can't set a real per-session cwd (every session runs in the fixed server's
dir), so we tell the model its working directory via a system prompt appended to
OpenCode's own — sent as a system message, so it never appears in the visible chat
(verified against source + live). Claude doesn't need this (it honors cwd natively).
- client.postMessage(…, system?) forwards a `system` string on the message.
- send-opencode builds the Officer prompt from `workingDir` and sends it every turn.
- websocket: workingDir = the resolved cwd, except the general /chat (whose cwd is the
claude_sessions grouping placeholder) uses the user's home.
Verified live: with the prompt, the model reports the injected dir as its cwd.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
OpenCode has no per-session directory (every session runs in the fixed server's cwd),
so sessions from every context (/chat pwd, email account, project) all landed in one
list. Now each session is tagged on creation with its logical cwd via the free-form
session `metadata`: { officer: { cwd } } — API-settable, round-trips on list+detail,
never touched by opencode core (confirmed by source dive + live test).
- client.createSession(metadata?) sends `metadata`; adds OfficerSessionMeta + officerMeta() helper.
- send-opencode tags new sessions with { officer: { cwd } } (the resolved chat cwd).
- websocket: pass the full resolved cwd for every context (not just non-/chat).
- listOpenCodeSessions(cwd?) filters by metadata.officer.cwd; chat.ts passes the request cwd.
Verified live: sessions tagged with distinct cwds list only under their own cwd.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
OpenCode has no per-session `directory` — a session inherits the server's cwd (POST
/session ignores extra fields). So the previous "pass directory on create" was a no-op
and every OpenCode chat ran in the fixed server's dir (~), including the email chat.
Fix: hybrid server model. server-manager.ensureServer(cwd?, home?) returns the fixed
pm2 server (OPENCODE_SERVER_URL, :4096) for the general /chat, but for a context-scoped
cwd (email account dir, project dir) it spawns/pools an `opencode serve` rooted at that
directory — so the session's agent actually operates there. send-opencode passes the
resolved cwd + the user's home; createSession drops the ignored directory param.
Verified live: a cwd-scoped server reports the session directory as the target dir
(not ~), while /chat still uses :4096.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The email chat's working directory now resolves to
DATA_PATH/<owner>/email_accounts/<accountEmail> (created if missing), so the agent
operates in the selected account's dir (emails.db, attachment_cache, …).
- websocket.ts: new resolveChatCwd — context 'email' → the account dir (via a new
resolveEmailCwd), 'chat' → the pwd/claude_sessions dir, else the given cwd. Both the
Claude and OpenCode handlers use it. The account defaults to the owner's first enabled
account for now; the account selector will pass it as contextId later.
- OpenCode honors the cwd again: send-opencode passes it as the session `directory`
(client.createSession(directory?)) for context-scoped chats; the general /chat still
omits it and uses the fixed server's default project. Verified against the live server
that directory-bound sessions create + list.
No frontend change — the /email panel already sends context:'email'.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The per-cwd `opencode serve` spawning is replaced by a single fixed server
(http://127.0.0.1:4096, OPENCODE_SERVER_URL) managed by pm2 — added as
`officer-opencode` in ecosystem.config.cjs (cwd = home).
Root-cause fix for the empty model selector: list-models shelled out to
`opencode models`, which failed at runtime on the deployed server (the picker got
only Claude tiers). It now reads the fixed server's GET /config/providers over HTTP —
11ms and reliable — so the curated OpenCode models (Big Pickle, Claude Haiku) show up.
- server-manager.ts — drops spawning; exposes OPENCODE_SERVER_URL + a health check.
- client.ts — createSession no longer binds a directory (sessions live in the one
server's project).
- send-opencode.ts / opencode-sessions.ts / chat.ts — use the fixed server; drop the
cwd/home plumbing. Session list/load/delete/rename now hit :4096.
Verified end-to-end against the live server: model list, streaming turn, session
list, and transcript load all work with no spawning.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
/chat's session list, transcript load, delete, and rename now span both harnesses.
- opencode-sessions.ts — REST-backed reader (OpenCode's SQLite via its HTTP API, never
the DB): listOpenCodeSessions / loadOpenCodeSession / delete / rename, returning the
same shapes as the Claude reader, tagged harness:'opencode'. A serve is directory-
scoped, so listing a cwd = asking the serve rooted there. Transcript rebuild maps
user/assistant/tool parts and drops reasoning (parity with the delta filter).
- client.ts — adds listSessions/getMessages/deleteSession/renameSession over /session/*.
- chat.ts — /sessions merges both (newest first); /sessions/:id, DELETE, and
/title route by id shape (ses_ = OpenCode). ClaudeSessionSummary gains an optional
`harness` tag.
- send-opencode.ts — resuming from history: when the sessionKey is itself a ses_ id,
reuse that OpenCode session instead of creating a new one.
- SessionList.tsx — shows an "OpenCode" badge for OpenCode sessions.
Verified end-to-end against a live serve: list (5 sessions, tagged), load (transcript
rebuilt, reasoning filtered), and rename all work. Phases 1-3 complete; needs a restart.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Introduces an OpenCode chat harness alongside Claude, driven over HTTP + SSE against
a persistent `opencode serve`, emitting the same ChatEvent contract so the entire
chat UI and createEventHandler pipeline are unchanged.
New servers/api/chat/opencode/:
- server-manager.ts — one warm `opencode serve` per cwd (free port, health-gated,
respawn on exit; HOME set so it reads the user's ~/.local/share/opencode auth).
Binary pinned via OPENCODE_BIN (installed is 1.17.9; the 1.18.4 upgrade never landed).
- client.ts — per-server HTTP calls (/session create, /message, /abort) + a single
reconnecting `/event` SSE stream demuxed to per-session listeners.
- event-mapper.ts — SSE → ChatEvent. Verified live against 1.17.9: message.part.delta
→ delta, tool parts → tool:start/tool:result, message.updated → cost, session.idle
→ result. Crucially, deltas are gated on partID being a `text` part (declared before
its deltas) so the model's reasoning — which also streams as field:'text' — is
dropped, matching the Claude harness hiding thinking.
- state.ts — sessionKey ↔ opencode ses_ id map for resume.
channels/send-opencode.ts — the OpenCode analog of send-claude-code: ensure serve,
create/reuse session, subscribe, post the message, forward mapped events; kill = abort.
websocket.ts — replaces the Claude-only coercion with harness routing:
provider 'claude-code' → Claude sidecar, everything else → handleOpenCodeChat.
handleStop aborts the right harness.
Verified end-to-end (streaming text, tool call/result, cost, abort) against a
throwaway serve using the free deepseek model — no prod restart involved. UI-level
model selection + session history follow in Phases 2–3.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>